From 120a142733cbee88b464883c9add9697c71086fc Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 3 Mar 2016 16:12:48 +0000 Subject: [PATCH 001/208] Add in required abstracted functions to CollaboratorsHandler --- .../Collaborators/CollaboratorsHandler.coffee | 40 +++++++++ .../CollaboratorsHandlerTests.coffee | 87 +++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 4ab855af80..d574a0b263 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -6,8 +6,48 @@ logger = require('logger-sharelatex') UserGetter = require "../User/UserGetter" ContactManager = require "../Contacts/ContactManager" CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" +async = require "async" module.exports = CollaboratorsHandler = + getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> + Project.findOne { _id: project_id }, { collaberator_refs: 1, readOnly_refs: 1 }, (error, project) -> + return callback(error) if error? + return callback null, null if !project? + members = [] + for member_id in project.readOnly_refs or [] + members.push { id: member_id, privilegeLevel: "readOnly" } + for member_id in project.collaberator_refs or [] + members.push { id: member_id, privilegeLevel: "readAndWrite" } + return callback null, members + + getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + return callback(error) if error? + async.mapLimit (members or []), 3, + (member, cb) -> + UserGetter.getUser member.id, (error, user) -> + return cb(error) if error? + return cb(null, { user: user, privilegeLevel: member.privilegeLevel }) + callback + + getMemberCount: (project_id, callback = (error, count) ->) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + return callback(error) if error? + return callback null, (members or []).length + + isUserMemberOfProject: (user_id, project_id, callback = (error, isMember, privilegeLevel) ->) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + return callback(error) if error? + for member in members or [] + if member.id.toString() == user_id.toString() + return callback null, true, member.privilegeLevel + return callback null, false, null + + getProjectsUserIsMemberOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) -> + Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=> + Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=> + callback(err, readAndWriteProjects, readOnlyProjects) + removeUserFromProject: (project_id, user_id, callback = (error) ->)-> logger.log user_id: user_id, project_id: project_id, "removing user" conditions = _id:project_id diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 1fd3a0c2d5..631559b84e 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -22,6 +22,93 @@ describe "CollaboratorsHandler", -> @adding_user_id = "adding-user-id" @email = "joe@sharelatex.com" @callback = sinon.stub() + + describe "getMemberIdsWithPrivilegeLevels", -> + beforeEach -> + @Project.findOne = sinon.stub() + @Project.findOne.withArgs({_id: @project_id}, {collaberator_refs: 1, readOnly_refs: 1}).yields(null, @project = { + readOnly_refs: [ "read-only-ref-1", "read-only-ref-2" ] + collaberator_refs: [ "read-write-ref-1", "read-write-ref-2" ] + }) + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, @callback + + it "should return an array of member ids with their privilege levels", -> + @callback + .calledWith(null, [ + { id: "read-only-ref-1", privilegeLevel: "readOnly" } + { id: "read-only-ref-2", privilegeLevel: "readOnly" } + { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } + { id: "read-write-ref-2", privilegeLevel: "readAndWrite" } + ]) + .should.equal true + + describe "getMembersWithPrivilegeLevels", -> + beforeEach -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels.withArgs(@project_id).yields(null, [ + { id: "read-only-ref-1", privilegeLevel: "readOnly" } + { id: "read-only-ref-2", privilegeLevel: "readOnly" } + { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } + { id: "read-write-ref-2", 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" }) + @CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback + + it "should return an array of members with their privilege levels", -> + @callback + .calledWith(undefined, [ + { user: { _id: "read-only-ref-1" }, privilegeLevel: "readOnly" } + { user: { _id: "read-only-ref-2" }, privilegeLevel: "readOnly" } + { user: { _id: "read-write-ref-1" }, privilegeLevel: "readAndWrite" } + { user: { _id: "read-write-ref-2" }, privilegeLevel: "readAndWrite" } + ]) + .should.equal true + + describe "isUserMemberOfProject", -> + beforeEach -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + + describe "when user is a member of the project", -> + beforeEach -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels.withArgs(@project_id).yields(null, [ + { id: "not-the-user", privilegeLevel: "readOnly" } + { id: @user_id, privilegeLevel: "readAndWrite" } + ]) + @CollaboratorHandler.isUserMemberOfProject @user_id, @project_id, @callback + + it "should return true and the privilegeLevel", -> + @callback + .calledWith(null, true, "readAndWrite") + .should.equal true + + describe "when user is not a member of the project", -> + beforeEach -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels.withArgs(@project_id).yields(null, [ + { id: "not-the-user", privilegeLevel: "readOnly" } + ]) + @CollaboratorHandler.isUserMemberOfProject @user_id, @project_id, @callback + + it "should return false", -> + @callback + .calledWith(null, false, null) + .should.equal true + + describe "getProjectsUserIsMemberOf", -> + beforeEach -> + @fields = "mock fields" + @Project.find = sinon.stub() + @Project.find.withArgs({collaberator_refs:@user_id}, @fields).yields(null, ["mock-read-write-project-1", "mock-read-write-project-2"]) + @Project.find.withArgs({readOnly_refs:@user_id}, @fields).yields(null, ["mock-read-only-project-1", "mock-read-only-project-2"]) + @CollaboratorHandler.getProjectsUserIsMemberOf @user_id, @fields, @callback + + it "should call the callback with the projects", -> + @callback + .calledWith(null, ["mock-read-write-project-1", "mock-read-write-project-2"], ["mock-read-only-project-1", "mock-read-only-project-2"]) + .should.equal true describe "removeUserFromProject", -> beforeEach -> From 1a689aa1fd63ed7204dfb80af3c714ec91b2d204 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 3 Mar 2016 17:19:03 +0000 Subject: [PATCH 002/208] Move findAllUsersProjects from Project to ProjectGetter --- .../Features/Project/ProjectController.coffee | 3 ++- .../Features/Project/ProjectGetter.coffee | 9 +++++++++ .../Features/Project/ProjectLocator.coffee | 4 +++- services/web/app/coffee/models/Project.coffee | 6 ------ .../Project/ProjectControllerTests.coffee | 6 ++++-- .../coffee/Project/ProjectGetterTests.coffee | 17 ++++++++++++++++- .../coffee/Project/ProjectLocatorTests.coffee | 9 +++++---- 7 files changed, 39 insertions(+), 15 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 7b36465f9f..b30c2b8f91 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -17,6 +17,7 @@ SecurityManager = require("../../managers/SecurityManager") fs = require "fs" InactiveProjectManager = require("../InactiveData/InactiveProjectManager") ProjectUpdateHandler = require("./ProjectUpdateHandler") +ProjectGetter = require("./ProjectGetter") module.exports = ProjectController = @@ -129,7 +130,7 @@ module.exports = ProjectController = notifications: (cb)-> NotificationsHandler.getUserNotifications user_id, cb projects: (cb)-> - Project.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb + ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb hasSubscription: (cb)-> LimitationsManager.userHasSubscriptionOrIsGroupMember req.session.user, cb user: (cb) -> diff --git a/services/web/app/coffee/Features/Project/ProjectGetter.coffee b/services/web/app/coffee/Features/Project/ProjectGetter.coffee index 129ff831dc..826ea4116c 100644 --- a/services/web/app/coffee/Features/Project/ProjectGetter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectGetter.coffee @@ -2,6 +2,8 @@ mongojs = require("../../infrastructure/mongojs") db = mongojs.db ObjectId = mongojs.ObjectId async = require "async" +Project = require("../../models/Project").Project +CollaboratorsHandler = require "../Collaborators/CollaboratorsHandler" module.exports = ProjectGetter = EXCLUDE_DEPTH: 8 @@ -27,6 +29,13 @@ module.exports = ProjectGetter = else if query instanceof ObjectId query = _id: query db.projects.findOne query, projection, callback + + findAllUsersProjects: (user_id, fields, callback = (error, ownedProjects, readAndWriteProjects, readOnlyProjects) ->) -> + Project.find {owner_ref: user_id}, fields, (error, projects) -> + return callback(error) if error? + CollaboratorsHandler.getProjectsUserIsMemberOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) -> + return callback(error) if error? + callback null, projects, readAndWriteProjects, readOnlyProjects populateProjectWithUsers: (project, callback=(error, project) ->) -> # eventually this should be in a UserGetter.getUser module diff --git a/services/web/app/coffee/Features/Project/ProjectLocator.coffee b/services/web/app/coffee/Features/Project/ProjectLocator.coffee index fe60e0ba3e..0302bdc11b 100644 --- a/services/web/app/coffee/Features/Project/ProjectLocator.coffee +++ b/services/web/app/coffee/Features/Project/ProjectLocator.coffee @@ -3,6 +3,7 @@ Errors = require "../../errors" _ = require('underscore') logger = require('logger-sharelatex') async = require('async') +ProjectGetter = require "./ProjectGetter" module.exports = findElement: (options, _callback = (err, element, path, parentFolder)->)-> @@ -126,7 +127,8 @@ module.exports = async.waterfall jobs, callback findUsersProjectByName: (user_id, projectName, callback)-> - Project.findAllUsersProjects user_id, 'name archived', (err, projects, collabertions=[])-> + ProjectGetter.findAllUsersProjects user_id, 'name archived', (err, projects, collabertions=[])-> + return callback(error) if error? projects = projects.concat(collabertions) projectName = projectName.toLowerCase() project = _.find projects, (project)-> diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 77fbdaf1d4..3a391adfc0 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -60,12 +60,6 @@ ProjectSchema.statics.findPopulatedById = (project_id, callback)-> logger.log project_id:project_id, "finished findPopulatedById" callback(null, projects[0]) -ProjectSchema.statics.findAllUsersProjects = (user_id, requiredFields, callback)-> - this.find {owner_ref:user_id}, requiredFields, (err, projects)=> - this.find {collaberator_refs:user_id}, requiredFields, (err, collabertions)=> - this.find {readOnly_refs:user_id}, requiredFields, (err, readOnlyProjects)=> - callback(err, projects, collabertions, readOnlyProjects) - sanitizeTypeOfElement = (elementType)-> lastChar = elementType.slice -1 if lastChar != "s" diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 3ecb31ba68..186c2e9836 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -36,7 +36,6 @@ describe "ProjectController", -> @NotificationsHandler = getUserNotifications: sinon.stub() @ProjectModel = - findAllUsersProjects: sinon.stub() findPopulatedById: sinon.stub() @UserModel = findById: sinon.stub() @@ -50,6 +49,8 @@ describe "ProjectController", -> markAsOpened: sinon.stub() @ReferencesSearchHandler = indexProjectReferences: sinon.stub() + @ProjectGetter = + findAllUsersProjects: sinon.stub() @ProjectController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings "logger-sharelatex": @@ -69,6 +70,7 @@ describe "ProjectController", -> "../InactiveData/InactiveProjectManager":@InactiveProjectManager "./ProjectUpdateHandler":@ProjectUpdateHandler "../ReferencesSearch/ReferencesSearchHandler": @ReferencesSearchHandler + "./ProjectGetter": @ProjectGetter @user = _id:"!£123213kjljkl" @@ -220,7 +222,7 @@ describe "ProjectController", -> @LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, false) @TagsHandler.getAllTags.callsArgWith(1, null, @tags, {}) @NotificationsHandler.getUserNotifications = sinon.stub().callsArgWith(1, null, @notifications, {}) - @ProjectModel.findAllUsersProjects.callsArgWith(2, null, @projects, @collabertions, @readOnly) + @ProjectGetter.findAllUsersProjects.callsArgWith(2, null, @projects, @collabertions, @readOnly) it "should render the project/list page", (done)-> @res.render = (pageName, opts)=> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee index b8f1480be6..a107031c0b 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee @@ -16,6 +16,8 @@ describe "ProjectGetter", -> projects: {} users: {} ObjectId: ObjectId + "../../models/Project": Project: @Project = {} + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} describe "getProjectWithoutDocLines", -> beforeEach -> @@ -111,4 +113,17 @@ describe "ProjectGetter", -> it "should call the callback", -> assert.deepEqual @callback.args[0][1], @project - + + describe "findAllUsersProjects", -> + beforeEach -> + @fields = {"mock": "fields"} + @Project.find = sinon.stub() + @Project.find.withArgs({owner_ref: @user_id}, @fields).yields(null, ["mock-owned-projects"]) + @CollaboratorsHandler.getProjectsUserIsMemberOf = sinon.stub() + @CollaboratorsHandler.getProjectsUserIsMemberOf.withArgs(@user_id, @fields).yields(null, ["mock-rw-projects"], ["mock-ro-projects"]) + @ProjectGetter.findAllUsersProjects @user_id, @fields, @callback + + it "should call the callback with all the projects", -> + @callback + .calledWith(null, ["mock-owned-projects"], ["mock-rw-projects"], ["mock-ro-projects"]) + .should.equal true diff --git a/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee index 5c36c18493..2bbcefd340 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee @@ -30,7 +30,7 @@ project.rootFolder[0] = rootFolder project.rootDoc_id = rootDoc._id -describe 'project model', -> +describe 'ProjectLocator', -> beforeEach -> Project.getProject = (project_id, fields, callback)=> @@ -41,6 +41,7 @@ describe 'project model', -> @locator = SandboxedModule.require modulePath, requires: '../../models/Project':{Project:Project} '../../models/User':{User:@User} + './ProjectGetter': @ProjectGetter = {} 'logger-sharelatex': log:-> err:-> @@ -298,7 +299,7 @@ describe 'project model', -> user_id = "123jojoidns" stubbedProject = {name:"findThis"} projects = [{name:"notThis"}, {name:"wellll"}, stubbedProject, {name:"Noooo"}] - Project.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects) + @ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects) @locator.findUsersProjectByName user_id, stubbedProject.name.toLowerCase(), (err, project)-> project.should.equal stubbedProject done() @@ -307,7 +308,7 @@ describe 'project model', -> user_id = "123jojoidns" stubbedProject = {name:"findThis", _id:12331321} projects = [{name:"notThis"}, {name:"wellll"}, {name:"findThis",archived:true}, stubbedProject, {name:"findThis",archived:true}, {name:"Noooo"}] - Project.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects) + @ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects) @locator.findUsersProjectByName user_id, stubbedProject.name.toLowerCase(), (err, project)-> project._id.should.equal stubbedProject._id done() @@ -316,7 +317,7 @@ describe 'project model', -> user_id = "123jojoidns" stubbedProject = {name:"findThis"} projects = [{name:"notThis"}, {name:"wellll"}, {name:"Noooo"}] - Project.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects, [stubbedProject]) + @ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, projects, [stubbedProject]) @locator.findUsersProjectByName user_id, stubbedProject.name.toLowerCase(), (err, project)-> project.should.equal stubbedProject done() From 6d93076d515bd83f8b13b80b5838bf5bbdfca657 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 7 Mar 2016 12:02:48 +0000 Subject: [PATCH 003/208] Refactor getCollaborators http method to use CollaboratorsHandler --- .../CollaboratorsController.coffee | 27 ++-- .../Collaborators/CollaboratorsHandler.coffee | 8 +- .../Features/Project/ProjectGetter.coffee | 2 +- .../CollaboratorsControllerTests.coffee | 145 +++++------------- .../CollaboratorsHandlerTests.coffee | 4 +- 5 files changed, 63 insertions(+), 123 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee index 788b782eb2..82912e1b6b 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -8,13 +8,12 @@ mimelib = require("mimelib") module.exports = CollaboratorsController = getCollaborators: (req, res, next = (error) ->) -> - ProjectGetter.getProject req.params.Project_id, { owner_ref: true, collaberator_refs: true, readOnly_refs: true}, (error, project) -> + project_id = req.params.Project_id + CollaboratorsHandler.getMembersWithPrivilegeLevels project_id, (error, members) -> return next(error) if error? - ProjectGetter.populateProjectWithUsers project, (error, project) -> + CollaboratorsController._formatCollaborators members, (error, collaborators) -> return next(error) if error? - CollaboratorsController._formatCollaborators project, (error, collaborators) -> - return next(error) if error? - res.send(JSON.stringify(collaborators)) + res.json(collaborators) addUserToProject: (req, res, next) -> project_id = req.params.Project_id @@ -59,7 +58,7 @@ module.exports = CollaboratorsController = EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id) callback() - _formatCollaborators: (project, callback = (error, collaborators) ->) -> + _formatCollaborators: (members, callback = (error, collaborators) ->) -> collaborators = [] pushCollaborator = (user, permissions, owner) -> @@ -71,16 +70,14 @@ module.exports = CollaboratorsController = permissions: permissions owner: owner } - - if project.owner_ref? - pushCollaborator(project.owner_ref, ["read", "write", "admin"], true) - - if project.collaberator_refs? and project.collaberator_refs.length > 0 - for user in project.collaberator_refs + + for member in members + {user, privilegeLevel} = member + if privilegeLevel == "admin" + pushCollaborator(user, ["read", "write", "admin"], true) + else if privilegeLevel == "readAndWrite" pushCollaborator(user, ["read", "write"], false) - - if project.readOnly_refs? and project.readOnly_refs.length > 0 - for user in project.readOnly_refs + else pushCollaborator(user, ["read"], false) callback null, collaborators diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index d574a0b263..d1c355b79a 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -10,10 +10,11 @@ async = require "async" module.exports = CollaboratorsHandler = getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> - Project.findOne { _id: project_id }, { collaberator_refs: 1, readOnly_refs: 1 }, (error, project) -> + Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) -> return callback(error) if error? return callback null, null if !project? members = [] + members.push { id: project.owner_ref.toString(), privilegeLevel: "admin" } for member_id in project.readOnly_refs or [] members.push { id: member_id, privilegeLevel: "readOnly" } for member_id in project.collaberator_refs or [] @@ -34,6 +35,11 @@ module.exports = CollaboratorsHandler = CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> return callback(error) if error? return callback null, (members or []).length + + getCollaboratorCount: (project_id, callback = (error, count) ->) -> + CollaboratorsHandler.getMemberCount project_id, (error, count) -> + return callback(error) if error? + return callback null, count - 1 # Don't count project owner isUserMemberOfProject: (user_id, project_id, callback = (error, isMember, privilegeLevel) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> diff --git a/services/web/app/coffee/Features/Project/ProjectGetter.coffee b/services/web/app/coffee/Features/Project/ProjectGetter.coffee index 826ea4116c..13fa409e99 100644 --- a/services/web/app/coffee/Features/Project/ProjectGetter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectGetter.coffee @@ -3,7 +3,6 @@ db = mongojs.db ObjectId = mongojs.ObjectId async = require "async" Project = require("../../models/Project").Project -CollaboratorsHandler = require "../Collaborators/CollaboratorsHandler" module.exports = ProjectGetter = EXCLUDE_DEPTH: 8 @@ -31,6 +30,7 @@ module.exports = ProjectGetter = db.projects.findOne query, projection, callback findAllUsersProjects: (user_id, fields, callback = (error, ownedProjects, readAndWriteProjects, readOnlyProjects) ->) -> + CollaboratorsHandler = require "../Collaborators/CollaboratorsHandler" Project.find {owner_ref: user_id}, fields, (error, projects) -> return callback(error) if error? CollaboratorsHandler.getProjectsUserIsMemberOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee index 6930707249..84b0a250d3 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee @@ -26,32 +26,49 @@ describe "CollaboratorsController", -> describe "getCollaborators", -> beforeEach -> - @project = - _id: @project_id = "project-id-123" - @collaborators = ["array of collaborators"] - @req.params = Project_id: @project_id - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) - @ProjectGetter.populateProjectWithUsers = sinon.stub().callsArgWith(1, null, @project) - @CollaboratorsController._formatCollaborators = sinon.stub().callsArgWith(1, null, @collaborators) + @req.params = + Project_id: @project_id + @members = [ + { + user: { _id: "admin-id", email: "admin@example.com", first_name: "Joe", last_name: "Admin", foo: "bar" } + privilegeLevel: "admin" + }, + { + user: { _id: "rw-id", email: "rw@example.com", first_name: "Jane", last_name: "Write", foo: "bar" } + privilegeLevel: "readAndWrite" + }, + { + user: { _id: "ro-id", email: "ro@example.com", first_name: "Joe", last_name: "Read", foo: "bar" } + privilegeLevel: "readOnly" + } + ] + @CollaboratorsHandler.getMembersWithPrivilegeLevels = sinon.stub() + @CollaboratorsHandler.getMembersWithPrivilegeLevels + .withArgs(@project_id) + .yields(null, @members) + @res.json = sinon.stub() @CollaboratorsController.getCollaborators(@req, @res) - it "should get the project", -> - @ProjectGetter.getProject - .calledWith(@project_id, { owner_ref: true, collaberator_refs: true, readOnly_refs: true }) - .should.equal true - - it "should populate the users in the project", -> - @ProjectGetter.populateProjectWithUsers - .calledWith(@project) - .should.equal true - - it "should format the collaborators", -> - @CollaboratorsController._formatCollaborators - .calledWith(@project) - .should.equal true - it "should return the formatted collaborators", -> - @res.body.should.equal JSON.stringify(@collaborators) + @res.json + .calledWith([ + { + id: "admin-id", email: "admin@example.com", first_name: "Joe", last_name: "Admin" + permissions: ["read", "write", "admin"] + owner: true + } + { + id: "rw-id", email: "rw@example.com", first_name: "Jane", last_name: "Write" + permissions: ["read", "write"] + owner: false + } + { + id: "ro-id", email: "ro@example.com", first_name: "Joe", last_name: "Read" + permissions: ["read"] + owner: false + } + ]) + .should.equal true describe "addUserToProject", -> beforeEach -> @@ -179,85 +196,3 @@ describe "CollaboratorsController", -> it "should return a success code", -> @res.sendStatus.calledWith(204).should.equal true - describe "_formatCollaborators", -> - beforeEach -> - @owner = - _id: ObjectId() - first_name: "Lenny" - last_name: "Lion" - email: "test@sharelatex.com" - hashed_password: "password" # should not be included - - describe "formatting the owner", -> - beforeEach -> - @project = - owner_ref: @owner - collaberator_refs: [] - @CollaboratorsController._formatCollaborators(@project, @callback) - - it "should return the owner with read, write and admin permissions", -> - @formattedOwner = @callback.args[0][1][0] - expect(@formattedOwner).to.deep.equal { - id: @owner._id.toString() - first_name: @owner.first_name - last_name: @owner.last_name - email: @owner.email - permissions: ["read", "write", "admin"] - owner: true - } - - describe "formatting a collaborator with write access", -> - beforeEach -> - @collaborator = - _id: ObjectId() - first_name: "Douglas" - last_name: "Adams" - email: "doug@sharelatex.com" - hashed_password: "password" # should not be included - - @project = - owner_ref: @owner - collaberator_refs: [ @collaborator ] - @CollaboratorsController._formatCollaborators(@project, @callback) - - it "should return the collaborator with read and write permissions", -> - @formattedCollaborator = @callback.args[0][1][1] - expect(@formattedCollaborator).to.deep.equal { - id: @collaborator._id.toString() - first_name: @collaborator.first_name - last_name: @collaborator.last_name - email: @collaborator.email - permissions: ["read", "write"] - owner: false - } - - describe "formatting a collaborator with read only access", -> - beforeEach -> - @collaborator = - _id: ObjectId() - first_name: "Douglas" - last_name: "Adams" - email: "doug@sharelatex.com" - hashed_password: "password" # should not be included - - @project = - owner_ref: @owner - collaberator_refs: [] - readOnly_refs: [ @collaborator ] - @CollaboratorsController._formatCollaborators(@project, @callback) - - it "should return the collaborator with read permissions", -> - @formattedCollaborator = @callback.args[0][1][1] - expect(@formattedCollaborator).to.deep.equal { - id: @collaborator._id.toString() - first_name: @collaborator.first_name - last_name: @collaborator.last_name - email: @collaborator.email - permissions: ["read"] - owner: false - } - - - - - diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 631559b84e..f97364b756 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -26,7 +26,8 @@ describe "CollaboratorsHandler", -> describe "getMemberIdsWithPrivilegeLevels", -> beforeEach -> @Project.findOne = sinon.stub() - @Project.findOne.withArgs({_id: @project_id}, {collaberator_refs: 1, readOnly_refs: 1}).yields(null, @project = { + @Project.findOne.withArgs({_id: @project_id}, {owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1}).yields(null, @project = { + owner_ref: [ "owner-ref" ] readOnly_refs: [ "read-only-ref-1", "read-only-ref-2" ] collaberator_refs: [ "read-write-ref-1", "read-write-ref-2" ] }) @@ -35,6 +36,7 @@ describe "CollaboratorsHandler", -> it "should return an array of member ids with their privilege levels", -> @callback .calledWith(null, [ + { id: "owner-ref", privilegeLevel: "admin" } { id: "read-only-ref-1", privilegeLevel: "readOnly" } { id: "read-only-ref-2", privilegeLevel: "readOnly" } { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } From 2ba2b72fd1232f01ab4b7ee589000bef1a597dcb Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 7 Mar 2016 12:27:40 +0000 Subject: [PATCH 004/208] Refactor ProjectDeleter to use CollaboratorHandler --- .../Collaborators/CollaboratorsHandler.coffee | 5 +++++ .../coffee/Features/Project/ProjectDeleter.coffee | 13 ++++--------- .../Collaborators/CollaboratorsHandlerTests.coffee | 13 +++++++++++++ .../coffee/Project/ProjectDeleterTests.coffee | 11 +++++------ 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index d1c355b79a..ef2f96ea3d 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -21,6 +21,11 @@ module.exports = CollaboratorsHandler = members.push { id: member_id, privilegeLevel: "readAndWrite" } return callback null, members + getMemberIds: (project_id, callback = (error, member_ids) ->) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + return callback(error) if error? + return callback null, members.map (m) -> m.id + getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> return callback(error) if error? diff --git a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee index 6fdba733e7..3c11a777d6 100644 --- a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee @@ -4,6 +4,7 @@ documentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') tagsHandler = require("../Tags/TagsHandler") async = require("async") FileStoreHandler = require("../FileStore/FileStoreHandler") +CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") module.exports = ProjectDeleter = @@ -43,16 +44,10 @@ module.exports = ProjectDeleter = (cb)-> documentUpdaterHandler.flushProjectToMongoAndDelete project_id, cb (cb)-> - tagsHandler.removeProjectFromAllTags project.owner_ref, project_id, (err)-> + CollaboratorsHandler.getMemberIds project_id, (error, member_ids = []) -> + for member_id in member_ids + tagsHandler.removeProjectFromAllTags member_id, project_id, (err)-> cb() #doesn't matter if this fails or the order it happens in - (cb)-> - project.collaberator_refs.forEach (collaberator_ref)-> - tagsHandler.removeProjectFromAllTags collaberator_ref, project_id, -> - cb() - (cb)-> - project.readOnly_refs.forEach (readOnly_ref)-> - tagsHandler.removeProjectFromAllTags readOnly_ref, project_id, -> - cb() (cb)-> Project.update {_id:project_id}, { $set: { archived: true }}, cb ], (err)-> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index f97364b756..f0fd8db4a9 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -44,6 +44,19 @@ describe "CollaboratorsHandler", -> ]) .should.equal true + describe "getMemberIds", -> + beforeEach -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels + .withArgs(@project_id) + .yields(null, [{id: "member-id-1"}, {id: "member-id-2"}]) + @CollaboratorHandler.getMemberIds @project_id, @callback + + it "should return the ids", -> + @callback + .calledWith(null, ["member-id-1", "member-id-2"]) + .should.equal true + describe "getMembersWithPrivilegeLevels", -> beforeEach -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() diff --git a/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee index 13f5d2a694..7ed1dac666 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee @@ -31,6 +31,7 @@ describe 'ProjectDeleter', -> '../DocumentUpdater/DocumentUpdaterHandler': @documentUpdaterHandler "../Tags/TagsHandler":@TagsHandler "../FileStore/FileStoreHandler": @FileStoreHandler = {} + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} 'logger-sharelatex': log:-> @@ -89,6 +90,8 @@ describe 'ProjectDeleter', -> describe "archiveProject", -> beforeEach -> + @CollaboratorsHandler.getMemberIds = sinon.stub() + @CollaboratorsHandler.getMemberIds.withArgs(@project_id).yields(null, ["member-id-1", "member-id-2"]) @Project.update.callsArgWith(2) it "should flushProjectToMongoAndDelete in doc updater", (done)-> @@ -107,12 +110,8 @@ describe 'ProjectDeleter', -> it "should removeProjectFromAllTags", (done)-> @deleter.archiveProject @project_id, => - @TagsHandler.removeProjectFromAllTags.calledWith(@project.owner_ref, @project_id).should.equal true - @TagsHandler.removeProjectFromAllTags.calledWith(@project.collaberator_refs[0], @project_id).should.equal true - @TagsHandler.removeProjectFromAllTags.calledWith(@project.collaberator_refs[1], @project_id).should.equal true - @TagsHandler.removeProjectFromAllTags.calledWith(@project.readOnly_refs[0], @project_id).should.equal true - @TagsHandler.removeProjectFromAllTags.calledWith(@project.readOnly_refs[1], @project_id).should.equal true - + @TagsHandler.removeProjectFromAllTags.calledWith("member-id-1", @project_id).should.equal true + @TagsHandler.removeProjectFromAllTags.calledWith("member-id-2", @project_id).should.equal true done() describe "restoreProject", -> From bedc8a049210b9b51732f57665fa06f4670731ca Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 7 Mar 2016 15:25:10 +0000 Subject: [PATCH 005/208] Remove ProjectGetter.populateProjectWithUsers --- .../Editor/EditorHttpController.coffee | 5 +- .../Project/ProjectEditorHandler.coffee | 69 +++++++++--------- .../Features/Project/ProjectGetter.coffee | 40 ---------- .../Editor/EditorHttpControllerTests.coffee | 8 +- .../Project/ProjectEditorHandlerTests.coffee | 73 +++++++++---------- .../coffee/Project/ProjectGetterTests.coffee | 35 --------- 6 files changed, 76 insertions(+), 154 deletions(-) diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 2619316e6d..18e8f7d308 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -8,6 +8,7 @@ UserGetter = require('../User/UserGetter') AuthorizationManager = require("../Security/AuthorizationManager") ProjectEditorHandler = require('../Project/ProjectEditorHandler') Metrics = require('../../infrastructure/Metrics') +CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") module.exports = EditorHttpController = joinProject: (req, res, next) -> @@ -29,7 +30,7 @@ module.exports = EditorHttpController = ProjectGetter.getProjectWithoutDocLines project_id, (error, project) -> return callback(error) if error? return callback(new Error("not found")) if !project? - ProjectGetter.populateProjectWithUsers project, (error, project) -> + CollaboratorsHandler.getMembersWithPrivilegeLevels project, (error, members) -> return callback(error) if error? UserGetter.getUser user_id, { isAdmin: true }, (error, user) -> return callback(error) if error? @@ -39,7 +40,7 @@ module.exports = EditorHttpController = callback null, null, false else callback(null, - ProjectEditorHandler.buildProjectModelView(project), + ProjectEditorHandler.buildProjectModelView(project, members), privilegeLevel ) diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index f12a7d548e..5b3143d765 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -1,11 +1,7 @@ _ = require("underscore") module.exports = ProjectEditorHandler = - buildProjectModelView: (project, options) -> - options ||= {} - if !options.includeUsers? - options.includeUsers = true - + buildProjectModelView: (project, members) -> result = _id : project._id name : project.name @@ -18,40 +14,41 @@ module.exports = ProjectEditorHandler = spellCheckLanguage: project.spellCheckLanguage deletedByExternalDataSource : project.deletedByExternalDataSource || false deletedDocs: project.deletedDocs + members: [] - if options.includeUsers - result.features = - collaborators: -1 # Infinite - versioning: false - dropbox:false - compileTimeout: 60 - compileGroup:"standard" - templates: false - references: false + result.features = # defaults + collaborators: -1 # Infinite + versioning: false + dropbox:false + compileTimeout: 60 + compileGroup:"standard" + templates: false + references: false + + owner = null + for member in members + if member.privilegeLevel == "admin" + owner = member.user + else + result.members.push @buildUserModelView member.user, member.privilegeLevel + result.owner = @buildUserModelView owner, "owner" - if project.owner_ref.features? - if project.owner_ref.features.collaborators? - result.features.collaborators = project.owner_ref.features.collaborators - if project.owner_ref.features.versioning? - result.features.versioning = project.owner_ref.features.versioning - if project.owner_ref.features.dropbox? - result.features.dropbox = project.owner_ref.features.dropbox - if project.owner_ref.features.compileTimeout? - result.features.compileTimeout = project.owner_ref.features.compileTimeout - if project.owner_ref.features.compileGroup? - result.features.compileGroup = project.owner_ref.features.compileGroup - if project.owner_ref.features.templates? - result.features.templates = project.owner_ref.features.templates - if project.owner_ref.features.references? - result.features.references = project.owner_ref.features.references + if owner?.features? + if owner.features.collaborators? + result.features.collaborators = owner.features.collaborators + if owner.features.versioning? + result.features.versioning = owner.features.versioning + if owner.features.dropbox? + result.features.dropbox = owner.features.dropbox + if owner.features.compileTimeout? + result.features.compileTimeout = owner.features.compileTimeout + if owner.features.compileGroup? + result.features.compileGroup = owner.features.compileGroup + if owner.features.templates? + result.features.templates = owner.features.templates + if owner.features.references? + result.features.references = owner.features.references - - result.owner = @buildUserModelView project.owner_ref, "owner" - result.members = [] - for ref in project.readOnly_refs - result.members.push @buildUserModelView ref, "readOnly" - for ref in project.collaberator_refs - result.members.push @buildUserModelView ref, "readAndWrite" return result buildUserModelView: (user, privileges) -> diff --git a/services/web/app/coffee/Features/Project/ProjectGetter.coffee b/services/web/app/coffee/Features/Project/ProjectGetter.coffee index 13fa409e99..7d9fd13bb6 100644 --- a/services/web/app/coffee/Features/Project/ProjectGetter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectGetter.coffee @@ -36,43 +36,3 @@ module.exports = ProjectGetter = CollaboratorsHandler.getProjectsUserIsMemberOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) -> return callback(error) if error? callback null, projects, readAndWriteProjects, readOnlyProjects - - populateProjectWithUsers: (project, callback=(error, project) ->) -> - # eventually this should be in a UserGetter.getUser module - getUser = (user_id, callback=(error, user) ->) -> - unless user_id instanceof ObjectId - user_id = ObjectId(user_id) - db.users.find _id: user_id, (error, users = []) -> - callback error, users[0] - - jobs = [] - jobs.push (callback) -> - getUser project.owner_ref, (error, user) -> - return callback(error) if error? - if user? - project.owner_ref = user - callback null, project - - readOnly_refs = project.readOnly_refs - project.readOnly_refs = [] - for readOnly_ref in readOnly_refs - do (readOnly_ref) -> - jobs.push (callback) -> - getUser readOnly_ref, (error, user) -> - return callback(error) if error? - if user? - project.readOnly_refs.push user - callback null, project - - collaberator_refs = project.collaberator_refs - project.collaberator_refs = [] - for collaberator_ref in collaberator_refs - do (collaberator_ref) -> - jobs.push (callback) -> - getUser collaberator_ref, (error, user) -> - return callback(error) if error? - if user? - project.collaberator_refs.push user - callback null, project - - async.parallelLimit jobs, 3, (error) -> callback error, project diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee index ab003208c2..21c8a195e4 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee @@ -16,6 +16,7 @@ describe "EditorHttpController", -> "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } "./EditorController": @EditorController = {} '../../infrastructure/Metrics': @Metrics = {inc: sinon.stub()} + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} @project_id = "mock-project-id" @doc_id = "mock-doc-id" @@ -85,13 +86,14 @@ describe "EditorHttpController", -> @user = _id: @user_id = "user-id" projects: {} + @members = ["members", "mock"] @projectModelView = _id: @project_id owner:{_id:"something"} view: true @ProjectEditorHandler.buildProjectModelView = sinon.stub().returns(@projectModelView) @ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project) - @ProjectGetter.populateProjectWithUsers = sinon.stub().callsArgWith(1, null, @project) + @CollaboratorsHandler.getMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, null, @members) @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user) describe "when authorized", -> @@ -105,8 +107,8 @@ describe "EditorHttpController", -> .calledWith(@project_id) .should.equal true - it "should populate the user references in the project", -> - @ProjectGetter.populateProjectWithUsers + it "should get the list of users in the project", -> + @CollaboratorsHandler.getMembersWithPrivilegeLevels .calledWith(@project) .should.equal true diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee index 52c871669a..7d71f6c89f 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee @@ -38,33 +38,41 @@ describe "ProjectEditorHandler", -> folders : [] }] }] - owner_ref : - _id: "owner-id" - first_name : "Owner" - last_name : "ShareLaTeX" - email : "owner@sharelatex.com" - readOnly_refs: [{ - _id: "read-only-id" - first_name : "Read" - last_name : "Only" - email : "read-only@sharelatex.com" - }] - collaberator_refs: [{ - _id: "read-write-id" - first_name : "Read" - last_name : "Write" - email : "read-write@sharelatex.com" - }] deletedDocs: [{ _id: "deleted-doc-id" name: "main.tex" }] + @members = [{ + user: @owner = { + _id: "owner-id" + first_name : "Owner" + last_name : "ShareLaTeX" + email : "owner@sharelatex.com" + }, + privilegeLevel: "admin" + },{ + user: { + _id: "read-only-id" + first_name : "Read" + last_name : "Only" + email : "read-only@sharelatex.com" + }, + privilegeLevel: "readOnly" + },{ + user: { + _id: "read-write-id" + first_name : "Read" + last_name : "Write" + email : "read-write@sharelatex.com" + }, + privilegeLevel: "readAndWrite" + }] @handler = SandboxedModule.require modulePath describe "buildProjectModelView", -> describe "with owner and members included", -> beforeEach -> - @result = @handler.buildProjectModelView @project + @result = @handler.buildProjectModelView @project, @members it "should include the id", -> should.exist @result._id @@ -140,41 +148,30 @@ describe "ProjectEditorHandler", -> it "should set the deletedByExternalDataSource flag to false when it is not there", -> delete @project.deletedByExternalDataSource - result = @handler.buildProjectModelView @project + result = @handler.buildProjectModelView @project, @members result.deletedByExternalDataSource.should.equal false it "should set the deletedByExternalDataSource flag to false when it is false", -> - result = @handler.buildProjectModelView @project + result = @handler.buildProjectModelView @project, @members result.deletedByExternalDataSource.should.equal false it "should set the deletedByExternalDataSource flag to true when it is true", -> @project.deletedByExternalDataSource = true - result = @handler.buildProjectModelView @project + result = @handler.buildProjectModelView @project, @members result.deletedByExternalDataSource.should.equal true describe "features", -> beforeEach -> - @project.owner_ref.features = + @owner.features = versioning: true collaborators: 3 compileGroup:"priority" compileTimeout: 96 - @result = @handler.buildProjectModelView @project + @result = @handler.buildProjectModelView @project, @members it "should copy the owner features to the project", -> - @result.features.versioning.should.equal @project.owner_ref.features.versioning - @result.features.collaborators.should.equal @project.owner_ref.features.collaborators - @result.features.compileGroup.should.equal @project.owner_ref.features.compileGroup - @result.features.compileTimeout.should.equal @project.owner_ref.features.compileTimeout + @result.features.versioning.should.equal @owner.features.versioning + @result.features.collaborators.should.equal @owner.features.collaborators + @result.features.compileGroup.should.equal @owner.features.compileGroup + @result.features.compileTimeout.should.equal @owner.features.compileTimeout - - describe "without owners and members", -> - beforeEach -> - @result = @handler.buildProjectModelView @project, includeUsers: false - - it "should not include the owner", -> - should.not.exist @result.owner - - it "should not include the members", -> - should.not.exist @result.members - diff --git a/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee index a107031c0b..da9b9765e3 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee @@ -79,41 +79,6 @@ describe "ProjectGetter", -> it "should call the callback with the project", -> @callback.calledWith(null, @project).should.equal true - - describe "populateProjectWithUsers", -> - beforeEach -> - @users = [] - @user_lookup = {} - for i in [0..4] - @users[i] = _id: ObjectId.createPk() - @user_lookup[@users[i]._id.toString()] = @users[i] - @project = - _id: ObjectId.createPk() - owner_ref: @users[0]._id - readOnly_refs: [@users[1]._id, @users[2]._id] - collaberator_refs: [@users[3]._id, @users[4]._id] - @db.users.find = (query, callback) => - callback null, [@user_lookup[query._id.toString()]] - sinon.spy @db.users, "find" - @ProjectGetter.populateProjectWithUsers @project, (err, project)=> - @callback err, project - - it "should look up each user", -> - for user in @users - @db.users.find.calledWith(_id: user._id).should.equal true - - it "should set the owner_ref to the owner", -> - @project.owner_ref.should.equal @users[0] - - it "should set the readOnly_refs to the read only users", -> - expect(@project.readOnly_refs).to.deep.equal [@users[1], @users[2]] - - it "should set the collaberator_refs to the collaborators", -> - expect(@project.collaberator_refs).to.deep.equal [@users[3], @users[4]] - - it "should call the callback", -> - assert.deepEqual @callback.args[0][1], @project - describe "findAllUsersProjects", -> beforeEach -> @fields = {"mock": "fields"} From a50bdaf5cc8985535b81145c5f97474d6f5a689e Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 7 Mar 2016 15:32:04 +0000 Subject: [PATCH 006/208] Refactor LimitationsManager to use CollaboratorsHandler --- .../Subscription/LimitationsManager.coffee | 8 ++------ .../Subscription/LimitationsManagerTests.coffee | 15 ++------------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee index d323c19a9d..e2f633e185 100644 --- a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee +++ b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee @@ -3,6 +3,7 @@ Project = require("../../models/Project").Project User = require("../../models/User").User SubscriptionLocator = require("./SubscriptionLocator") Settings = require("settings-sharelatex") +CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") module.exports = @@ -13,16 +14,11 @@ module.exports = callback null, owner.features.collaborators else callback null, Settings.defaultPlanCode.collaborators - - currentNumberOfCollaboratorsInProject: (project_id, callback) -> - Project.findById project_id, 'collaberator_refs readOnly_refs', (error, project) -> - return callback(error) if error? - callback null, (project.collaberator_refs.length + project.readOnly_refs.length) canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) -> @allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) => return callback(error) if error? - @currentNumberOfCollaboratorsInProject project_id, (error, current_number) => + CollaboratorsHandler.getCollaboratorCount project_id, (error, current_number) => return callback(error) if error? if current_number + x_collaborators <= allowed_number or allowed_number < 0 callback null, true diff --git a/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee index df1edca280..581d5cd6da 100644 --- a/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee @@ -29,6 +29,7 @@ describe "LimitationsManager", -> '../../models/User' : User: @User './SubscriptionLocator':@SubscriptionLocator 'settings-sharelatex' : @Settings = {} + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} 'logger-sharelatex':log:-> describe "allowedNumberOfCollaboratorsInProject", -> @@ -51,22 +52,10 @@ describe "LimitationsManager", -> it "should return the number of collaborators the user is allowed", -> @callback.calledWith(null, @user.features.collaborators).should.equal true - - describe "currentNumberOfCollaboratorsInProject", -> - beforeEach -> - @project.collaberator_refs = ["one", "two"] - @project.readOnly_refs = ["three"] - @callback = sinon.stub() - @LimitationsManager.currentNumberOfCollaboratorsInProject(@project_id, @callback) - - it "should return the total number of collaborators", -> - @callback.calledWith(null, 3).should.equal true describe "canAddXCollaborators", -> beforeEach -> - sinon.stub @LimitationsManager, - "currentNumberOfCollaboratorsInProject", - (project_id, callback) => callback(null, @current_number) + @CollaboratorsHandler.getCollaboratorCount = (project_id, callback) => callback(null, @current_number) sinon.stub @LimitationsManager, "allowedNumberOfCollaboratorsInProject", (project_id, callback) => callback(null, @allowed_number) From 5f5445f625f3c2825f4200e3c3f648139d9dc6d2 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 11:54:45 +0000 Subject: [PATCH 007/208] Use TpdsUpdateSender to use CollaboratorsHandler --- .../Features/Collaborators/CollaboratorsHandler.coffee | 2 +- .../web/app/coffee/Features/Project/ProjectGetter.coffee | 2 +- .../Features/ThirdPartyDataStore/TpdsUpdateSender.coffee | 9 ++++++--- .../Collaborators/CollaboratorsHandlerTests.coffee | 4 ++-- .../ThirdPartyDataStore/TpdsUpdateSenderTests.coffee | 6 +++++- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index ef2f96ea3d..72372345f8 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -54,7 +54,7 @@ module.exports = CollaboratorsHandler = return callback null, true, member.privilegeLevel return callback null, false, null - getProjectsUserIsMemberOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) -> + getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) -> Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=> Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=> callback(err, readAndWriteProjects, readOnlyProjects) diff --git a/services/web/app/coffee/Features/Project/ProjectGetter.coffee b/services/web/app/coffee/Features/Project/ProjectGetter.coffee index 7d9fd13bb6..28f687116f 100644 --- a/services/web/app/coffee/Features/Project/ProjectGetter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectGetter.coffee @@ -33,6 +33,6 @@ module.exports = ProjectGetter = CollaboratorsHandler = require "../Collaborators/CollaboratorsHandler" Project.find {owner_ref: user_id}, fields, (error, projects) -> return callback(error) if error? - CollaboratorsHandler.getProjectsUserIsMemberOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) -> + CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) -> return callback(error) if error? callback null, projects, readAndWriteProjects, readOnlyProjects diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee index f2d5bb94d1..88397c9ef6 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee @@ -5,6 +5,7 @@ Project = require('../../models/Project').Project keys = require('../../infrastructure/Keys') metrics = require("../../infrastructure/Metrics") request = require("request") +CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') buildPath = (user_id, project_name, filePath)-> projectPath = path.join(project_name, "/", filePath) @@ -116,9 +117,11 @@ module.exports = TpdsUpdateSender = TpdsUpdateSender._enqueue "poll-dropbox:#{user_id}", "standardHttpRequest", options, callback getProjectsUsersIds = (project_id, callback = (err, owner_id, allUserIds)->)-> - Project.findById project_id, "_id owner_ref readOnly_refs collaberator_refs", (err, project)-> - allUserIds = [].concat(project.collaberator_refs).concat(project.readOnly_refs).concat(project.owner_ref) - callback err, project.owner_ref, allUserIds + Project.findById project_id, "_id owner_ref", (err, project) -> + return callback(err) if err? + CollaboratorsHandler.getMemberIds project_id, (err, member_ids) -> + return callback(err) if err? + callback err, project.owner_ref, member_ids mergeProjectNameAndPath = (project_name, path)-> if(path.indexOf('/') == 0) diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index f0fd8db4a9..391b533cdf 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -112,13 +112,13 @@ describe "CollaboratorsHandler", -> .calledWith(null, false, null) .should.equal true - describe "getProjectsUserIsMemberOf", -> + describe "getProjectsUserIsCollaboratorOf", -> beforeEach -> @fields = "mock fields" @Project.find = sinon.stub() @Project.find.withArgs({collaberator_refs:@user_id}, @fields).yields(null, ["mock-read-write-project-1", "mock-read-write-project-2"]) @Project.find.withArgs({readOnly_refs:@user_id}, @fields).yields(null, ["mock-read-only-project-1", "mock-read-only-project-2"]) - @CollaboratorHandler.getProjectsUserIsMemberOf @user_id, @fields, @callback + @CollaboratorHandler.getProjectsUserIsCollaboratorOf @user_id, @fields, @callback it "should call the callback with the projects", -> @callback diff --git a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee index cd84080d17..bc2290eb04 100644 --- a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee +++ b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee @@ -20,7 +20,10 @@ filestoreUrl = "filestore.sharelatex.com" describe 'TpdsUpdateSender', -> beforeEach -> @requestQueuer = (queue, meth, opts, callback)-> - project = {owner_ref:user_id,readOnly_refs:[read_only_ref_1], collaberator_refs:[collaberator_ref_1]} + project = {owner_ref:user_id} + member_ids = [collaberator_ref_1, read_only_ref_1, user_id] + @CollaboratorsHandler = + getMemberIds: sinon.stub().yields(null, member_ids) @Project = findById:sinon.stub().callsArgWith(2, null, project) @docstoreUrl = "docstore.sharelatex.env" @request = sinon.stub().returns(pipe:->) @@ -38,6 +41,7 @@ describe 'TpdsUpdateSender', -> "logger-sharelatex":{log:->} '../../models/Project': Project:@Project 'request':@request + '../Collaborators/CollaboratorsHandler': @CollaboratorsHandler describe "_enqueue", -> From 40048d49a2f3b8627ad6f6c58aedb43582266383 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 12:07:42 +0000 Subject: [PATCH 008/208] Fix unit test --- .../test/UnitTests/coffee/Project/ProjectGetterTests.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee index da9b9765e3..a0b2f19172 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee @@ -84,8 +84,8 @@ describe "ProjectGetter", -> @fields = {"mock": "fields"} @Project.find = sinon.stub() @Project.find.withArgs({owner_ref: @user_id}, @fields).yields(null, ["mock-owned-projects"]) - @CollaboratorsHandler.getProjectsUserIsMemberOf = sinon.stub() - @CollaboratorsHandler.getProjectsUserIsMemberOf.withArgs(@user_id, @fields).yields(null, ["mock-rw-projects"], ["mock-ro-projects"]) + @CollaboratorsHandler.getProjectsUserIsCollaboratorOf = sinon.stub() + @CollaboratorsHandler.getProjectsUserIsCollaboratorOf.withArgs(@user_id, @fields).yields(null, ["mock-rw-projects"], ["mock-ro-projects"]) @ProjectGetter.findAllUsersProjects @user_id, @fields, @callback it "should call the callback with all the projects", -> From b64c8e3d78fdd5366c40d42580597f6643f2ebdc Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 12:07:50 +0000 Subject: [PATCH 009/208] Delete dead code in User model --- services/web/app/coffee/models/User.coffee | 26 ---------------------- 1 file changed, 26 deletions(-) diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index 8fca181d0e..0cd7196b64 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -55,32 +55,6 @@ UserSchema = new Schema # has this set to true, despite never having had a free trial hadFreeTrial: {type: Boolean, default: false} -UserSchema.statics.getAllIds = (callback)-> - this.find {}, ["first_name"], callback - - -UserSchema.statics.findReadOnlyProjects = (user_id, callback)-> - @find({'projects.readOnly_refs':user_id}).populate('projects.readOnly_refs').run (err, users)-> - projects = [] - _.each users, (user)-> - _.each user.projects, (project)-> - _.each project.readOnly_refs, (subUser)-> - if(subUser._id == user_id) - projects.push(project) - callback(projects) - -UserSchema.statics.findCollaborationProjects = (user_id, callback)-> - @find({'projects.collaberator_refs':user_id}).populate('projects.collaberator_refs').run (err, users)-> - projects = [] - _.each users, (user)-> - _.each user.projects, (project)-> - _.each project.collaberator_refs, (subUser)-> - if(subUser._id == user_id) - projects.push(project) - callback(projects) - - - conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: 10) User = conn.model('User', UserSchema) From 0882eb2a996e385c1ceea6d89054302ba3290452 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 14:05:56 +0000 Subject: [PATCH 010/208] Don't use deprecated Project.findPopulatedById in ReferencesManager --- .../Features/Project/ProjectController.coffee | 1 - .../References/ReferencesHandler.coffee | 10 +++-- .../References/ReferencesHandlerTests.coffee | 44 +++++++++++-------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index b30c2b8f91..d89a014e2b 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -229,7 +229,6 @@ module.exports = ProjectController = title: project.name priority_title: true bodyClasses: ["editor"] - project : project project_id : project._id user : { id : user.id diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index 0b2ddb1e26..1113a2f8a6 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -2,6 +2,7 @@ logger = require("logger-sharelatex") request = require("request") settings = require("settings-sharelatex") Project = require("../../models/Project").Project +User = require("../../models/User").User DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') U = require('underscore') Async = require('async') @@ -31,11 +32,12 @@ module.exports = ReferencesHandler = return ids _isFullIndex: (project, callback = (err, result) ->) -> - owner = project.owner_ref - callback(null, owner.features.references == true) + User.find { _id: project.owner_ref }, { features: true }, (err, owner) -> + return callback(err) if err? + callback(null, owner?.features?.references == true) indexAll: (projectId, callback=(err, data)->) -> - Project.findPopulatedById projectId, (err, project) -> + Project.find { _id: projectId }, (err, project) -> if err logger.err {err, projectId}, "error finding project" return callback(err) @@ -44,7 +46,7 @@ module.exports = ReferencesHandler = ReferencesHandler._doIndexOperation(projectId, project, docIds, callback) index: (projectId, docIds, callback=(err, data)->) -> - Project.findPopulatedById projectId, (err, project) -> + Project.find { _id: projectId }, (err, project) -> if err logger.err {err, projectId}, "error finding project" return callback(err) diff --git a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee index 86cf9111dc..3eacae73cd 100644 --- a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee @@ -41,9 +41,12 @@ describe 'ReferencesHandler', -> } '../../models/Project': { Project: @Project = { - findPopulatedById: sinon.stub().callsArgWith(1, null, @fakeProject) + find: sinon.stub().callsArgWith(1, null, @fakeProject) } } + '../../models/User': { + User: @User = {} + } '../DocumentUpdater/DocumentUpdaterHandler': @DocumentUpdaterHandler = { flushDocToMongo: sinon.stub().callsArgWith(2, null) } @@ -70,10 +73,10 @@ describe 'ReferencesHandler', -> @handler._findBibDocIds.callCount.should.equal 0 done() - it 'should call Project.findPopulatedById', (done) -> + it 'should call Project.find', (done) -> @call (err, data) => - @Project.findPopulatedById.callCount.should.equal 1 - @Project.findPopulatedById.calledWith(@projectId).should.equal true + @Project.find.callCount.should.equal 1 + @Project.find.calledWith(_id: @projectId).should.equal true done() it 'should not call _findBibDocIds', (done) -> @@ -109,10 +112,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.findPopulatedById produces an error', -> + describe 'when Project.find produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, new Error('woops')) + @Project.find.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -129,7 +132,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @Project.find.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -147,7 +150,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @Project.find.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) @@ -167,7 +170,7 @@ describe 'ReferencesHandler', -> describe 'when request produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @Project.find.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, null, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, null) @request.post.callsArgWith(1, new Error('woops')) @@ -182,7 +185,7 @@ describe 'ReferencesHandler', -> describe 'when request responds with error status', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @Project.find.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, null, false) @request.post.callsArgWith(1, null, {statusCode: 500}, null) @@ -234,10 +237,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.findPopulatedById produces an error', -> + describe 'when Project.find produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, new Error('woops')) + @Project.find.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -254,7 +257,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @Project.find.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -272,7 +275,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.findPopulatedById.callsArgWith(1, null, @fakeProject) + @Project.find.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) @@ -312,16 +315,19 @@ describe 'ReferencesHandler', -> beforeEach -> @fakeProject = - owner_ref: - features: - references: false + owner_ref: @owner_ref = "owner-ref-123" + @owner = + features: + references: false + @User.find = sinon.stub() + @User.find.withArgs({_id: @owner_ref}, {features: true}).yields(null, @owner) @call = (callback) => @handler._isFullIndex @fakeProject, callback describe 'with references feature on', -> beforeEach -> - @fakeProject.owner_ref.features.references = true + @owner.features.references = true it 'should return true', -> @call (err, isFullIndex) => @@ -331,7 +337,7 @@ describe 'ReferencesHandler', -> describe 'with references feature off', -> beforeEach -> - @fakeProject.owner_ref.features.references = false + @owner.features.references = false it 'should return false', -> @call (err, isFullIndex) => From 359689ffea096c3f69fd46ab2ba348edc4871fcc Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 14:19:38 +0000 Subject: [PATCH 011/208] find -> findOne --- .../References/ReferencesHandler.coffee | 4 +-- .../References/ReferencesHandlerTests.coffee | 28 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index 1113a2f8a6..25bdbd443c 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -37,7 +37,7 @@ module.exports = ReferencesHandler = callback(null, owner?.features?.references == true) indexAll: (projectId, callback=(err, data)->) -> - Project.find { _id: projectId }, (err, project) -> + Project.findOne { _id: projectId }, (err, project) -> if err logger.err {err, projectId}, "error finding project" return callback(err) @@ -46,7 +46,7 @@ module.exports = ReferencesHandler = ReferencesHandler._doIndexOperation(projectId, project, docIds, callback) index: (projectId, docIds, callback=(err, data)->) -> - Project.find { _id: projectId }, (err, project) -> + Project.findOne { _id: projectId }, (err, project) -> if err logger.err {err, projectId}, "error finding project" return callback(err) diff --git a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee index 3eacae73cd..f1ae93f3bb 100644 --- a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee @@ -41,7 +41,7 @@ describe 'ReferencesHandler', -> } '../../models/Project': { Project: @Project = { - find: sinon.stub().callsArgWith(1, null, @fakeProject) + findOne: sinon.stub().callsArgWith(1, null, @fakeProject) } } '../../models/User': { @@ -73,10 +73,10 @@ describe 'ReferencesHandler', -> @handler._findBibDocIds.callCount.should.equal 0 done() - it 'should call Project.find', (done) -> + it 'should call Project.findOne', (done) -> @call (err, data) => - @Project.find.callCount.should.equal 1 - @Project.find.calledWith(_id: @projectId).should.equal true + @Project.findOne.callCount.should.equal 1 + @Project.findOne.calledWith(_id: @projectId).should.equal true done() it 'should not call _findBibDocIds', (done) -> @@ -112,10 +112,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.find produces an error', -> + describe 'when Project.findOne produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, new Error('woops')) + @Project.findOne.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -132,7 +132,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, null, @fakeProject) + @Project.findOne.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -150,7 +150,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, null, @fakeProject) + @Project.findOne.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) @@ -170,7 +170,7 @@ describe 'ReferencesHandler', -> describe 'when request produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, null, @fakeProject) + @Project.findOne.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, null, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, null) @request.post.callsArgWith(1, new Error('woops')) @@ -185,7 +185,7 @@ describe 'ReferencesHandler', -> describe 'when request responds with error status', -> beforeEach -> - @Project.find.callsArgWith(1, null, @fakeProject) + @Project.findOne.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, null, false) @request.post.callsArgWith(1, null, {statusCode: 500}, null) @@ -237,10 +237,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.find produces an error', -> + describe 'when Project.findOne produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, new Error('woops')) + @Project.findOne.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -257,7 +257,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, null, @fakeProject) + @Project.findOne.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -275,7 +275,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.find.callsArgWith(1, null, @fakeProject) + @Project.findOne.callsArgWith(1, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) From 76af5e5563e6f5a89adbe842e48ca7d88998fa62 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 14:20:00 +0000 Subject: [PATCH 012/208] Don't call deprecated findPopulatedById in loadEditor --- .../coffee/Features/Project/ProjectController.coffee | 2 +- .../coffee/Project/ProjectControllerTests.coffee | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index d89a014e2b..74513ec3e1 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -187,7 +187,7 @@ module.exports = ProjectController = async.parallel { project: (cb)-> - Project.findPopulatedById project_id, cb + Project.findOne { _id: project_id }, cb user: (cb)-> if user_id == 'openUser' cb null, defaultSettingsForAnonymousUser(user_id) diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 186c2e9836..73ada7a3cd 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -36,7 +36,7 @@ describe "ProjectController", -> @NotificationsHandler = getUserNotifications: sinon.stub() @ProjectModel = - findPopulatedById: sinon.stub() + findOne: sinon.stub() @UserModel = findById: sinon.stub() @SecurityManager = @@ -295,7 +295,7 @@ describe "ProjectController", -> fontSize:"massive" theme:"sexy" email: "bob@bob.com" - @ProjectModel.findPopulatedById.callsArgWith 1, null, @project + @ProjectModel.findOne.callsArgWith 1, null, @project @UserModel.findById.callsArgWith(1, null, @user) @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) @SecurityManager.userCanAccessProject.callsArgWith 2, true, "owner" @@ -310,12 +310,6 @@ describe "ProjectController", -> done() @ProjectController.loadEditor @req, @res - it "should add the project onto the opts", (done)-> - @res.render = (pageName, opts)=> - opts.project.should.equal @project - done() - @ProjectController.loadEditor @req, @res - it "should add user", (done)-> @res.render = (pageName, opts)=> opts.user.email.should.equal @user.email From e53fc5f0b652599138a2a7a4bc59700e43b6adf3 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 14:20:53 +0000 Subject: [PATCH 013/208] Remove dead code (Project.findPopulatedById) --- services/web/app/coffee/models/Project.coffee | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 3a391adfc0..147ff12911 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -43,23 +43,6 @@ ProjectSchema.statics.getProject = (project_or_id, fields, callback)-> return callback(new Errors.NotFoundError(e.message)) this.findById project_or_id, fields, callback -ProjectSchema.statics.findPopulatedById = (project_id, callback)-> - logger.log project_id:project_id, "findPopulatedById" - this.find(_id: project_id ) - .populate('collaberator_refs') - .populate('readOnly_refs') - .populate('owner_ref') - .exec (err, projects)-> - if err? - logger.err err:err, project_id:project_id, "something went wrong looking for project findPopulatedById" - callback(err) - else if !projects? || projects.length == 0 - logger.err project_id:project_id, "something went wrong looking for project findPopulatedById, no project could be found" - callback "not found" - else - logger.log project_id:project_id, "finished findPopulatedById" - callback(null, projects[0]) - sanitizeTypeOfElement = (elementType)-> lastChar = elementType.slice -1 if lastChar != "s" From 3e423b8a0690763d589e7b26cee403c29b09628c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 8 Mar 2016 14:38:25 +0000 Subject: [PATCH 014/208] Another find->findOne --- .../web/app/coffee/Features/References/ReferencesHandler.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index 25bdbd443c..b64600fdb1 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -32,7 +32,7 @@ module.exports = ReferencesHandler = return ids _isFullIndex: (project, callback = (err, result) ->) -> - User.find { _id: project.owner_ref }, { features: true }, (err, owner) -> + User.findOne { _id: project.owner_ref }, { features: true }, (err, owner) -> return callback(err) if err? callback(null, owner?.features?.references == true) From 37c966ba7ebedbf9c32df2bc1017e0a18bcfb291 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 14:42:11 +0000 Subject: [PATCH 015/208] Fix unit test --- .../UnitTests/coffee/References/ReferencesHandlerTests.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee index f1ae93f3bb..1666a2def1 100644 --- a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee @@ -319,8 +319,8 @@ describe 'ReferencesHandler', -> @owner = features: references: false - @User.find = sinon.stub() - @User.find.withArgs({_id: @owner_ref}, {features: true}).yields(null, @owner) + @User.findOne = sinon.stub() + @User.findOne.withArgs({_id: @owner_ref}, {features: true}).yields(null, @owner) @call = (callback) => @handler._isFullIndex @fakeProject, callback From e1fa77dd72e0dc789dff951167a9470a53eb60f4 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 8 Mar 2016 15:59:04 +0000 Subject: [PATCH 016/208] Add beginnings of acceptance tests --- services/web/.gitignore | 1 + services/web/Gruntfile.coffee | 17 ++++ services/web/package.json | 3 +- .../coffee/AuthorizationTests.coffee | 86 +++++++++++++++++++ 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 services/web/test/acceptance/coffee/AuthorizationTests.coffee diff --git a/services/web/.gitignore b/services/web/.gitignore index f8a1fc0842..a3f982ef53 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -40,6 +40,7 @@ app.js app/js/* test/UnitTests/js/* test/smoke/js/* +test/acceptance/js/* cookies.txt requestQueueWorker.js TpdsWorker.js diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 7d49df07af..3cfe803b17 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -71,6 +71,14 @@ module.exports = (grunt) -> dest: 'test/UnitTests/js/', ext: '.js' + acceptance_tests: + expand: true, + flatten: false, + cwd: 'test/acceptance/coffee', + src: ['**/*.coffee'], + dest: 'test/acceptance/js/', + ext: '.js' + less: app: files: @@ -119,6 +127,7 @@ module.exports = (grunt) -> clean: app: ["app/js"] unit_tests: ["test/UnitTests/js"] + acceptance_tests: ["test/acceptance/js"] mochaTest: unit: @@ -131,6 +140,11 @@ module.exports = (grunt) -> options: reporter: grunt.option('reporter') or 'spec' grep: grunt.option("grep") + acceptance: + src: ["test/acceptance/js/#{grunt.option('feature') or '**'}/*.js"] + options: + reporter: grunt.option('reporter') or 'spec' + grep: grunt.option("grep") "git-rev-parse": version: @@ -184,6 +198,7 @@ module.exports = (grunt) -> ] "Test tasks": [ "test:unit" + "test:acceptance" ] "Run tasks": [ "run" @@ -290,6 +305,7 @@ module.exports = (grunt) -> grunt.registerTask 'compile:css', 'Compile the less files to css', ['less'] grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append"] grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests'] + grunt.registerTask 'compile:acceptance_tests', 'Compile the acceptance tests', ['clean:acceptance_tests', 'coffee:acceptance_tests'] grunt.registerTask 'compile:smoke_tests', 'Compile the smoke tests', ['coffee:smoke_tests'] grunt.registerTask 'compile:tests', 'Compile all the tests', ['compile:smoke_tests', 'compile:unit_tests'] grunt.registerTask 'compile', 'Compiles everything need to run web-sharelatex', ['compile:server', 'compile:client', 'compile:css'] @@ -297,6 +313,7 @@ module.exports = (grunt) -> grunt.registerTask 'install', "Compile everything when installing as an npm module", ['compile'] grunt.registerTask 'test:unit', 'Run the unit tests (use --grep= or --feature= for individual tests)', ['compile:server', 'compile:modules:server', 'compile:unit_tests', 'compile:modules:unit_tests', 'mochaTest:unit'].concat(moduleUnitTestTasks) + grunt.registerTask 'test:acceptance', 'Run the acceptance tests (use --grep= or --feature= for individual tests)', ['compile:acceptance_tests', 'mochaTest:acceptance'] grunt.registerTask 'test:smoke', 'Run the smoke tests', ['compile:smoke_tests', 'mochaTest:smoke'] grunt.registerTask 'test:modules:unit', 'Run the unit tests for the modules', ['compile:modules:server', 'compile:modules:unit_tests'].concat(moduleUnitTestTasks) diff --git a/services/web/package.json b/services/web/package.json index 637df85a8f..021d86f6d7 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -44,7 +44,8 @@ "redback": "0.4.0", "redis": "0.10.1", "redis-sharelatex": "0.0.9", - "request": "2.34.0", + "request": "^2.69.0", + "requests": "^0.1.7", "rimraf": "2.2.6", "sanitizer": "0.1.1", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee new file mode 100644 index 0000000000..d5c1602ca6 --- /dev/null +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -0,0 +1,86 @@ +request = require("request") +expect = require("chai").expect + +count = 0 +BASE_URL = "http://localhost:3000" + +request = request.defaults({ + baseUrl: BASE_URL, + followRedirect: false +}) + +class User + constructor: (options = {}) -> + @email = "acceptance-test-#{count}@example.com" + @password = "acceptance-test-#{count}-password" + count++ + @jar = request.jar() + @request = request.defaults({ + jar: @jar + }) + + login: (callback = (error) ->) -> + @getCsrfToken (error) => + return callback(error) if error? + @request.post { + url: "/register" # Register will log in, but also ensure user exists + json: + email: @email + password: @password + }, (error, response, body) => + return callback(error) if error? + callback() + + createProject: (name, callback = (error, project_id) ->) -> + @request.post { + url: "/project/new", + json: + projectName: name + }, (error, response, body) -> + return callback(error) if error? + callback(null, body.project_id) + + getCsrfToken: (callback = (error) ->) -> + @request.get { + url: "/register" + }, (err, response, body) => + return callback(error) if error? + csrfMatches = body.match("window.csrfToken = \"(.*?)\";") + if !csrfMatches? + return callback(new Error("no csrf token found")) + @request = @request.defaults({ + headers: + "x-csrf-token": csrfMatches[1] + }) + callback() + +describe "Authorization", -> + describe "private project", -> + before (done) -> + @owner = new User() + @other = new User() + @owner.login (error) => + return done(error) if error? + @other.login (error) => + return done(error) if error? + @owner.createProject "private-project", (error, project_id) => + return done(error) if error? + @project_id = project_id + done() + + it "should allow the owner to access it", (done) -> + @owner.request.get "/project/#{@project_id}", (error, response, body) -> + expect(response.statusCode).to.equal 200 + done() + + it "should not allow another user to access it", (done) -> + @other.request.get "/project/#{@project_id}", (error, response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/restricted" + done() + + it "should not allow anonymous user to access it", (done) -> + request.get "/project/#{@project_id}", (error, response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/login" + done() \ No newline at end of file From 4f9f255153ef12d7a538760417329b367e44d7c4 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 9 Mar 2016 12:31:46 +0000 Subject: [PATCH 017/208] Extend acceptance tests to include shared projects --- .../Collaborators/CollaboratorsHandler.coffee | 2 +- .../coffee/AuthorizationTests.coffee | 196 ++++++++++++++++-- 2 files changed, 174 insertions(+), 24 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 72372345f8..f6b8ca7bbd 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -1,6 +1,5 @@ UserCreator = require('../User/UserCreator') Project = require("../../models/Project").Project -ProjectEntityHandler = require("../Project/ProjectEntityHandler") mimelib = require("mimelib") logger = require('logger-sharelatex') UserGetter = require "../User/UserGetter" @@ -108,6 +107,7 @@ module.exports = CollaboratorsHandler = Project.update { _id: project_id }, { $addToSet: level }, (error) -> return callback(error) if error? # Flush to TPDS in background to add files to collaborator's Dropbox + ProjectEntityHandler = require("../Project/ProjectEntityHandler") ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, (error) -> if error? logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator" diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index d5c1602ca6..478ef598a0 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -1,5 +1,6 @@ request = require("request") expect = require("chai").expect +async = require "async" count = 0 BASE_URL = "http://localhost:3000" @@ -40,6 +41,14 @@ class User return callback(error) if error? callback(null, body.project_id) + addUserToProject: (project_id, email, privileges, callback = (error, user) ->) -> + @request.post { + url: "/project/#{project_id}/users", + json: {email, privileges} + }, (error, response, body) -> + return callback(error) if error? + callback(null, body.user) + getCsrfToken: (callback = (error) ->) -> @request.get { url: "/register" @@ -54,33 +63,174 @@ class User }) callback() +try_read_access = (requester, project_id, test, callback) -> + async.parallel [ + (cb) -> + requester.get "/project/#{project_id}", (error, response, body) -> + return cb(error) if error? + test(response, body) + cb() + (cb) -> + requester.get "/project/#{project_id}/download/zip", (error, response, body) -> + return cb(error) if error? + test(response, body) + cb() + ], callback + +try_write_access = (requester, project_id, test, callback) -> + async.parallel [ + (cb) -> + requester.post { + uri: "/project/#{project_id}/settings" + json: + compiler: "latex" + }, (error, response, body) -> + return cb(error) if error? + test(response, body) + cb() + ], callback + +try_admin_access = (requester, project_id, test, callback) -> + async.parallel [ + (cb) -> + requester.post { + uri: "/project/#{project_id}/rename" + json: + newProjectName: "new-name" + }, (error, response, body) -> + return cb(error) if error? + test(response, body) + cb() + ], callback + +expect_read_access = (requester, project_id, callback) -> + try_read_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.be.oneOf [200, 204] + , callback) + +expect_write_access = (requester, project_id, callback) -> + try_write_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.be.oneOf [200, 204] + , callback) + +expect_admin_access = (requester, project_id, callback) -> + try_admin_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.be.oneOf [200, 204] + , callback) + +expect_no_read_access = (requester, project_id, callback) -> + try_read_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/restricted" + , callback) + +expect_no_write_access = (requester, project_id, callback) -> + try_write_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/restricted" + , callback) + +expect_no_admin_access = (requester, project_id, callback) -> + try_admin_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/restricted" + , callback) + +expect_no_anonymous_read_access = (requester, project_id, callback) -> + try_read_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/login" + , callback) + +expect_no_anonymous_write_access = (requester, project_id, callback) -> + try_write_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/login" + , callback) + +expect_no_anonymous_admin_access = (requester, project_id, callback) -> + try_admin_access(requester, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal "/login" + , callback) + describe "Authorization", -> + before (done) -> + @timeout(10000) + @owner = new User() + @other1 = new User() + @other2 = new User() + @anon = new User() + async.parallel [ + (cb) => @owner.login cb + (cb) => @other1.login cb + (cb) => @other2.login cb + (cb) => @anon.getCsrfToken cb + ], done + describe "private project", -> before (done) -> - @owner = new User() - @other = new User() - @owner.login (error) => + @owner.createProject "private-project", (error, project_id) => return done(error) if error? - @other.login (error) => - return done(error) if error? - @owner.createProject "private-project", (error, project_id) => - return done(error) if error? - @project_id = project_id - done() + @project_id = project_id + done() - it "should allow the owner to access it", (done) -> - @owner.request.get "/project/#{@project_id}", (error, response, body) -> - expect(response.statusCode).to.equal 200 - done() + it "should allow the owner read access to it", (done) -> + expect_read_access @owner.request, @project_id, done - it "should not allow another user to access it", (done) -> - @other.request.get "/project/#{@project_id}", (error, response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/restricted" - done() + it "should allow the owner write access to it", (done) -> + expect_write_access @owner.request, @project_id, done + + it "should allow the owner admin access to it", (done) -> + expect_admin_access @owner.request, @project_id, done - it "should not allow anonymous user to access it", (done) -> - request.get "/project/#{@project_id}", (error, response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/login" - done() \ No newline at end of file + it "should not allow another user read access to it", (done) -> + expect_no_read_access(@other1.request, @project_id, done) + + it "should not allow another user write access to it", (done) -> + expect_no_write_access(@other1.request, @project_id, done) + + it "should not allow another user admin access to it", (done) -> + expect_no_admin_access(@other1.request, @project_id, done) + + it "should not allow anonymous user read access to it", (done) -> + expect_no_anonymous_read_access(@anon.request, @project_id, done) + + it "should not allow anonymous user write access to it", (done) -> + expect_no_anonymous_write_access(@anon.request, @project_id, done) + + it "should not allow anonymous user write access to it", (done) -> + expect_no_anonymous_admin_access(@anon.request, @project_id, done) + + describe "shared project", -> + before (done) -> + @rw_user = @other1 + @ro_user = @other2 + @owner.createProject "private-project", (error, project_id) => + return done(error) if error? + @project_id = project_id + @owner.addUserToProject @project_id, @ro_user.email, "readOnly", (error) => + return done(error) if error? + @owner.addUserToProject @project_id, @rw_user.email, "readAndWrite", (error) => + return done(error) if error? + done() + + it "should allow the read-only user read access to it", (done) -> + expect_read_access @ro_user.request, @project_id, done + + it "should not allow the read-only user write access to it", (done) -> + expect_no_write_access @ro_user.request, @project_id, done + + it "should not allow the read-only user admin access to it", (done) -> + expect_no_admin_access @ro_user.request, @project_id, done + + it "should allow the read-write user read access to it", (done) -> + expect_read_access @rw_user.request, @project_id, done + + it "should allow the read-write user write access to it", (done) -> + expect_write_access @rw_user.request, @project_id, done + + it "should not allow the read-write user admin access to it", (done) -> + expect_no_admin_access @rw_user.request, @project_id, done + + \ No newline at end of file From 2116d0271c047166a13ad96b00f5b43cf5eb22af Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 9 Mar 2016 15:30:23 +0000 Subject: [PATCH 018/208] Update acceptance tests for public projects --- services/web/config/settings.defaults.coffee | 2 +- .../coffee/AuthorizationTests.coffee | 107 ++++++++++-------- 2 files changed, 63 insertions(+), 46 deletions(-) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index d900d17062..b45837193b 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -256,7 +256,7 @@ module.exports = # Should we allow access to any page without logging in? This includes # public projects, /learn, /templates, about pages, etc. - allowPublicAccess: false + allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"]? then true else false # Maximum size of text documents in the real-time editing system. max_doc_length: 2 * 1024 * 1024 # 2mb diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index 478ef598a0..6c7ff65539 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -49,6 +49,15 @@ class User return callback(error) if error? callback(null, body.user) + makePublic: (project_id, level, callback = (error) ->) -> + @request.post { + url: "/project/#{project_id}/settings", + json: + publicAccessLevel: level + }, (error, response, body) -> + return callback(error) if error? + callback(null) + getCsrfToken: (callback = (error) ->) -> @request.get { url: "/register" @@ -77,7 +86,7 @@ try_read_access = (requester, project_id, test, callback) -> cb() ], callback -try_write_access = (requester, project_id, test, callback) -> +try_settings_write_access = (requester, project_id, test, callback) -> async.parallel [ (cb) -> requester.post { @@ -105,11 +114,13 @@ try_admin_access = (requester, project_id, test, callback) -> expect_read_access = (requester, project_id, callback) -> try_read_access(requester, project_id, (response, body) -> + if response.statusCode not in [200,204] + console.log response.statusCode, response.headers expect(response.statusCode).to.be.oneOf [200, 204] , callback) -expect_write_access = (requester, project_id, callback) -> - try_write_access(requester, project_id, (response, body) -> +expect_settings_write_access = (requester, project_id, callback) -> + try_settings_write_access(requester, project_id, (response, body) -> expect(response.statusCode).to.be.oneOf [200, 204] , callback) @@ -118,40 +129,22 @@ expect_admin_access = (requester, project_id, callback) -> expect(response.statusCode).to.be.oneOf [200, 204] , callback) -expect_no_read_access = (requester, project_id, callback) -> +expect_no_read_access = (requester, project_id, options, callback) -> try_read_access(requester, project_id, (response, body) -> expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/restricted" + expect(response.headers.location).to.equal options.redirect_to , callback) -expect_no_write_access = (requester, project_id, callback) -> - try_write_access(requester, project_id, (response, body) -> +expect_no_settings_write_access = (requester, project_id, options, callback) -> + try_settings_write_access(requester, project_id, (response, body) -> expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/restricted" + expect(response.headers.location).to.equal options.redirect_to , callback) -expect_no_admin_access = (requester, project_id, callback) -> +expect_no_admin_access = (requester, project_id, options, callback) -> try_admin_access(requester, project_id, (response, body) -> expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/restricted" - , callback) - -expect_no_anonymous_read_access = (requester, project_id, callback) -> - try_read_access(requester, project_id, (response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/login" - , callback) - -expect_no_anonymous_write_access = (requester, project_id, callback) -> - try_write_access(requester, project_id, (response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/login" - , callback) - -expect_no_anonymous_admin_access = (requester, project_id, callback) -> - try_admin_access(requester, project_id, (response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal "/login" + expect(response.headers.location).to.equal options.redirect_to , callback) describe "Authorization", -> @@ -178,29 +171,29 @@ describe "Authorization", -> it "should allow the owner read access to it", (done) -> expect_read_access @owner.request, @project_id, done - it "should allow the owner write access to it", (done) -> - expect_write_access @owner.request, @project_id, done + it "should allow the owner write access to its settings", (done) -> + expect_settings_write_access @owner.request, @project_id, done it "should allow the owner admin access to it", (done) -> expect_admin_access @owner.request, @project_id, done it "should not allow another user read access to it", (done) -> - expect_no_read_access(@other1.request, @project_id, done) + expect_no_read_access @other1.request, @project_id, redirect_to: "/restricted", done - it "should not allow another user write access to it", (done) -> - expect_no_write_access(@other1.request, @project_id, done) + it "should not allow another user write access to its settings", (done) -> + expect_no_settings_write_access @other1.request, @project_id, redirect_to: "/restricted", done it "should not allow another user admin access to it", (done) -> - expect_no_admin_access(@other1.request, @project_id, done) + expect_no_admin_access @other1.request, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user read access to it", (done) -> - expect_no_anonymous_read_access(@anon.request, @project_id, done) + expect_no_read_access @other1.request, @project_id, redirect_to: "/restricted", done - it "should not allow anonymous user write access to it", (done) -> - expect_no_anonymous_write_access(@anon.request, @project_id, done) + it "should not allow anonymous user write access to its settings", (done) -> + expect_no_settings_write_access @other1.request, @project_id, redirect_to: "/restricted", done - it "should not allow anonymous user write access to it", (done) -> - expect_no_anonymous_admin_access(@anon.request, @project_id, done) + it "should not allow anonymous user admin access to it", (done) -> + expect_no_admin_access @other1.request, @project_id, redirect_to: "/restricted", done describe "shared project", -> before (done) -> @@ -218,19 +211,43 @@ describe "Authorization", -> it "should allow the read-only user read access to it", (done) -> expect_read_access @ro_user.request, @project_id, done - it "should not allow the read-only user write access to it", (done) -> - expect_no_write_access @ro_user.request, @project_id, done + it "should not allow the read-only user write access to its settings", (done) -> + expect_no_settings_write_access @ro_user.request, @project_id, redirect_to: "/restricted", done it "should not allow the read-only user admin access to it", (done) -> - expect_no_admin_access @ro_user.request, @project_id, done + expect_no_admin_access @ro_user.request, @project_id, redirect_to: "/restricted", done it "should allow the read-write user read access to it", (done) -> expect_read_access @rw_user.request, @project_id, done - it "should allow the read-write user write access to it", (done) -> - expect_write_access @rw_user.request, @project_id, done + it "should allow the read-write user write access to its settings", (done) -> + expect_settings_write_access @rw_user.request, @project_id, done it "should not allow the read-write user admin access to it", (done) -> - expect_no_admin_access @rw_user.request, @project_id, done + expect_no_admin_access @rw_user.request, @project_id, redirect_to: "/restricted", done + describe "public read-write project", -> + before (done) -> + @owner.createProject "public-rw-project", (error, project_id) => + return done(error) if error? + @project_id = project_id + @owner.makePublic @project_id, "readAndWrite", done + + it "should allow a user read access to it", (done) -> + expect_read_access @other1.request, @project_id, done + + it "should not allow a user write access to its settings"#, (done) -> + # expect_no_settings_write_access @other1.request, @project_id, redirect_to: "/restricted", done + + it "should not allow a user admin access to it", (done) -> + expect_no_admin_access @other1.request, @project_id, redirect_to: "/restricted", done + + it "should allow anonymous user read access to it", (done) -> + expect_read_access @anon.request, @project_id, done + + it "should not allow anonymous user write access to its settings", (done) -> + expect_no_settings_write_access @anon.request, @project_id, redirect_to: "/restricted", done + + it "should not allow anonymous user admin access to it", (done) -> + expect_no_admin_access @anon.request, @project_id, redirect_to: "/restricted", done \ No newline at end of file From c46c083b3191bdc0caaa9e0ca4f23b45e5d30928 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 9 Mar 2016 16:26:18 +0000 Subject: [PATCH 019/208] Check write access to documents via real-time end point --- .../coffee/AuthorizationTests.coffee | 164 ++++++++++++------ 1 file changed, 110 insertions(+), 54 deletions(-) diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index 6c7ff65539..a5eaa12fda 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -1,6 +1,8 @@ request = require("request") expect = require("chai").expect async = require "async" +settings = require("settings-sharelatex") +{db} = require("../../../app/js/infrastructure/mongojs") count = 0 BASE_URL = "http://localhost:3000" @@ -30,7 +32,10 @@ class User password: @password }, (error, response, body) => return callback(error) if error? - callback() + db.users.findOne {email: @email}, (error, user) => + return callback(error) if error? + @id = user?._id?.toString() + callback() createProject: (name, callback = (error, project_id) ->) -> @request.post { @@ -72,24 +77,24 @@ class User }) callback() -try_read_access = (requester, project_id, test, callback) -> +try_read_access = (user, project_id, test, callback) -> async.parallel [ (cb) -> - requester.get "/project/#{project_id}", (error, response, body) -> + user.request.get "/project/#{project_id}", (error, response, body) -> return cb(error) if error? test(response, body) cb() (cb) -> - requester.get "/project/#{project_id}/download/zip", (error, response, body) -> + user.request.get "/project/#{project_id}/download/zip", (error, response, body) -> return cb(error) if error? test(response, body) cb() ], callback -try_settings_write_access = (requester, project_id, test, callback) -> +try_settings_write_access = (user, project_id, test, callback) -> async.parallel [ (cb) -> - requester.post { + user.request.post { uri: "/project/#{project_id}/settings" json: compiler: "latex" @@ -99,10 +104,10 @@ try_settings_write_access = (requester, project_id, test, callback) -> cb() ], callback -try_admin_access = (requester, project_id, test, callback) -> +try_admin_access = (user, project_id, test, callback) -> async.parallel [ (cb) -> - requester.post { + user.request.post { uri: "/project/#{project_id}/rename" json: newProjectName: "new-name" @@ -112,39 +117,78 @@ try_admin_access = (requester, project_id, test, callback) -> cb() ], callback -expect_read_access = (requester, project_id, callback) -> - try_read_access(requester, project_id, (response, body) -> - if response.statusCode not in [200,204] - console.log response.statusCode, response.headers +try_content_access = (user, project_id, test, callback) -> + # The real-time service calls this end point to determine the user's + # permissions. + request.post { + url: "/project/#{project_id}/join" + qs: {user_id: user.id} + auth: + user: settings.apis.web.user + pass: settings.apis.web.pass + sendImmediately: true + json: true + jar: false + }, (error, response, body) -> + return callback(error) if error? + test(response, body) + callback() + +expect_read_access = (user, project_id, callback) -> + async.series [ + (cb) -> + try_read_access(user, project_id, (response, body) -> + expect(response.statusCode).to.be.oneOf [200, 204] + , cb) + (cb) -> + try_content_access(user, project_id, (response, body) -> + expect(body.privilegeLevel).to.be.oneOf ["owner", "readAndWrite", "readOnly"] + , cb) + ], callback + +expect_content_write_access = (user, project_id, callback) -> + try_content_access(user, project_id, (response, body) -> + expect(body.privilegeLevel).to.be.oneOf ["owner", "readAndWrite"] + , callback) + +expect_settings_write_access = (user, project_id, callback) -> + try_settings_write_access(user, project_id, (response, body) -> expect(response.statusCode).to.be.oneOf [200, 204] , callback) -expect_settings_write_access = (requester, project_id, callback) -> - try_settings_write_access(requester, project_id, (response, body) -> +expect_admin_access = (user, project_id, callback) -> + try_admin_access(user, project_id, (response, body) -> expect(response.statusCode).to.be.oneOf [200, 204] , callback) -expect_admin_access = (requester, project_id, callback) -> - try_admin_access(requester, project_id, (response, body) -> - expect(response.statusCode).to.be.oneOf [200, 204] +expect_no_read_access = (user, project_id, options, callback) -> + async.series [ + (cb) -> + try_read_access(user, project_id, (response, body) -> + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.match new RegExp(options.redirect_to) + , cb) + (cb) -> + try_content_access(user, project_id, (response, body) -> + expect(body.privilegeLevel).to.be.equal false + , cb) + ], callback + +expect_no_content_write_access = (user, project_id, callback) -> + try_content_access(user, project_id, (response, body) -> + expect(body.privilegeLevel).to.be.oneOf [false, "readOnly"] , callback) -expect_no_read_access = (requester, project_id, options, callback) -> - try_read_access(requester, project_id, (response, body) -> +expect_no_settings_write_access = (user, project_id, options, callback) -> + try_settings_write_access(user, project_id, (response, body) -> expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal options.redirect_to + expect(response.headers.location).to.match new RegExp(options.redirect_to) , callback) -expect_no_settings_write_access = (requester, project_id, options, callback) -> - try_settings_write_access(requester, project_id, (response, body) -> +expect_no_admin_access = (user, project_id, options, callback) -> + try_admin_access(user, project_id, (response, body) -> expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal options.redirect_to - , callback) - -expect_no_admin_access = (requester, project_id, options, callback) -> - try_admin_access(requester, project_id, (response, body) -> - expect(response.statusCode).to.equal 302 - expect(response.headers.location).to.equal options.redirect_to + expect(response.headers.location).to.match new RegExp(options.redirect_to) , callback) describe "Authorization", -> @@ -169,31 +213,31 @@ describe "Authorization", -> done() it "should allow the owner read access to it", (done) -> - expect_read_access @owner.request, @project_id, done + expect_read_access @owner, @project_id, done it "should allow the owner write access to its settings", (done) -> - expect_settings_write_access @owner.request, @project_id, done + expect_settings_write_access @owner, @project_id, done it "should allow the owner admin access to it", (done) -> - expect_admin_access @owner.request, @project_id, done + expect_admin_access @owner, @project_id, done - it "should not allow another user read access to it", (done) -> - expect_no_read_access @other1.request, @project_id, redirect_to: "/restricted", done + it "should not allow another user read access to the project", (done) -> + expect_no_read_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow another user write access to its settings", (done) -> - expect_no_settings_write_access @other1.request, @project_id, redirect_to: "/restricted", done + expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow another user admin access to it", (done) -> - expect_no_admin_access @other1.request, @project_id, redirect_to: "/restricted", done + expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user read access to it", (done) -> - expect_no_read_access @other1.request, @project_id, redirect_to: "/restricted", done + expect_no_read_access @anon, @project_id, redirect_to: "/login", done it "should not allow anonymous user write access to its settings", (done) -> - expect_no_settings_write_access @other1.request, @project_id, redirect_to: "/restricted", done + expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user admin access to it", (done) -> - expect_no_admin_access @other1.request, @project_id, redirect_to: "/restricted", done + expect_no_admin_access @anon, @project_id, redirect_to: "/restricted", done describe "shared project", -> before (done) -> @@ -209,22 +253,28 @@ describe "Authorization", -> done() it "should allow the read-only user read access to it", (done) -> - expect_read_access @ro_user.request, @project_id, done + expect_read_access @ro_user, @project_id, done + + it "should not allow the read-only user write access to its content", (done) -> + expect_no_content_write_access @ro_user, @project_id, done it "should not allow the read-only user write access to its settings", (done) -> - expect_no_settings_write_access @ro_user.request, @project_id, redirect_to: "/restricted", done + expect_no_settings_write_access @ro_user, @project_id, redirect_to: "/restricted", done it "should not allow the read-only user admin access to it", (done) -> - expect_no_admin_access @ro_user.request, @project_id, redirect_to: "/restricted", done + expect_no_admin_access @ro_user, @project_id, redirect_to: "/restricted", done it "should allow the read-write user read access to it", (done) -> - expect_read_access @rw_user.request, @project_id, done + expect_read_access @rw_user, @project_id, done + + it "should allow the read-write user write access to its content", (done) -> + expect_content_write_access @rw_user, @project_id, done it "should allow the read-write user write access to its settings", (done) -> - expect_settings_write_access @rw_user.request, @project_id, done + expect_settings_write_access @rw_user, @project_id, done it "should not allow the read-write user admin access to it", (done) -> - expect_no_admin_access @rw_user.request, @project_id, redirect_to: "/restricted", done + expect_no_admin_access @rw_user, @project_id, redirect_to: "/restricted", done describe "public read-write project", -> before (done) -> @@ -234,20 +284,26 @@ describe "Authorization", -> @owner.makePublic @project_id, "readAndWrite", done it "should allow a user read access to it", (done) -> - expect_read_access @other1.request, @project_id, done + expect_read_access @other1, @project_id, done + + it "should allow a user write access to its content", (done) -> + expect_content_write_access @owner, @project_id, done it "should not allow a user write access to its settings"#, (done) -> - # expect_no_settings_write_access @other1.request, @project_id, redirect_to: "/restricted", done + # expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow a user admin access to it", (done) -> - expect_no_admin_access @other1.request, @project_id, redirect_to: "/restricted", done + expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done - it "should allow anonymous user read access to it", (done) -> - expect_read_access @anon.request, @project_id, done + it "should allow an anonymous user read access to it", (done) -> + expect_read_access @anon, @project_id, done - it "should not allow anonymous user write access to its settings", (done) -> - expect_no_settings_write_access @anon.request, @project_id, redirect_to: "/restricted", done + it "should allow an anonymous user write access to its content", (done) -> + expect_content_write_access @owner, @project_id, done - it "should not allow anonymous user admin access to it", (done) -> - expect_no_admin_access @anon.request, @project_id, redirect_to: "/restricted", done + it "should not allow an anonymous user write access to its settings", (done) -> + expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done + + it "should not allow an anonymous user admin access to it", (done) -> + expect_no_admin_access @anon, @project_id, redirect_to: "/restricted", done \ No newline at end of file From d235ab22ed6a7121ea097abc371b1a71a5a88cff Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 9 Mar 2016 16:28:46 +0000 Subject: [PATCH 020/208] Add in tests for public read-only projects --- .../coffee/AuthorizationTests.coffee | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index a5eaa12fda..ad6f817429 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -287,7 +287,7 @@ describe "Authorization", -> expect_read_access @other1, @project_id, done it "should allow a user write access to its content", (done) -> - expect_content_write_access @owner, @project_id, done + expect_content_write_access @other1, @project_id, done it "should not allow a user write access to its settings"#, (done) -> # expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done @@ -299,11 +299,41 @@ describe "Authorization", -> expect_read_access @anon, @project_id, done it "should allow an anonymous user write access to its content", (done) -> - expect_content_write_access @owner, @project_id, done + expect_content_write_access @anon, @project_id, done it "should not allow an anonymous user write access to its settings", (done) -> expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done it "should not allow an anonymous user admin access to it", (done) -> expect_no_admin_access @anon, @project_id, redirect_to: "/restricted", done - \ No newline at end of file + + describe "public read-only project", -> + before (done) -> + @owner.createProject "public-ro-project", (error, project_id) => + return done(error) if error? + @project_id = project_id + @owner.makePublic @project_id, "readOnly", done + + it "should allow a user read access to it", (done) -> + expect_read_access @other1, @project_id, done + + it "should not allow a user write access to its content", (done) -> + expect_no_content_write_access @other1, @project_id, done + + it "should not allow a user write access to its settings"#, (done) -> + # expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done + + it "should not allow a user admin access to it", (done) -> + expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done + + it "should allow an anonymous user read access to it", (done) -> + expect_read_access @anon, @project_id, done + + it "should not allow an anonymous user write access to its content", (done) -> + expect_no_content_write_access @anon, @project_id, done + + it "should not allow an anonymous user write access to its settings", (done) -> + expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done + + it "should not allow an anonymous user admin access to it", (done) -> + expect_no_admin_access @anon, @project_id, redirect_to: "/restricted", done \ No newline at end of file From e36be96ec93fc06e5d91d0f566cd3f2cf0253e93 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 10 Mar 2016 11:13:57 +0000 Subject: [PATCH 021/208] Move public access setting to its own end point --- services/web/Gruntfile.coffee | 1 + .../Features/Project/ProjectController.coffee | 8 ++++++ services/web/app/coffee/router.coffee | 1 + .../ide/settings/services/settings.coffee | 5 ++++ .../ShareProjectModalController.coffee | 4 +-- .../Project/ProjectControllerTests.coffee | 25 ++++++++++--------- .../coffee/AuthorizationTests.coffee | 19 ++++++++++---- 7 files changed, 44 insertions(+), 19 deletions(-) diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 3cfe803b17..428e4d506d 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -143,6 +143,7 @@ module.exports = (grunt) -> acceptance: src: ["test/acceptance/js/#{grunt.option('feature') or '**'}/*.js"] options: + timeout: 10000 reporter: grunt.option('reporter') or 'spec' grep: grunt.option("grep") diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 74513ec3e1..0ca14090c2 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -42,6 +42,14 @@ module.exports = ProjectController = jobs.push (callback) -> editorController.setRootDoc project_id, req.body.rootDocId, callback + async.series jobs, (error) -> + return next(error) if error? + res.sendStatus(204) + + updateProjectAdminSettings: (req, res, next) -> + project_id = req.params.Project_id + + jobs = [] if req.body.publicAccessLevel? jobs.push (callback) -> editorController.setPublicAccessLevel project_id, req.body.publicAccessLevel, callback diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 46b1db2157..eafe41470b 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -103,6 +103,7 @@ module.exports = class Router }), SecurityManager.requestCanAccessProject, ProjectController.loadEditor webRouter.get '/Project/:Project_id/file/:File_id', SecurityManager.requestCanAccessProject, FileStoreController.getFile webRouter.post '/project/:Project_id/settings', SecurityManager.requestCanModifyProject, ProjectController.updateProjectSettings + webRouter.post '/project/:Project_id/settings/admin', SecurityManager.requestIsOwner, ProjectController.updateProjectAdminSettings webRouter.post '/project/:Project_id/compile', SecurityManager.requestCanAccessProject, CompileController.compile webRouter.get '/Project/:Project_id/output/output.pdf', SecurityManager.requestCanAccessProject, CompileController.downloadPdf diff --git a/services/web/public/coffee/ide/settings/services/settings.coffee b/services/web/public/coffee/ide/settings/services/settings.coffee index 78dda105ad..4e6bbcea3d 100644 --- a/services/web/public/coffee/ide/settings/services/settings.coffee +++ b/services/web/public/coffee/ide/settings/services/settings.coffee @@ -10,5 +10,10 @@ define [ saveProjectSettings: (data) -> data._csrf = window.csrfToken ide.$http.post "/project/#{ide.project_id}/settings", data + + saveProjectAdminSettings: (data) -> + data._csrf = window.csrfToken + ide.$http.post "/project/#{ide.project_id}/settings/admin", data + } ] \ No newline at end of file diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index bb945a9ebc..13d5faea9f 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -143,7 +143,7 @@ define [ $scope.makePublic = () -> $scope.project.publicAccesLevel = $scope.inputs.privileges - settings.saveProjectSettings({publicAccessLevel: $scope.inputs.privileges}) + settings.saveProjectAdminSettings({publicAccessLevel: $scope.inputs.privileges}) $modalInstance.close() $scope.cancel = () -> @@ -153,7 +153,7 @@ define [ App.controller "MakePrivateModalController", ["$scope", "$modalInstance", "settings", ($scope, $modalInstance, settings) -> $scope.makePrivate = () -> $scope.project.publicAccesLevel = "private" - settings.saveProjectSettings({publicAccessLevel: "private"}) + settings.saveProjectAdminSettings({publicAccessLevel: "private"}) $modalInstance.close() $scope.cancel = () -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 73ada7a3cd..0be616da51 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -126,18 +126,6 @@ describe "ProjectController", -> done() @ProjectController.updateProjectSettings @req, @res - it "should update the public access level", (done) -> - @EditorController.setPublicAccessLevel = sinon.stub().callsArg(2) - @req.body = - publicAccessLevel: @publicAccessLevel = "readonly" - @res.sendStatus = (code) => - @EditorController.setPublicAccessLevel - .calledWith(@project_id, @publicAccessLevel) - .should.equal true - code.should.equal 204 - done() - @ProjectController.updateProjectSettings @req, @res - it "should update the root doc", (done) -> @EditorController.setRootDoc = sinon.stub().callsArg(2) @req.body = @@ -149,6 +137,19 @@ describe "ProjectController", -> code.should.equal 204 done() @ProjectController.updateProjectSettings @req, @res + + describe "updateProjectAdminSettings", -> + it "should update the public access level", (done) -> + @EditorController.setPublicAccessLevel = sinon.stub().callsArg(2) + @req.body = + publicAccessLevel: @publicAccessLevel = "readonly" + @res.sendStatus = (code) => + @EditorController.setPublicAccessLevel + .calledWith(@project_id, @publicAccessLevel) + .should.equal true + code.should.equal 204 + done() + @ProjectController.updateProjectAdminSettings @req, @res describe "deleteProject", -> it "should tell the project deleter to archive when forever=false", (done)-> diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index ad6f817429..a9533e9533 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -56,7 +56,7 @@ class User makePublic: (project_id, level, callback = (error) ->) -> @request.post { - url: "/project/#{project_id}/settings", + url: "/project/#{project_id}/settings/admin", json: publicAccessLevel: level }, (error, response, body) -> @@ -78,7 +78,7 @@ class User callback() try_read_access = (user, project_id, test, callback) -> - async.parallel [ + async.series [ (cb) -> user.request.get "/project/#{project_id}", (error, response, body) -> return cb(error) if error? @@ -92,7 +92,7 @@ try_read_access = (user, project_id, test, callback) -> ], callback try_settings_write_access = (user, project_id, test, callback) -> - async.parallel [ + async.series [ (cb) -> user.request.post { uri: "/project/#{project_id}/settings" @@ -105,7 +105,7 @@ try_settings_write_access = (user, project_id, test, callback) -> ], callback try_admin_access = (user, project_id, test, callback) -> - async.parallel [ + async.series [ (cb) -> user.request.post { uri: "/project/#{project_id}/rename" @@ -115,6 +115,15 @@ try_admin_access = (user, project_id, test, callback) -> return cb(error) if error? test(response, body) cb() + (cb) -> + user.request.post { + uri: "/project/#{project_id}/settings/admin" + json: + publicAccessLevel: "private" + }, (error, response, body) -> + return cb(error) if error? + test(response, body) + cb() ], callback try_content_access = (user, project_id, test, callback) -> @@ -198,7 +207,7 @@ describe "Authorization", -> @other1 = new User() @other2 = new User() @anon = new User() - async.parallel [ + async.series [ (cb) => @owner.login cb (cb) => @other1.login cb (cb) => @other2.login cb From 36966f0c9bfa5025938033f0e101fb5fec2bab80 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 2 Mar 2016 15:29:52 +0000 Subject: [PATCH 022/208] wip --- .../Spelling/SpellingController.coffee | 2 +- services/web/app/views/layout.jade | 27 +++++++++++-------- .../public/coffee/main/user-details.coffee | 1 + 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/services/web/app/coffee/Features/Spelling/SpellingController.coffee b/services/web/app/coffee/Features/Spelling/SpellingController.coffee index 81a697525f..d92311ddb1 100644 --- a/services/web/app/coffee/Features/Spelling/SpellingController.coffee +++ b/services/web/app/coffee/Features/Spelling/SpellingController.coffee @@ -12,5 +12,5 @@ module.exports = SpellingController = request(url: Settings.apis.spelling.url + url, method: req.method, headers: req.headers, json: req.body, timeout:TEN_SECONDS) .on "error", (error) -> logger.error err: error, "Spelling API error" - res.sendStatus 500 + res.end() .pipe(res) diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index 013c8e5a60..a8cc76ee90 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -7,6 +7,7 @@ html(itemscope, itemtype='http://schema.org/Product') // Stop superfish from loading window.similarproducts = true style [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {display: none !important; display: none; } + style #groove-button { display:none;} -if (typeof(gaExperiments) != "undefined") |!{gaExperiments} @@ -25,6 +26,7 @@ html(itemscope, itemtype='http://schema.org/Product') each subdomainDetails in settings.i18n.subdomainLang link(rel="alternate", href=subdomainDetails.url+currentUrl, hreflang=subdomainDetails.lngCode) + meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor") -if (typeof(meta) == "undefined") @@ -60,8 +62,12 @@ html(itemscope, itemtype='http://schema.org/Product') sixpackDomain: '#{settings.sixpack.domain}' }; window.systemMessages = !{JSON.stringify(systemMessages).replace(/\//g, '\\/')}; - window.ab = {} - window.user_id = '#{getLoggedInUserId()}' + window.ab = {}; + window.user_id = '#{getLoggedInUserId()}'; + + script. + (function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true; s.src=('https:'==document.location.protocol?'https':'http') + '://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket.js'; var q = document.getElementsByTagName('script')[0];q.parentNode.insertBefore(s, q);})(); + - if (typeof(settings.algolia) != "undefined") script. @@ -118,21 +124,20 @@ html(itemscope, itemtype='http://schema.org/Product') "paths" : { "moment": "libs/moment-2.7.0" } - }; + }; script( data-main=jsPath+'main.js', baseurl=jsPath, src=jsPath+'libs/require.js?fingerprint='+fingerprint(jsPath + 'libs/require.js') ) - - if (typeof(tenderUrl) != "undefined") - script(src="https://#{tenderUrl}/tender_widget.js" ) - script(type="text/javascript"). - Tender = { - hideToggle: true, - widgetToggles: $(".js-tender-widget"), - category: "questions" - }; + + script. + var toggleGroove = function(){ + var e = arguments[0]||window.event; + e.stopPropagation(); + GrooveWidget.toggle() + } diff --git a/services/web/public/coffee/main/user-details.coffee b/services/web/public/coffee/main/user-details.coffee index e79ccfbbe4..f9e159e9d0 100644 --- a/services/web/public/coffee/main/user-details.coffee +++ b/services/web/public/coffee/main/user-details.coffee @@ -16,6 +16,7 @@ define [ _csrf : window.csrfToken $scope.showForm = -> + GrooveWidget.toggle() $scope.formVisable = true $scope.getPercentComplete = -> From 3e03164ed4e6f29f669d74fe0cab7f19e2359dca Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 10 Mar 2016 17:15:14 +0000 Subject: [PATCH 023/208] Remove dead auth_token code --- .../AuthenticationController.coffee | 35 +----- .../AuthenticationManager.coffee | 16 --- .../CollaboratorsController.coffee | 32 ----- .../Collaborators/CollaboratorsRouter.coffee | 1 - .../Features/User/UserInfoController.coffee | 4 - services/web/app/coffee/router.coffee | 3 +- .../AuthenticationControllerTests.coffee | 114 ++++-------------- .../AuthenticationManagerTests.coffee | 46 ------- .../CollaboratorsControllerTests.coffee | 46 ------- 9 files changed, 30 insertions(+), 267 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 312b441158..e7db5d9f65 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -44,50 +44,27 @@ module.exports = AuthenticationController = text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error' - getAuthToken: (req, res, next = (error) ->) -> - AuthenticationController.getLoggedInUserId req, (error, user_id) -> - return next(error) if error? - AuthenticationManager.getAuthToken user_id, (error, auth_token) -> - return next(error) if error? - res.send(auth_token) - getLoggedInUserId: (req, callback = (error, user_id) ->) -> if req?.session?.user?._id? callback null, req.session.user._id.toString() else callback null, null - getLoggedInUser: (req, options = {allow_auth_token: false}, callback = (error, user) ->) -> - if typeof(options) == "function" - callback = options - options = {allow_auth_token: false} - + getLoggedInUser: (req, callback = (error, user) ->) -> if req.session?.user?._id? query = req.session.user._id - else if req.query?.auth_token? and options.allow_auth_token - query = { auth_token: req.query.auth_token } else return callback null, null UserGetter.getUser query, callback - requireLogin: (options = {allow_auth_token: false, load_from_db: false}) -> + requireLogin: () -> doRequest = (req, res, next = (error) ->) -> - load_from_db = options.load_from_db - if req.query?.auth_token? and options.allow_auth_token - load_from_db = true - if load_from_db - AuthenticationController.getLoggedInUser req, { allow_auth_token: options.allow_auth_token }, (error, user) -> - return next(error) if error? - return AuthenticationController._redirectToLoginOrRegisterPage(req, res) if !user? - req.user = user - return next() + if !req.session.user? + AuthenticationController._redirectToLoginOrRegisterPage(req, res) else - if !req.session.user? - AuthenticationController._redirectToLoginOrRegisterPage(req, res) - else - req.user = req.session.user - return next() + req.user = req.session.user + return next() return doRequest diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee index 254c5c6c41..d2422be90b 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee @@ -36,19 +36,3 @@ module.exports = AuthenticationManager = $unset: password: true }, callback) - getAuthToken: (user_id, callback = (error, auth_token) ->) -> - db.users.findOne { _id: ObjectId(user_id.toString()) }, { auth_token : true }, (error, user) => - return callback(error) if error? - return callback(new Error("user could not be found: #{user_id}")) if !user? - if user.auth_token? - callback null, user.auth_token - else - @_createSecureToken (error, auth_token) -> - db.users.update { _id: ObjectId(user_id.toString()) }, { $set : auth_token: auth_token }, (error) -> - return callback(error) if error? - callback null, auth_token - - _createSecureToken: (callback = (error, token) ->) -> - crypto.randomBytes 48, (error, buffer) -> - return callback(error) if error? - callback null, buffer.toString("hex") diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee index 82912e1b6b..1905d0d0a6 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -7,14 +7,6 @@ UserGetter = require "../User/UserGetter" mimelib = require("mimelib") module.exports = CollaboratorsController = - getCollaborators: (req, res, next = (error) ->) -> - project_id = req.params.Project_id - CollaboratorsHandler.getMembersWithPrivilegeLevels project_id, (error, members) -> - return next(error) if error? - CollaboratorsController._formatCollaborators members, (error, collaborators) -> - return next(error) if error? - res.json(collaborators) - addUserToProject: (req, res, next) -> project_id = req.params.Project_id LimitationsManager.canAddXCollaborators project_id, 1, (error, allowed) => @@ -58,27 +50,3 @@ module.exports = CollaboratorsController = EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id) callback() - _formatCollaborators: (members, callback = (error, collaborators) ->) -> - collaborators = [] - - pushCollaborator = (user, permissions, owner) -> - collaborators.push { - id: user._id.toString() - first_name: user.first_name - last_name: user.last_name - email: user.email - permissions: permissions - owner: owner - } - - for member in members - {user, privilegeLevel} = member - if privilegeLevel == "admin" - pushCollaborator(user, ["read", "write", "admin"], true) - else if privilegeLevel == "readAndWrite" - pushCollaborator(user, ["read", "write"], false) - else - pushCollaborator(user, ["read"], false) - - callback null, collaborators - diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index d5abf23350..f4f1b0343c 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -5,7 +5,6 @@ AuthenticationController = require('../Authentication/AuthenticationController') module.exports = apply: (webRouter, apiRouter) -> webRouter.post '/project/:Project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject - apiRouter.get '/project/:Project_id/collaborators', SecurityManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators webRouter.post '/project/:Project_id/users', SecurityManager.requestIsOwner, CollaboratorsController.addUserToProject webRouter.delete '/project/:Project_id/users/:user_id', SecurityManager.requestIsOwner, CollaboratorsController.removeUserFromProject diff --git a/services/web/app/coffee/Features/User/UserInfoController.coffee b/services/web/app/coffee/Features/User/UserInfoController.coffee index ac7556bc90..edaf836c80 100644 --- a/services/web/app/coffee/Features/User/UserInfoController.coffee +++ b/services/web/app/coffee/Features/User/UserInfoController.coffee @@ -6,10 +6,6 @@ sanitize = require('sanitizer') module.exports = UserController = getLoggedInUsersPersonalInfo: (req, res, next = (error) ->) -> - # this is funcky as hell, we don't use the current session to get the user - # we use the auth token, actually destroying session from the chat api request - if req.query?.auth_token? - req.session?.destroy() logger.log user: req.user, "reciving request for getting logged in users personal info" return next(new Error("User is not logged in")) if !req.user? UserGetter.getUser req.user._id, { diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index eafe41470b..36b1bd7ea2 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -88,8 +88,7 @@ module.exports = class Router webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser - webRouter.get '/user/auth_token', AuthenticationController.requireLogin(), AuthenticationController.getAuthToken - webRouter.get '/user/personal_info', AuthenticationController.requireLogin(allow_auth_token: true), UserInfoController.getLoggedInUsersPersonalInfo + webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo apiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee index 63ff12a374..f83b38617b 100644 --- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee @@ -173,7 +173,7 @@ describe "AuthenticationController", -> beforeEach -> @req.session = user: @user - @AuthenticationController.getLoggedInUser(@req, {}, @callback) + @AuthenticationController.getLoggedInUser(@req, @callback) it "should look up the user in the database", -> @UserGetter.getUser @@ -183,105 +183,37 @@ describe "AuthenticationController", -> it "should return the user", -> @callback.calledWith(null, @user).should.equal true - describe "with an auth token, but without auth_token_allowed set to true", -> - beforeEach -> - @req.query = - auth_token: "auth-token" - @AuthenticationController.getLoggedInUser(@req, {}, @callback) - - it "should not look up the user in the database", -> - @UserGetter.getUser.called.should.equal false - - it "should return null in the callback", -> - @callback.calledWith(null, null).should.equal true - - describe "with an auth token and auth_token_allowed set to true", -> - beforeEach -> - @req.query = - auth_token: "auth-token" - @AuthenticationController.getLoggedInUser(@req, {allow_auth_token: true}, @callback) - - it "should look up the user in the database", -> - @UserGetter.getUser - .calledWith(auth_token: @req.query.auth_token) - .should.equal true - - it "should return the user", -> - @callback.calledWith(null, @user).should.equal true - describe "requireLogin", -> beforeEach -> @user = _id: "user-id-123" email: "user@sharelatex.com" + @middleware = @AuthenticationController.requireLogin() - describe "when loading from the database", -> + describe "when the user is logged in", -> beforeEach -> - @middleware = @AuthenticationController.requireLogin(@options = { allow_auth_token: true, load_from_db: true }) - - describe "when the user is logged in", -> - beforeEach -> - @AuthenticationController.getLoggedInUser = sinon.stub().callsArgWith(2, null, @user) - @middleware(@req, @res, @next) - - it "should call getLoggedInUser with the passed options", -> - @AuthenticationController.getLoggedInUser.calledWith(@req, { allow_auth_token: true }).should.equal true - - it "should set the user property on the request", -> - @req.user.should.deep.equal @user - - it "should call the next method in the chain", -> - @next.called.should.equal true - - describe "when the user is not logged in", -> - beforeEach -> - @AuthenticationController._redirectToLoginOrRegisterPage = sinon.stub() - @AuthenticationController.getLoggedInUser = sinon.stub().callsArgWith(2, null, null) - @middleware(@req, @res, @next) - - it "should redirect to the register page", -> - @AuthenticationController._redirectToLoginOrRegisterPage.calledWith(@req, @res).should.equal true - - describe "when not loading from the database", -> - beforeEach -> - @middleware = @AuthenticationController.requireLogin(@options = { load_from_db: false }) - - describe "when the user is logged in", -> - beforeEach -> - @req.session = - user: @user = { - _id: "user-id-123" - email: "user@sharelatex.com" - } - @middleware(@req, @res, @next) - - it "should set the user property on the request", -> - @req.user.should.deep.equal @user - - it "should call the next method in the chain", -> - @next.called.should.equal true - - describe "when the user is not logged in", -> - beforeEach -> - @req.session = {} - @AuthenticationController._redirectToLoginOrRegisterPage = sinon.stub() - @req.query = {} - @middleware(@req, @res, @next) - - it "should redirect to the register or login page", -> - @AuthenticationController._redirectToLoginOrRegisterPage.calledWith(@req, @res).should.equal true - - describe "when not loading from the database but an auth_token is provided", -> - beforeEach -> - @AuthenticationController.getLoggedInUser = sinon.stub().callsArgWith(2, null, @user) - @middleware = @AuthenticationController.requireLogin(@options = { load_from_db: false, allow_auth_token: true }) - @req.query = auth_token: @auth_token = "auth-token-provided" + @req.session = + user: @user = { + _id: "user-id-123" + email: "user@sharelatex.com" + } @middleware(@req, @res, @next) - it "should try to load the user from the database anyway", -> - @AuthenticationController.getLoggedInUser - .calledWith(@req, {allow_auth_token: true}) - .should.equal true + it "should set the user property on the request", -> + @req.user.should.deep.equal @user + + it "should call the next method in the chain", -> + @next.called.should.equal true + + describe "when the user is not logged in", -> + beforeEach -> + @req.session = {} + @AuthenticationController._redirectToLoginOrRegisterPage = sinon.stub() + @req.query = {} + @middleware(@req, @res, @next) + + it "should redirect to the register or login page", -> + @AuthenticationController._redirectToLoginOrRegisterPage.calledWith(@req, @res).should.equal true describe "requireGlobalLogin", -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee index 3a5c60cf66..f4d5e72c26 100644 --- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee @@ -95,49 +95,3 @@ describe "AuthenticationManager", -> it "should call the callback", -> @callback.called.should.equal true - - describe "getAuthToken", -> - beforeEach -> - @auth_token = "auth-token" - - describe "when the user has an auth token set", -> - beforeEach -> - @db.users.findOne = sinon.stub().callsArgWith(2, null, auth_token: @auth_token) - @AuthenticationManager.getAuthToken(@user_id, @callback) - - it "should look up the auth token in the db", -> - @db.users.findOne - .calledWith({ - _id: ObjectId(@user_id.toString()) - }, { - auth_token: true - }) - .should.equal true - - it "should return the auth token", -> - @callback.calledWith(null, @auth_token).should.equal true - - describe "when the user does not have an auth token set", -> - beforeEach -> - @db.users.findOne = sinon.stub().callsArgWith(2, null, auth_token: null) - @db.users.update = sinon.stub().callsArgWith(2, null) - @AuthenticationManager._createSecureToken = sinon.stub().callsArgWith(0, null, @auth_token) - @AuthenticationManager.getAuthToken(@user_id, @callback) - - it "should generate a new auth token", -> - @AuthenticationManager._createSecureToken.called.should.equal true - - it "should set the auth token on the user document in the db", -> - @db.users.update - .calledWith({ - _id: ObjectId(@user_id.toString()) - }, { - $set: auth_token: @auth_token - }) - .should.equal true - - it "should return the auth token", -> - @callback.calledWith(null, @auth_token).should.equal true - - - diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee index 84b0a250d3..32de9ebe0a 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee @@ -24,52 +24,6 @@ describe "CollaboratorsController", -> @project_id = "project-id-123" @callback = sinon.stub() - describe "getCollaborators", -> - beforeEach -> - @req.params = - Project_id: @project_id - @members = [ - { - user: { _id: "admin-id", email: "admin@example.com", first_name: "Joe", last_name: "Admin", foo: "bar" } - privilegeLevel: "admin" - }, - { - user: { _id: "rw-id", email: "rw@example.com", first_name: "Jane", last_name: "Write", foo: "bar" } - privilegeLevel: "readAndWrite" - }, - { - user: { _id: "ro-id", email: "ro@example.com", first_name: "Joe", last_name: "Read", foo: "bar" } - privilegeLevel: "readOnly" - } - ] - @CollaboratorsHandler.getMembersWithPrivilegeLevels = sinon.stub() - @CollaboratorsHandler.getMembersWithPrivilegeLevels - .withArgs(@project_id) - .yields(null, @members) - @res.json = sinon.stub() - @CollaboratorsController.getCollaborators(@req, @res) - - it "should return the formatted collaborators", -> - @res.json - .calledWith([ - { - id: "admin-id", email: "admin@example.com", first_name: "Joe", last_name: "Admin" - permissions: ["read", "write", "admin"] - owner: true - } - { - id: "rw-id", email: "rw@example.com", first_name: "Jane", last_name: "Write" - permissions: ["read", "write"] - owner: false - } - { - id: "ro-id", email: "ro@example.com", first_name: "Joe", last_name: "Read" - permissions: ["read"] - owner: false - } - ]) - .should.equal true - describe "addUserToProject", -> beforeEach -> @req.params = From 1bd8b8d1a3427f55019054d08785feed8a432da6 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 10 Mar 2016 17:17:26 +0000 Subject: [PATCH 024/208] Delete SecurityManager and replace with (unwritten) AuthorizationManager --- .../Authorization/AuthorizationManager.coffee | 12 ++ .../AuthorizationMiddlewear.coffee | 27 +++ .../Collaborators/CollaboratorsRouter.coffee | 6 +- .../Editor/EditorHttpController.coffee | 4 +- .../Features/Editor/EditorRouter.coffee | 18 +- .../Features/Project/ProjectController.coffee | 5 +- .../Security/AuthorizationManager.coffee | 38 ---- .../SubscriptionController.coffee | 26 +-- .../Features/Uploads/UploadsRouter.coffee | 4 +- .../coffee/managers/SecurityManager.coffee | 194 ------------------ services/web/app/coffee/router.coffee | 77 ++++--- .../Editor/EditorHttpControllerTests.coffee | 4 +- .../Project/ProjectControllerTests.coffee | 10 +- .../Security/AuthorizationManagerTests.coffee | 96 --------- .../SubscriptionControllerTests.coffee | 6 +- 15 files changed, 119 insertions(+), 408 deletions(-) create mode 100644 services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee create mode 100644 services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee delete mode 100644 services/web/app/coffee/Features/Security/AuthorizationManager.coffee delete mode 100644 services/web/app/coffee/managers/SecurityManager.coffee delete mode 100644 services/web/test/UnitTests/coffee/Security/AuthorizationManagerTests.coffee diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee new file mode 100644 index 0000000000..a6a80edae0 --- /dev/null +++ b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee @@ -0,0 +1,12 @@ +module.exports = + getPrivilegeLevelForProject: (user_id, project_id, callback = (error, canAccess, privilegeLevel) ->) -> + return callback(null, true, "readAndWrite") + + canUserReadProject: (user_id, project_id, callback = (error, canRead) ->) -> + + canUserWriteProjectSettings: (user_id, project_id, callback = (error, canWriteSettings) ->) -> + + canUserAdminProject: (user_id, project_id, callback = (error, canAdmin) ->) -> + + isUserSiteAdmin: (user_id, callback = (error, isAdmin) ->) -> + \ No newline at end of file diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee new file mode 100644 index 0000000000..a3bce5a2cc --- /dev/null +++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee @@ -0,0 +1,27 @@ +module.exports = + ensureUserCanReadMultipleProjects: (req, res, next) -> + next() + + ensureUserCanReadProject: (req, res, next) -> + next() + + ensureUserCanWriteProjectSettings: (req, res, next) -> + next() + + ensureUserCanWriteProjectContent: (req, res, next) -> + next() + + ensureUserCanAdminProject: (req, res, next) -> + next() + + ensureUserIsSiteAdmin: (req, res, next) -> + next() + + restricted : (req, res, next)-> + if req.session.user? + res.render 'user/restricted', + title:'restricted' + else + logger.log "user not logged in and trying to access #{req.url}, being redirected to login" + res.redirect '/register' + \ No newline at end of file diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index f4f1b0343c..34a6da9a02 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -1,10 +1,10 @@ CollaboratorsController = require('./CollaboratorsController') -SecurityManager = require('../../managers/SecurityManager') AuthenticationController = require('../Authentication/AuthenticationController') +AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear') module.exports = apply: (webRouter, apiRouter) -> webRouter.post '/project/:Project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject - webRouter.post '/project/:Project_id/users', SecurityManager.requestIsOwner, CollaboratorsController.addUserToProject - webRouter.delete '/project/:Project_id/users/:user_id', SecurityManager.requestIsOwner, CollaboratorsController.removeUserFromProject + webRouter.post '/project/:Project_id/users', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.addUserToProject + webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 18e8f7d308..776a1c1fb0 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -5,7 +5,7 @@ EditorRealTimeController = require "./EditorRealTimeController" EditorController = require "./EditorController" ProjectGetter = require('../Project/ProjectGetter') UserGetter = require('../User/UserGetter') -AuthorizationManager = require("../Security/AuthorizationManager") +AuthorizationManager = require("../Authorization/AuthorizationManager") ProjectEditorHandler = require('../Project/ProjectEditorHandler') Metrics = require('../../infrastructure/Metrics') CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") @@ -34,7 +34,7 @@ module.exports = EditorHttpController = return callback(error) if error? UserGetter.getUser user_id, { isAdmin: true }, (error, user) -> return callback(error) if error? - AuthorizationManager.getPrivilegeLevelForProject project, user, (error, canAccess, privilegeLevel) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, canAccess, privilegeLevel) -> return callback(error) if error? if !canAccess callback null, null, false diff --git a/services/web/app/coffee/Features/Editor/EditorRouter.coffee b/services/web/app/coffee/Features/Editor/EditorRouter.coffee index 0576a4adce..9de1544875 100644 --- a/services/web/app/coffee/Features/Editor/EditorRouter.coffee +++ b/services/web/app/coffee/Features/Editor/EditorRouter.coffee @@ -1,20 +1,20 @@ EditorHttpController = require('./EditorHttpController') -SecurityManager = require('../../managers/SecurityManager') AuthenticationController = require "../Authentication/AuthenticationController" +AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear') module.exports = apply: (webRouter, apiRouter) -> - webRouter.post '/project/:Project_id/doc', SecurityManager.requestCanModifyProject, EditorHttpController.addDoc - webRouter.post '/project/:Project_id/folder', SecurityManager.requestCanModifyProject, EditorHttpController.addFolder + webRouter.post '/project/:Project_id/doc', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.addDoc + webRouter.post '/project/:Project_id/folder', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.addFolder - webRouter.post '/project/:Project_id/:entity_type/:entity_id/rename', SecurityManager.requestCanModifyProject, EditorHttpController.renameEntity - webRouter.post '/project/:Project_id/:entity_type/:entity_id/move', SecurityManager.requestCanModifyProject, EditorHttpController.moveEntity + webRouter.post '/project/:Project_id/:entity_type/:entity_id/rename', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.renameEntity + webRouter.post '/project/:Project_id/:entity_type/:entity_id/move', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.moveEntity - webRouter.delete '/project/:Project_id/file/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteFile - webRouter.delete '/project/:Project_id/doc/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteDoc - webRouter.delete '/project/:Project_id/folder/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteFolder + webRouter.delete '/project/:Project_id/file/:entity_id', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.deleteFile + webRouter.delete '/project/:Project_id/doc/:entity_id', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.deleteDoc + webRouter.delete '/project/:Project_id/folder/:entity_id', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.deleteFolder - webRouter.post '/project/:Project_id/doc/:doc_id/restore', SecurityManager.requestCanModifyProject, EditorHttpController.restoreDoc + webRouter.post '/project/:Project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.restoreDoc # Called by the real-time API to load up the current project state. # This is a post request because it's more than just a getting of data. We take actions diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 0ca14090c2..ba5ff559f0 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -13,7 +13,7 @@ NotificationsHandler = require("../Notifications/NotificationsHandler") LimitationsManager = require("../Subscription/LimitationsManager") _ = require("underscore") Settings = require("settings-sharelatex") -SecurityManager = require("../../managers/SecurityManager") +AuthorizationManager = require("../Authorization/AuthorizationManager") fs = require "fs" InactiveProjectManager = require("../InactiveData/InactiveProjectManager") ProjectUpdateHandler = require("./ProjectUpdateHandler") @@ -225,7 +225,8 @@ module.exports = ProjectController = daysSinceLastUpdated = (new Date() - project.lastUpdated) /86400000 logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor" - SecurityManager.userCanAccessProject user, project, (canAccess, privilegeLevel)-> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, canAccess, privilegeLevel)-> + return next(error) if error? if !canAccess return res.sendStatus 401 diff --git a/services/web/app/coffee/Features/Security/AuthorizationManager.coffee b/services/web/app/coffee/Features/Security/AuthorizationManager.coffee deleted file mode 100644 index 0ec4985f35..0000000000 --- a/services/web/app/coffee/Features/Security/AuthorizationManager.coffee +++ /dev/null @@ -1,38 +0,0 @@ -SecurityManager = require '../../managers/SecurityManager' - -module.exports = AuthorizationManager = - getPrivilegeLevelForProject: ( - project, user, - callback = (error, canAccess, privilegeLevel)-> - ) -> - # This is not tested because eventually this function should be brought into - # this module. - SecurityManager.userCanAccessProject user, project, (canAccess, privilegeLevel) -> - if canAccess - callback null, true, privilegeLevel - else - callback null, false - - setPrivilegeLevelOnClient: (client, privilegeLevel) -> - client.set("privilege_level", privilegeLevel) - - ensureClientCanViewProject: (client, callback = (error, project_id)->) -> - @ensureClientHasPrivilegeLevelForProject client, ["owner", "readAndWrite", "readOnly"], callback - - ensureClientCanEditProject: (client, callback = (error, project_id)->) -> - @ensureClientHasPrivilegeLevelForProject client, ["owner", "readAndWrite"], callback - - ensureClientCanAdminProject: (client, callback = (error, project_id)->) -> - @ensureClientHasPrivilegeLevelForProject client, ["owner"], callback - - ensureClientHasPrivilegeLevelForProject: (client, levels, callback = (error, project_id)->) -> - client.get "privilege_level", (error, level) -> - return callback(error) if error? - if level? - client.get "project_id", (error, project_id) -> - return callback(error) if error? - if project_id? - if levels.indexOf(level) > -1 - callback null, project_id - - diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index a2287a5d44..ba3d7b2115 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -1,4 +1,4 @@ -SecurityManager = require '../../managers/SecurityManager' +AuthenticationController = require '../Authentication/AuthenticationController' SubscriptionHandler = require './SubscriptionHandler' PlansLocator = require("./PlansLocator") SubscriptionFormatters = require("./SubscriptionFormatters") @@ -32,7 +32,7 @@ module.exports = SubscriptionController = #get to show the recurly.js page paymentPage: (req, res, next) -> - SecurityManager.getCurrentUser req, (error, user) => + AuthenticationController.getLoggedInUser req, (error, user) => return next(error) if error? plan = PlansLocator.findLocalPlanInSettings(req.query.planCode) LimitationsManager.userHasSubscription user, (err, hasSubscription)-> @@ -81,7 +81,7 @@ module.exports = SubscriptionController = userSubscriptionPage: (req, res, next) -> - SecurityManager.getCurrentUser req, (error, user) => + AuthenticationController.getLoggedInUser req, (error, user) => return next(error) if error? LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)-> groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user) @@ -110,7 +110,7 @@ module.exports = SubscriptionController = userCustomSubscriptionPage: (req, res, next)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)-> res.render "subscriptions/custom_account", title: "your_subscription" @@ -118,7 +118,7 @@ module.exports = SubscriptionController = editBillingDetailsPage: (req, res, next) -> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> return next(error) if error? LimitationsManager.userHasSubscription user, (err, hasSubscription)-> if !hasSubscription @@ -139,7 +139,7 @@ module.exports = SubscriptionController = id : user.id createSubscription: (req, res, next)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> return callback(error) if error? recurly_token_id = req.body.recurly_token_id subscriptionDetails = req.body.subscriptionDetails @@ -151,14 +151,14 @@ module.exports = SubscriptionController = res.sendStatus 201 successful_subscription: (req, res)-> - SecurityManager.getCurrentUser req, (error, user) => + AuthenticationController.getLoggedInUser req, (error, user) => SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) -> res.render "subscriptions/successful_subscription", title: "thank_you" subscription:subscription cancelSubscription: (req, res, next) -> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> logger.log user_id:user._id, "canceling subscription" return next(error) if error? SubscriptionHandler.cancelSubscription user, (err)-> @@ -167,7 +167,7 @@ module.exports = SubscriptionController = res.redirect "/user/subscription" updateSubscription: (req, res)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> return next(error) if error? planCode = req.body.plan_code logger.log planCode: planCode, user_id:user._id, "updating subscription" @@ -177,7 +177,7 @@ module.exports = SubscriptionController = res.redirect "/user/subscription" reactivateSubscription: (req, res)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> logger.log user_id:user._id, "reactivating subscription" return next(error) if error? SubscriptionHandler.reactivateSubscription user, (err)-> @@ -196,7 +196,7 @@ module.exports = SubscriptionController = res.sendStatus 200 renderUpgradeToAnnualPlanPage: (req, res)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)-> planCode = subscription?.planCode.toLowerCase() if planCode?.indexOf("annual") != -1 @@ -213,7 +213,7 @@ module.exports = SubscriptionController = planName: planName processUpgradeToAnnualPlan: (req, res)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> {planName} = req.body coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName] annualPlanName = "#{planName}-annual" @@ -226,7 +226,7 @@ module.exports = SubscriptionController = res.sendStatus 200 extendTrial: (req, res)-> - SecurityManager.getCurrentUser req, (error, user) -> + AuthenticationController.getLoggedInUser req, (error, user) -> LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)-> SubscriptionHandler.extendTrial subscription, 14, (err)-> if err? diff --git a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee index c82144acc0..fdff8d0ea3 100644 --- a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee +++ b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee @@ -1,4 +1,4 @@ -SecurityManager = require('../../managers/SecurityManager') +AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear') AuthenticationController = require('../Authentication/AuthenticationController') ProjectUploadController = require "./ProjectUploadController" RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear') @@ -16,6 +16,6 @@ module.exports = maxRequests: 200 timeInterval: 60 * 30 }), - SecurityManager.requestCanModifyProject, + AuthorizationMiddlewear.ensureUserCanWriteProjectContent, ProjectUploadController.uploadFile diff --git a/services/web/app/coffee/managers/SecurityManager.coffee b/services/web/app/coffee/managers/SecurityManager.coffee deleted file mode 100644 index 699c307679..0000000000 --- a/services/web/app/coffee/managers/SecurityManager.coffee +++ /dev/null @@ -1,194 +0,0 @@ -logger = require('logger-sharelatex') -crypto = require 'crypto' -Assert = require 'assert' -Settings = require 'settings-sharelatex' -User = require('../models/User').User -Project = require('../models/Project').Project -ErrorController = require("../Features/Errors/ErrorController") -AuthenticationController = require("../Features/Authentication/AuthenticationController") -_ = require('underscore') -metrics = require('../infrastructure/Metrics') -querystring = require('querystring') -async = require "async" - -module.exports = SecurityManager = - restricted : (req, res, next)-> - if req.session.user? - res.render 'user/restricted', - title:'restricted' - else - logger.log "user not logged in and trying to access #{req.url}, being redirected to login" - res.redirect '/register' - - getCurrentUser: (req, callback) -> - if req.session.user? - User.findById req.session.user._id, callback - else - callback null, null - - requestCanAccessMultipleProjects: (req, res, next) -> - project_ids = req.query.project_ids?.split(",") - jobs = [] - for project_id in project_ids or [] - do (project_id) -> - jobs.push (callback) -> - # This is a bit hacky - better to have an abstracted method - # that we can pass project_id to, but this whole file needs - # a serious refactor ATM. - req.params.Project_id = project_id - SecurityManager.requestCanAccessProject req, res, (error) -> - delete req.params.Project_id - callback(error) - async.series jobs, next - - requestCanAccessProject : (req, res, next)-> - doRequest = (req, res, next) -> - getRequestUserAndProject req, res, {allow_auth_token: options?.allow_auth_token}, (err, user, project)-> - if !project? or project.archived - return ErrorController.notFound(req, res, next) - userCanAccessProject user, project, (canAccess, permissionLevel)-> - if canAccess - next() - else if user? - logger.log "user_id: #{user._id} email: #{user.email} trying to access restricted page #{req.path}" - res.redirect('/restricted') - else - logger.log "user not logged in and trying to access #{req.url}, being redirected to login" - AuthenticationController._redirectToLoginOrRegisterPage(req, res) - if arguments.length > 1 - options = - allow_auth_token: false - doRequest.apply(this, arguments) - else - options = req - return doRequest - - requestCanModifyProject : (req, res, next)-> - getRequestUserAndProject req, res, {}, (err, user, project)=> - userCanModifyProject user, project, (canModify)-> - if canModify - next() - else - logger.log "user_id: #{user?._id} email: #{user?.email} can not modify project redirecting to restricted page" - res.redirect('/restricted') - - userCanModifyProject : userCanModifyProject = (user, project, callback)-> - if !user? or !project? - callback false - else if userIsOwner user, project - callback true - else if userIsCollaberator user, project - callback true - else if project.publicAccesLevel == "readAndWrite" - callback true - else if user.isAdmin - callback true - else - callback false - - - requestIsOwner : (req, res, next)-> - getRequestUserAndProject req, res, {}, (err, user, project)-> - if !user? - return res.redirect('/restricted') - else if userIsOwner user, project || user.isAdmin - next() - else - logger.log user_id: user?._id, email: user?.email, "user is not owner of project redirecting to restricted page" - res.redirect('/restricted') - - requestIsAdmin : isAdmin = (req, res, next)-> - logger.log "checking if user is admin" - user = req.session.user - if(user? && user.isAdmin) - logger.log user: user, "User is admin" - next() - else - res.redirect('/restricted') - logger.log user:user, "is not admin redirecting to restricted page" - - userCanAccessProject : userCanAccessProject = (user, project, callback)=> - if !user? - user = {_id:'anonymous-user'} - if !project? - callback false - logger.log user:user, project:project, "Checking if can access" - if userIsOwner user, project - callback true, "owner" - else if userIsCollaberator user, project - callback true, "readAndWrite" - else if userIsReadOnly user, project - callback true, "readOnly" - else if user.isAdmin - logger.log user:user, project:project, "user is admin and can access project" - callback true, "owner" - else if project.publicAccesLevel == "readAndWrite" - logger.log user:user, project:project, "project is a public read and write project" - callback true, "readAndWrite" - else if project.publicAccesLevel == "readOnly" - logger.log user:user, project:project, "project is a public read only project" - callback true, "readOnly" - else - metrics.inc "security.denied" - logger.log user:user, project:project, "Security denied - user can not enter project" - callback false - - userIsOwner : userIsOwner = (user, project)-> - if !user? - return false - else - userId = user._id+'' - ownerRef = getProjectIdFromRef(project.owner_ref) - if userId == ownerRef - true - else - false - - userIsCollaberator : userIsCollaberator = (user, project)-> - if !user? - return false - else - userId = user._id+'' - result = false - _.each project.collaberator_refs, (colabRef)-> - colabRef = getProjectIdFromRef(colabRef) - if colabRef == userId - result = true - return result - - userIsReadOnly : userIsReadOnly = (user, project)-> - if !user? - return false - else - userId = user._id+'' - result = false - _.each project.readOnly_refs, (readOnlyRef)-> - readOnlyRef = getProjectIdFromRef(readOnlyRef) - - if readOnlyRef == userId - result = true - return result - -getRequestUserAndProject = (req, res, options, callback)-> - project_id = req.params.Project_id - if !project_id? - logger.log project_id:project_id, options:options, url:req?.url, "no project_id trying to getRequestUserAndProject" - return res.send 422 - Project.findById project_id, 'name owner_ref readOnly_refs collaberator_refs publicAccesLevel archived', (err, project)=> - if err? - logger.err err:err, "error getting project for security check" - return callback err - AuthenticationController.getLoggedInUser req, options, (err, user)=> - if err? - logger.err err:err, "error getting last logged in user for security check" - callback err, user, project - -getProjectIdFromRef = (ref)-> - if !ref? - return null - else if ref._id? - return ref._id+'' - else - return ref+'' - - diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 36b1bd7ea2..305b3bd564 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -3,8 +3,6 @@ ErrorController = require('./Features/Errors/ErrorController') ProjectController = require("./Features/Project/ProjectController") ProjectApiController = require("./Features/Project/ProjectApiController") SpellingController = require('./Features/Spelling/SpellingController') -SecurityManager = require('./managers/SecurityManager') -AuthorizationManager = require('./Features/Security/AuthorizationManager') EditorController = require("./Features/Editor/EditorController") EditorRouter = require("./Features/Editor/EditorRouter") Settings = require('settings-sharelatex') @@ -39,6 +37,7 @@ RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter') InactiveProjectController = require("./Features/InactiveData/InactiveProjectController") ContactRouter = require("./Features/Contacts/ContactRouter") ReferencesController = require('./Features/References/ReferencesController') +AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear') logger = require("logger-sharelatex") _ = require("underscore") @@ -54,7 +53,7 @@ module.exports = class Router webRouter.post '/login', AuthenticationController.login webRouter.get '/logout', UserController.logout - webRouter.get '/restricted', SecurityManager.restricted + webRouter.get '/restricted', AuthorizationMiddlewear.restricted # Left as a placeholder for implementing a public register page webRouter.get '/register', UserPagesController.registerPage @@ -99,13 +98,13 @@ module.exports = class Router params: ["Project_id"] maxRequests: 10 timeInterval: 60 - }), SecurityManager.requestCanAccessProject, ProjectController.loadEditor - webRouter.get '/Project/:Project_id/file/:File_id', SecurityManager.requestCanAccessProject, FileStoreController.getFile - webRouter.post '/project/:Project_id/settings', SecurityManager.requestCanModifyProject, ProjectController.updateProjectSettings - webRouter.post '/project/:Project_id/settings/admin', SecurityManager.requestIsOwner, ProjectController.updateProjectAdminSettings + }), AuthorizationMiddlewear.ensureUserCanReadProject, ProjectController.loadEditor + webRouter.get '/Project/:Project_id/file/:File_id', AuthorizationMiddlewear.ensureUserCanReadProject, FileStoreController.getFile + webRouter.post '/project/:Project_id/settings', AuthorizationMiddlewear.ensureUserCanWriteProjectSettings, ProjectController.updateProjectSettings + webRouter.post '/project/:Project_id/settings/admin', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.updateProjectAdminSettings - webRouter.post '/project/:Project_id/compile', SecurityManager.requestCanAccessProject, CompileController.compile - webRouter.get '/Project/:Project_id/output/output.pdf', SecurityManager.requestCanAccessProject, CompileController.downloadPdf + webRouter.post '/project/:Project_id/compile', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.compile + webRouter.get '/Project/:Project_id/output/output.pdf', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.downloadPdf webRouter.get /^\/project\/([^\/]*)\/output\/(.*)$/, ((req, res, next) -> params = @@ -113,24 +112,24 @@ module.exports = class Router "file": req.params[1] req.params = params next() - ), SecurityManager.requestCanAccessProject, CompileController.getFileFromClsi - webRouter.delete "/project/:Project_id/output", SecurityManager.requestCanAccessProject, CompileController.deleteAuxFiles - webRouter.get "/project/:Project_id/sync/code", SecurityManager.requestCanAccessProject, CompileController.proxySync - webRouter.get "/project/:Project_id/sync/pdf", SecurityManager.requestCanAccessProject, CompileController.proxySync - webRouter.get "/project/:Project_id/wordcount", SecurityManager.requestCanAccessProject, CompileController.wordCount + ), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi + webRouter.delete "/project/:Project_id/output", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.deleteAuxFiles + webRouter.get "/project/:Project_id/sync/code", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySync + webRouter.get "/project/:Project_id/sync/pdf", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySync + webRouter.get "/project/:Project_id/wordcount", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.wordCount - webRouter.delete '/Project/:Project_id', SecurityManager.requestIsOwner, ProjectController.deleteProject - webRouter.post '/Project/:Project_id/restore', SecurityManager.requestIsOwner, ProjectController.restoreProject - webRouter.post '/Project/:Project_id/clone', SecurityManager.requestCanAccessProject, ProjectController.cloneProject + webRouter.delete '/Project/:Project_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.deleteProject + webRouter.post '/Project/:Project_id/restore', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.restoreProject + webRouter.post '/Project/:Project_id/clone', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectController.cloneProject - webRouter.post '/project/:Project_id/rename', SecurityManager.requestIsOwner, ProjectController.renameProject + webRouter.post '/project/:Project_id/rename', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.renameProject - webRouter.get "/project/:Project_id/updates", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi - webRouter.get "/project/:Project_id/doc/:doc_id/diff", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi - webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi + webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi + webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi + webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi - webRouter.get '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject - webRouter.get '/project/download/zip', SecurityManager.requestCanAccessMultipleProjects, ProjectDownloadsController.downloadMultipleProjects + webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject + webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects webRouter.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags webRouter.post '/tag', AuthenticationController.requireLogin(), TagsController.createTag @@ -174,26 +173,26 @@ module.exports = class Router webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi - webRouter.get "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.getMessages - webRouter.post "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.sendMessage + webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages + webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage webRouter.get /learn(\/.*)?/, WikiController.getPage - webRouter.post "/project/:Project_id/references/index", SecurityManager.requestCanAccessProject, ReferencesController.index - webRouter.post "/project/:Project_id/references/indexAll", SecurityManager.requestCanAccessProject, ReferencesController.indexAll + webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index + webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll #Admin Stuff - webRouter.get '/admin', SecurityManager.requestIsAdmin, AdminController.index - webRouter.get '/admin/user', SecurityManager.requestIsAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon - webRouter.get '/admin/register', SecurityManager.requestIsAdmin, AdminController.registerNewUser - webRouter.post '/admin/register', SecurityManager.requestIsAdmin, UserController.register - webRouter.post '/admin/closeEditor', SecurityManager.requestIsAdmin, AdminController.closeEditor - webRouter.post '/admin/dissconectAllUsers', SecurityManager.requestIsAdmin, AdminController.dissconectAllUsers - webRouter.post '/admin/syncUserToSubscription', SecurityManager.requestIsAdmin, AdminController.syncUserToSubscription - webRouter.post '/admin/flushProjectToTpds', SecurityManager.requestIsAdmin, AdminController.flushProjectToTpds - webRouter.post '/admin/pollDropboxForUser', SecurityManager.requestIsAdmin, AdminController.pollDropboxForUser - webRouter.post '/admin/messages', SecurityManager.requestIsAdmin, AdminController.createMessage - webRouter.post '/admin/messages/clear', SecurityManager.requestIsAdmin, AdminController.clearMessages + webRouter.get '/admin', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.index + webRouter.get '/admin/user', AuthorizationMiddlewear.ensureUserIsSiteAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon + webRouter.get '/admin/register', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.registerNewUser + webRouter.post '/admin/register', AuthorizationMiddlewear.ensureUserIsSiteAdmin, UserController.register + webRouter.post '/admin/closeEditor', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.closeEditor + webRouter.post '/admin/dissconectAllUsers', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.dissconectAllUsers + webRouter.post '/admin/syncUserToSubscription', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.syncUserToSubscription + webRouter.post '/admin/flushProjectToTpds', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.flushProjectToTpds + webRouter.post '/admin/pollDropboxForUser', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.pollDropboxForUser + webRouter.post '/admin/messages', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.createMessage + webRouter.post '/admin/messages/clear', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.clearMessages apiRouter.get '/perfTest', (req,res)-> res.send("hello") @@ -205,7 +204,7 @@ module.exports = class Router webRouter.get '/health_check', HealthCheckController.check webRouter.get '/health_check/redis', HealthCheckController.checkRedis - apiRouter.get "/status/compiler/:Project_id", SecurityManager.requestCanAccessProject, (req, res) -> + apiRouter.get "/status/compiler/:Project_id", AuthorizationMiddlewear.ensureUserCanReadProject, (req, res) -> sendRes = _.once (statusCode, message)-> res.writeHead statusCode res.end message diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee index 21c8a195e4..1160c81c37 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee @@ -10,7 +10,7 @@ describe "EditorHttpController", -> '../Project/ProjectDeleter' : @ProjectDeleter = {} '../Project/ProjectGetter' : @ProjectGetter = {} '../User/UserGetter' : @UserGetter = {} - "../Security/AuthorizationManager": @AuthorizationManager = {} + "../Authorization/AuthorizationManager": @AuthorizationManager = {} '../Project/ProjectEditorHandler': @ProjectEditorHandler = {} "./EditorRealTimeController": @EditorRealTimeController = {} "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } @@ -119,7 +119,7 @@ describe "EditorHttpController", -> it "should check the privilege level", -> @AuthorizationManager.getPrivilegeLevelForProject - .calledWith(@project, @user) + .calledWith(@user_id, @project_id) .should.equal true it "should return the project model view, privilege level and protocol version", -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 0be616da51..1dbdc8539e 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -39,8 +39,8 @@ describe "ProjectController", -> findOne: sinon.stub() @UserModel = findById: sinon.stub() - @SecurityManager = - userCanAccessProject:sinon.stub() + @AuthorizationManager = + getPrivilegeLevelForProject:sinon.stub() @EditorController = renameProject:sinon.stub() @InactiveProjectManager = @@ -66,7 +66,7 @@ describe "ProjectController", -> "../Notifications/NotificationsHandler":@NotificationsHandler '../../models/Project': Project:@ProjectModel "../../models/User":User:@UserModel - "../../managers/SecurityManager":@SecurityManager + "../Authorization/AuthorizationManager":@AuthorizationManager "../InactiveData/InactiveProjectManager":@InactiveProjectManager "./ProjectUpdateHandler":@ProjectUpdateHandler "../ReferencesSearch/ReferencesSearchHandler": @ReferencesSearchHandler @@ -299,7 +299,7 @@ describe "ProjectController", -> @ProjectModel.findOne.callsArgWith 1, null, @project @UserModel.findById.callsArgWith(1, null, @user) @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) - @SecurityManager.userCanAccessProject.callsArgWith 2, true, "owner" + @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 2, null, true, "owner" @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() @InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1) @ProjectUpdateHandler.markAsOpened.callsArgWith(1) @@ -332,7 +332,7 @@ describe "ProjectController", -> @ProjectController.loadEditor @req, @res it "should not render the page if the project can not be accessed", (done)-> - @SecurityManager.userCanAccessProject = sinon.stub().callsArgWith 2, false + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub().callsArgWith 2, null, false @res.sendStatus = (resCode, opts)=> resCode.should.equal 401 done() diff --git a/services/web/test/UnitTests/coffee/Security/AuthorizationManagerTests.coffee b/services/web/test/UnitTests/coffee/Security/AuthorizationManagerTests.coffee deleted file mode 100644 index 5be9c5ea20..0000000000 --- a/services/web/test/UnitTests/coffee/Security/AuthorizationManagerTests.coffee +++ /dev/null @@ -1,96 +0,0 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../../app/js/Features/Security/AuthorizationManager' -MockClient = require "../helpers/MockClient" - -describe "AuthorizationManager", -> - beforeEach -> - @client = new MockClient() - @AuthorizationManager = SandboxedModule.require modulePath, requires: - '../../managers/SecurityManager':{} - - describe "ensureClientCanViewProject", -> - beforeEach -> - @client.set("project_id", "project-id") - - it "should let the request through for a readOnly privilege", (done) -> - @client.set("privilege_level", "readOnly") - @AuthorizationManager.ensureClientCanViewProject @client, done - - it "should let the request through for a readAndWrite privilege", (done) -> - @client.set("privilege_level", "readAndWrite") - @AuthorizationManager.ensureClientCanViewProject @client, done - - it "should let the request through for a owner privilege", (done) -> - @client.set("privilege_level", "owner") - @AuthorizationManager.ensureClientCanViewProject @client, done - - it "should ignore an empty privilege", -> - @AuthorizationManager.ensureClientCanViewProject @client, () -> - throw new Error("Should not be called") - - describe "ensureClientCanEditProject", -> - beforeEach -> - @client.set("project_id", "project-id") - - it "should ignore a readOnly privilege", -> - @client.set("privilege_level", "readOnly") - @AuthorizationManager.ensureClientCanEditProject @client, () -> - throw new Error("Should not be called") - - it "should let the request through for a readAndWrite privilege", (done) -> - @client.set("privilege_level", "readAndWrite") - @AuthorizationManager.ensureClientCanEditProject @client, done - - it "should let the request through for a owner privilege", (done) -> - @client.set("privilege_level", "owner") - @AuthorizationManager.ensureClientCanEditProject @client, done - - it "should ignore an empty privilege", -> - @AuthorizationManager.ensureClientCanEditProject @client, () -> - throw new Error("Should not be called") - - describe "ensureClientCanAdminProject", -> - beforeEach -> - @client.set("project_id", "project-id") - - it "should ignore a readOnly privilege", -> - @client.set("privilege_level", "readOnly") - @AuthorizationManager.ensureClientCanAdminProject @client, () -> - throw new Error("Should not be called") - - it "should ignore a readAndWrite privilege", -> - @client.set("privilege_level", "readAndWrite") - @AuthorizationManager.ensureClientCanAdminProject @client, () -> - throw new Error("Should not be called") - - it "should let the request through for a owner privilege", (done) -> - @client.set("privilege_level", "owner") - @AuthorizationManager.ensureClientCanAdminProject @client, done - - it "should ignore an empty privilege", -> - @AuthorizationManager.ensureClientCanAdminProject @client, () -> - throw new Error("Should not be called") - - describe "ensureClientHasPrivilegeLevelForProject", -> - it "should ignore callback if privilege_level is not set", -> - @client.set("project_id", "project-id") - @AuthorizationManager.ensureClientHasPrivilegeLevelForProject @client, - ["owner"], (error, project_id) -> - throw new Error("Should not be called") - - it "should ignore callback if project_id is not set", -> - @client.set("privilege_level", "owner") - @AuthorizationManager.ensureClientHasPrivilegeLevelForProject @client, - ["owner"], (error, project_id) -> - throw new Error("Should not be called") - - it "should return the project_id", (done) -> - @client.set("privilege_level", "owner") - @client.set("project_id", "project-id-123") - @AuthorizationManager.ensureClientHasPrivilegeLevelForProject @client, - ["owner"], (error, project_id) -> - project_id.should.equal "project-id-123" - done() - diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee index 87cbc11671..631e4b57b6 100644 --- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee @@ -23,8 +23,8 @@ describe "SubscriptionController sanboxed", -> @user = {email:"tom@yahoo.com"} @activeRecurlySubscription = mockSubscriptions["subscription-123-active"] - @SecurityManager = - getCurrentUser: sinon.stub().callsArgWith(1, null, @user) + @AuthenticationController = + getLoggedInUser: sinon.stub().callsArgWith(1, null, @user) @SubscriptionHandler = createSubscription: sinon.stub().callsArgWith(3) updateSubscription: sinon.stub().callsArgWith(3) @@ -61,7 +61,7 @@ describe "SubscriptionController sanboxed", -> @SubscriptionDomainHandler = getDomainLicencePage:sinon.stub() @SubscriptionController = SandboxedModule.require modulePath, requires: - '../../managers/SecurityManager': @SecurityManager + '../Authentication/AuthenticationController': @AuthenticationController './SubscriptionHandler': @SubscriptionHandler "./PlansLocator": @PlansLocator './SubscriptionViewModelBuilder': @SubscriptionViewModelBuilder From 7e216391b72c10749cb79a11b2f78826b5768101 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Sat, 12 Mar 2016 10:55:17 +0000 Subject: [PATCH 025/208] wip --- services/web/app/views/layout.jade | 12 ++---------- services/web/app/views/layout/navbar.jade | 2 +- services/web/public/coffee/base.coffee | 9 +++++++++ services/web/public/coffee/libs.coffee | 1 + services/web/public/coffee/main.coffee | 3 +++ 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index a8cc76ee90..2d3d13c227 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -65,8 +65,8 @@ html(itemscope, itemtype='http://schema.org/Product') window.ab = {}; window.user_id = '#{getLoggedInUserId()}'; - script. - (function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true; s.src=('https:'==document.location.protocol?'https':'http') + '://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket.js'; var q = document.getElementsByTagName('script')[0];q.parentNode.insertBefore(s, q);})(); + //- script. + //- (function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true; s.src=('https:'==document.location.protocol?'https':'http') + '://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket.js'; var q = document.getElementsByTagName('script')[0];q.parentNode.insertBefore(s, q);})(); - if (typeof(settings.algolia) != "undefined") @@ -132,12 +132,4 @@ html(itemscope, itemtype='http://schema.org/Product') ) - script. - var toggleGroove = function(){ - var e = arguments[0]||window.event; - e.stopPropagation(); - GrooveWidget.toggle() - } - - diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.jade index 89ff4e1dc2..5f8538cb31 100644 --- a/services/web/app/views/layout/navbar.jade +++ b/services/web/app/views/layout/navbar.jade @@ -27,7 +27,7 @@ nav.navbar.navbar-default a.dropdown-toggle(href, dropdown-toggle) | !{translate(item.text)} b.caret - ul.dropdown-menu + ul.dropdown-menu(ng-controller="NavController") each child in item.dropdown if child.divider li.divider diff --git a/services/web/public/coffee/base.coffee b/services/web/public/coffee/base.coffee index 98cdb3a871..bc040a16cc 100644 --- a/services/web/public/coffee/base.coffee +++ b/services/web/public/coffee/base.coffee @@ -24,4 +24,13 @@ define [ client_id: window.user_id }) + App.controller "NavController", ($scope) -> + + $scope.toggleGroove = -> + $scope['is-open'] = false + $scope['isOpen'] = false + GrooveWidget.toggle() + + + return App diff --git a/services/web/public/coffee/libs.coffee b/services/web/public/coffee/libs.coffee index 3c30dd63f3..fd1b5d0c91 100644 --- a/services/web/public/coffee/libs.coffee +++ b/services/web/public/coffee/libs.coffee @@ -12,6 +12,7 @@ define [ "libs/angular-cookies" "libs/passfield" "libs/sixpack" + "libs/groove" "libs/angular-sixpack" "libs/ng-tags-input-3.0.0" ], () -> diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index ad3e8d8a36..1d59eb19c8 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -30,3 +30,6 @@ define [ "__MAIN_CLIENTSIDE_INCLUDES__" ], () -> angular.bootstrap(document.body, ["SharelatexApp"]) + + + From 2d4ced6a1caf913a3f1493e42f2a0d2797496950 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 14 Mar 2016 16:19:08 +0000 Subject: [PATCH 026/208] If user is not project owner, ask them to ask the owner to upgrade. --- services/web/app/views/project/editor/pdf.jade | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index 5d58f64e13..bd611020bd 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -112,8 +112,10 @@ div.full-size.pdf(ng-controller="PdfController") | #{translate("learn_how_to_make_documents_compile_quickly")} .alert.alert-success(ng-show="pdf.timedout && !hasPremiumCompile") - p + p(ng-if="project.owner._id == user.id") strong #{translate("upgrade_for_faster_compiles")} + p(ng-if="project.owner._id != user.id") + strong #{translate("ask_proj_owner_to_upgrade_for_faster_compiles")} p #{translate("free_accounts_have_timeout_upgrade_to_increase")} p Plus: p @@ -142,7 +144,7 @@ div.full-size.pdf(ng-controller="PdfController") i.fa.fa-check   |#{translate("compile_larger_projects")} - p(ng-controller="FreeTrialModalController") + p(ng-controller="FreeTrialModalController", ng-if="project.owner._id == user.id") a.btn.btn-success.row-spaced-small( href ng-class="buttonClass" From 71ef0457282fcac6a26d304b1902ae50944e4296 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 14 Mar 2016 17:06:57 +0000 Subject: [PATCH 027/208] Implement authorization guards in Authorization{Manager,Controller} --- .../AuthenticationController.coffee | 2 + .../Authorization/AuthorizationManager.coffee | 57 ++- .../AuthorizationMiddlewear.coffee | 94 ++++- .../Collaborators/CollaboratorsHandler.coffee | 18 +- .../Editor/EditorHttpController.coffee | 4 +- .../Features/Project/ProjectController.coffee | 4 +- .../Project/ProjectEditorHandler.coffee | 5 +- .../AuthorizationManagerTests.coffee | 340 ++++++++++++++++++ .../AuthorizationMiddlewearTests.coffee | 221 ++++++++++++ .../CollaboratorsHandlerTests.coffee | 22 +- .../Editor/EditorHttpControllerTests.coffee | 4 +- .../Project/ProjectControllerTests.coffee | 4 +- .../Project/ProjectEditorHandlerTests.coffee | 2 +- .../coffee/AuthorizationTests.coffee | 4 +- 14 files changed, 753 insertions(+), 28 deletions(-) create mode 100644 services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index e7db5d9f65..24bc5743f3 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -99,6 +99,7 @@ module.exports = AuthenticationController = _redirectToLoginPage: (req, res) -> logger.log url: req.url, "user not logged in so redirecting to login page" + console.log req.session req.query.redir = req.path url = "/login?#{querystring.stringify(req.query)}" res.redirect url @@ -141,4 +142,5 @@ module.exports = AuthenticationController = req.session[key] = value req.session.user = lightUser + console.log "LOGGED IN", req.session callback() diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee index a6a80edae0..81d62c8b8e 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee @@ -1,12 +1,61 @@ -module.exports = - getPrivilegeLevelForProject: (user_id, project_id, callback = (error, canAccess, privilegeLevel) ->) -> - return callback(null, true, "readAndWrite") +CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") +Project = require("../../models/Project").Project +User = require("../../models/User").User + +module.exports = AuthorizationManager = + # Get the privilege level that the user has for the project + # Returns: + # * privilegeLevel: "owner", "readAndWrite", of "readOnly" if the user has + # access. false if the user does not have access + # * becausePublic: true if the access level is only because the project is public. + getPrivilegeLevelForProject: (user_id, project_id, callback = (error, privilegeLevel, becausePublic) ->) -> + getPublicAccessLevel = () -> + Project.findOne { _id: project_id }, { publicAccesLevel: 1 }, (error, project) -> + return callback(error) if error? + if project.publicAccesLevel in ["readOnly", "readAndWrite"] + return callback null, project.publicAccesLevel, true + else + return callback null, false, false + if !user_id? + getPublicAccessLevel() + else + CollaboratorsHandler.getMemberIdPrivilegeLevel user_id, project_id, (error, privilegeLevel) -> + return callback(error) if error? + if privilegeLevel? and privilegeLevel + # The user has direct access + callback null, privilegeLevel, false + else + getPublicAccessLevel() + canUserReadProject: (user_id, project_id, callback = (error, canRead) ->) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> + return callback(error) if error? + return callback null, (privilegeLevel in ["owner", "readAndWrite", "readOnly"]) + + canUserWriteProjectContent: (user_id, project_id, callback = (error, canWriteContent) ->) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> + return callback(error) if error? + return callback null, (privilegeLevel in ["owner", "readAndWrite"]) canUserWriteProjectSettings: (user_id, project_id, callback = (error, canWriteSettings) ->) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel, becausePublic) -> + return callback(error) if error? + if privilegeLevel == "owner" + return callback null, true + else if privilegeLevel == "readAndWrite" and !becausePublic + return callback null, true + else + return callback null, false canUserAdminProject: (user_id, project_id, callback = (error, canAdmin) ->) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> + return callback(error) if error? + return callback null, (privilegeLevel == "owner") isUserSiteAdmin: (user_id, callback = (error, isAdmin) ->) -> - \ No newline at end of file + if !user_id? + return callback null, false + User.findOne { _id: user_id }, { isAdmin: 1 }, (error, user) -> + return callback(error) if error? + return callback null, (user?.isAdmin == true) \ No newline at end of file diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee index a3bce5a2cc..2db1632ecf 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee @@ -1,21 +1,101 @@ -module.exports = +AuthorizationManager = require("./AuthorizationManager") +async = require "async" +logger = require "logger-sharelatex" + +module.exports = AuthorizationMiddlewear = ensureUserCanReadMultipleProjects: (req, res, next) -> - next() + project_ids = (req.query.project_ids or "").split(",") + AuthorizationMiddlewear._getUserId req, (error, user_id) -> + return next(error) if error? + # Remove the projects we have access to. Note rejectSeries doesn't use + # errors in callbacks + async.rejectSeries project_ids, (project_id, cb) -> + AuthorizationManager.canUserReadProject user_id, project_id, (error, canRead) -> + return next(error) if error? + cb(canRead) + , (unauthorized_project_ids) -> + if unauthorized_project_ids.length > 0 + AuthorizationMiddlewear.redirectToRestricted req, res, next + else + next() ensureUserCanReadProject: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> + return next(error) if error? + AuthorizationManager.canUserReadProject user_id, project_id, (error, canRead) -> + return next(error) if error? + if canRead + logger.log {user_id, project_id}, "allowing user read access to project" + next() + else + logger.log {user_id, project_id}, "denying user read access to project" + AuthorizationMiddlewear.redirectToRestricted req, res, next ensureUserCanWriteProjectSettings: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> + return next(error) if error? + AuthorizationManager.canUserWriteProjectSettings user_id, project_id, (error, canWrite) -> + return next(error) if error? + if canWrite + logger.log {user_id, project_id}, "allowing user write access to project settings" + next() + else + logger.log {user_id, project_id}, "denying user write access to project settings" + AuthorizationMiddlewear.redirectToRestricted req, res, next ensureUserCanWriteProjectContent: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> + return next(error) if error? + AuthorizationManager.canUserWriteProjectContent user_id, project_id, (error, canWrite) -> + return next(error) if error? + if canWrite + logger.log {user_id, project_id}, "allowing user write access to project content" + next() + else + logger.log {user_id, project_id}, "denying user write access to project settings" + AuthorizationMiddlewear.redirectToRestricted req, res, next ensureUserCanAdminProject: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> + return next(error) if error? + AuthorizationManager.canUserAdminProject user_id, project_id, (error, canAdmin) -> + return next(error) if error? + if canAdmin + logger.log {user_id, project_id}, "allowing user admin access to project" + next() + else + logger.log {user_id, project_id}, "denying user admin access to project" + AuthorizationMiddlewear.redirectToRestricted req, res, next ensureUserIsSiteAdmin: (req, res, next) -> - next() + AuthorizationMiddlewear._getUserId req, (error, user_id) -> + return next(error) if error? + AuthorizationManager.isUserSiteAdmin user_id, (error, isAdmin) -> + return next(error) if error? + if isAdmin + logger.log {user_id}, "allowing user admin access to site" + next() + else + logger.log {user_id}, "denying user admin access to site" + AuthorizationMiddlewear.redirectToRestricted req, res, next + + _getUserAndProjectId: (req, callback = (error, user_id, project_id) ->) -> + project_id = req.params?.project_id or req.params?.Project_id + if !project_id? + return callback(new Error("Expected project_id in request parameters")) + AuthorizationMiddlewear._getUserId req, (error, user_id) -> + return callback(error) if error? + callback(null, user_id, project_id) + + _getUserId: (req, callback = (error, user_id) ->) -> + if req.session?.user?._id? + user_id = req.session.user._id + else + user_id = null + callback null, user_id + + redirectToRestricted: (req, res, next) -> + res.redirect "/restricted" restricted : (req, res, next)-> if req.session.user? diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index f6b8ca7bbd..4f49505af7 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -13,11 +13,11 @@ module.exports = CollaboratorsHandler = return callback(error) if error? return callback null, null if !project? members = [] - members.push { id: project.owner_ref.toString(), privilegeLevel: "admin" } + members.push { id: project.owner_ref.toString(), privilegeLevel: "owner" } for member_id in project.readOnly_refs or [] - members.push { id: member_id, privilegeLevel: "readOnly" } + members.push { id: member_id.toString(), privilegeLevel: "readOnly" } for member_id in project.collaberator_refs or [] - members.push { id: member_id, privilegeLevel: "readAndWrite" } + members.push { id: member_id.toString(), privilegeLevel: "readAndWrite" } return callback null, members getMemberIds: (project_id, callback = (error, member_ids) ->) -> @@ -33,7 +33,17 @@ module.exports = CollaboratorsHandler = UserGetter.getUser member.id, (error, user) -> return cb(error) if error? return cb(null, { user: user, privilegeLevel: member.privilegeLevel }) - callback + callback + + getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) -> + # In future if the schema changes and getting all member ids is more expensive (multiple documents) + # then optimise this. + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + return callback(error) if error? + for member in members or [] + if member.id == user_id?.toString() + return callback null, member.privilegeLevel + return callback null, false getMemberCount: (project_id, callback = (error, count) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 776a1c1fb0..ab252472bd 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -34,9 +34,9 @@ module.exports = EditorHttpController = return callback(error) if error? UserGetter.getUser user_id, { isAdmin: true }, (error, user) -> return callback(error) if error? - AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, canAccess, privilegeLevel) -> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - if !canAccess + if !privilegeLevel callback null, null, false else callback(null, diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index ba5ff559f0..ae90f291b2 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -225,9 +225,9 @@ module.exports = ProjectController = daysSinceLastUpdated = (new Date() - project.lastUpdated) /86400000 logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor" - AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, canAccess, privilegeLevel)-> + AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel)-> return next(error) if error? - if !canAccess + if !privilegeLevel return res.sendStatus 401 if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt? diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index 5b3143d765..fb4e072672 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -27,11 +27,12 @@ module.exports = ProjectEditorHandler = owner = null for member in members - if member.privilegeLevel == "admin" + if member.privilegeLevel == "owner" owner = member.user else result.members.push @buildUserModelView member.user, member.privilegeLevel - result.owner = @buildUserModelView owner, "owner" + if owner? + result.owner = @buildUserModelView owner, "owner" if owner?.features? if owner.features.collaborators? diff --git a/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee new file mode 100644 index 0000000000..b1a464829c --- /dev/null +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee @@ -0,0 +1,340 @@ +sinon = require('sinon') +chai = require('chai') +should = chai.should() +expect = chai.expect +modulePath = "../../../../app/js/Features/Authorization/AuthorizationManager.js" +SandboxedModule = require('sandboxed-module') + +describe "AuthorizationManager", -> + beforeEach -> + @AuthorizationManager = SandboxedModule.require modulePath, requires: + "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} + "../../models/Project": Project: @Project = {} + "../../models/User": User: @User = {} + @user_id = "user-id-1" + @project_id = "project-id-1" + @callback = sinon.stub() + + describe "getPrivilegeLevelForProject", -> + beforeEach -> + @Project.findOne = sinon.stub() + @CollaboratorsHandler.getMemberIdPrivilegeLevel = sinon.stub() + + describe "with a private project", -> + beforeEach -> + @Project.findOne + .withArgs({ _id: @project_id }, { publicAccesLevel: 1 }) + .yields(null, { publicAccesLevel: "private" }) + + describe "with a user_id with a privilege level", -> + beforeEach -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, "readOnly") + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return the user's privilege level", -> + @callback.calledWith(null, "readOnly", false).should.equal true + + describe "with a user_id with no privilege level", -> + beforeEach -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, false) + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return false", -> + @callback.calledWith(null, false, false).should.equal true + + describe "with no user (anonymous)", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @callback + + it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false + + it "should return false", -> + @callback.calledWith(null, false, false).should.equal true + + describe "with a public project", -> + beforeEach -> + @Project.findOne + .withArgs({ _id: @project_id }, { publicAccesLevel: 1 }) + .yields(null, { publicAccesLevel: "readAndWrite" }) + + describe "with a user_id with a privilege level", -> + beforeEach -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, "readOnly") + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return the user's privilege level", -> + @callback.calledWith(null, "readOnly", false).should.equal true + + describe "with a user_id with no privilege level", -> + beforeEach -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, false) + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return the public privilege level", -> + @callback.calledWith(null, "readAndWrite", true).should.equal true + + describe "with no user (anonymous)", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @callback + + it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> + @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false + + it "should return the public privilege level", -> + @callback.calledWith(null, "readAndWrite", true).should.equal true + + describe "canUserReadProject", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() + + describe "when user is owner", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "owner", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserReadProject @user_id, @project_id, (error, canRead) -> + expect(canRead).to.equal true + done() + + describe "when user has read-write access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserReadProject @user_id, @project_id, (error, canRead) -> + expect(canRead).to.equal true + done() + + describe "when user has read-only access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readOnly", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserReadProject @user_id, @project_id, (error, canRead) -> + expect(canRead).to.equal true + done() + + describe "when user has no access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, false, false) + + it "should return false", (done) -> + @AuthorizationManager.canUserReadProject @user_id, @project_id, (error, canRead) -> + expect(canRead).to.equal false + done() + + describe "canUserWriteProjectContent", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() + + describe "when user is owner", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "owner", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal true + done() + + describe "when user has read-write access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal true + done() + + describe "when user has read-only access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readOnly", false) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "when user has no access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, false, false) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectContent @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "canUserWriteProjectSettings", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() + + describe "when user is owner", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "owner", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal true + done() + + describe "when user has read-write access as a collaborator", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal true + done() + + describe "when user has read-write access as the public", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", true) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "when user has read-only access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readOnly", false) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "when user has no access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, false, false) + + it "should return false", (done) -> + @AuthorizationManager.canUserWriteProjectSettings @user_id, @project_id, (error, canWrite) -> + expect(canWrite).to.equal false + done() + + describe "canUserAdminProject", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub() + + describe "when user is owner", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "owner", false) + + it "should return true", (done) -> + @AuthorizationManager.canUserAdminProject @user_id, @project_id, (error, canAdmin) -> + expect(canAdmin).to.equal true + done() + + describe "when user has read-write access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readAndWrite", false) + + it "should return false", (done) -> + @AuthorizationManager.canUserAdminProject @user_id, @project_id, (error, canAdmin) -> + expect(canAdmin).to.equal false + done() + + describe "when user has read-only access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, "readOnly", false) + + it "should return false", (done) -> + @AuthorizationManager.canUserAdminProject @user_id, @project_id, (error, canAdmin) -> + expect(canAdmin).to.equal false + done() + + describe "when user has no access", -> + beforeEach -> + @AuthorizationManager.getPrivilegeLevelForProject + .withArgs(@user_id, @project_id) + .yields(null, false, false) + + it "should return false", (done) -> + @AuthorizationManager.canUserAdminProject @user_id, @project_id, (error, canAdmin) -> + expect(canAdmin).to.equal false + done() + + describe "isUserSiteAdmin", -> + beforeEach -> + @User.findOne = sinon.stub() + + describe "when user is admin", -> + beforeEach -> + @User.findOne + .withArgs({ _id: @user_id }, { isAdmin: 1 }) + .yields(null, { isAdmin: true }) + + it "should return true", (done) -> + @AuthorizationManager.isUserSiteAdmin @user_id, (error, isAdmin) -> + expect(isAdmin).to.equal true + done() + + describe "when user is not admin", -> + beforeEach -> + @User.findOne + .withArgs({ _id: @user_id }, { isAdmin: 1 }) + .yields(null, { isAdmin: false }) + + it "should return false", (done) -> + @AuthorizationManager.isUserSiteAdmin @user_id, (error, isAdmin) -> + expect(isAdmin).to.equal false + done() + + describe "when user is not found", -> + beforeEach -> + @User.findOne + .withArgs({ _id: @user_id }, { isAdmin: 1 }) + .yields(null, null) + + it "should return false", (done) -> + @AuthorizationManager.isUserSiteAdmin @user_id, (error, isAdmin) -> + expect(isAdmin).to.equal false + done() + + describe "when no user is passed", -> + it "should return false", (done) -> + @AuthorizationManager.isUserSiteAdmin null, (error, isAdmin) => + @User.findOne.called.should.equal false + expect(isAdmin).to.equal false + done() diff --git a/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee new file mode 100644 index 0000000000..7e1ca2d5ef --- /dev/null +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee @@ -0,0 +1,221 @@ +sinon = require('sinon') +chai = require('chai') +should = chai.should() +expect = chai.expect +modulePath = "../../../../app/js/Features/Authorization/AuthorizationMiddlewear.js" +SandboxedModule = require('sandboxed-module') + +describe "AuthorizationMiddlewear", -> + beforeEach -> + @AuthorizationMiddlewear = SandboxedModule.require modulePath, requires: + "./AuthorizationManager": @AuthorizationManager = {} + "logger-sharelatex": {log: () ->} + @user_id = "user-id-123" + @project_id = "project-id-123" + @req = {} + @res = {} + @next = sinon.stub() + + METHODS_TO_TEST = { + "ensureUserCanReadProject": "canUserReadProject" + "ensureUserCanWriteProjectSettings": "canUserWriteProjectSettings" + "ensureUserCanWriteProjectContent": "canUserWriteProjectContent" + "ensureUserCanAdminProject": "canUserAdminProject" + } + for middlewearMethod, managerMethod of METHODS_TO_TEST + do (middlewearMethod, managerMethod) -> + describe middlewearMethod, -> + beforeEach -> + @req.params = + project_id: @project_id + @AuthorizationManager[managerMethod] = sinon.stub() + @AuthorizationMiddlewear.redirectToRestricted = sinon.stub() + + describe "with missing project_id", -> + beforeEach -> + @req.params = {} + + it "should return an error to next", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.calledWith(new Error()).should.equal true + + describe "with logged in user", -> + beforeEach -> + @req.session = + user: _id: @user_id + + describe "when user has permission", -> + beforeEach -> + @AuthorizationManager[managerMethod] + .withArgs(@user_id, @project_id) + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission", -> + beforeEach -> + @AuthorizationManager[managerMethod] + .withArgs(@user_id, @project_id) + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "with anonymous user", -> + describe "when user has permission", -> + beforeEach -> + @AuthorizationManager[managerMethod] + .withArgs(null, @project_id) + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission", -> + beforeEach -> + @AuthorizationManager[managerMethod] + .withArgs(null, @project_id) + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "ensureUserIsSiteAdmin", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin = sinon.stub() + @AuthorizationMiddlewear.redirectToRestricted = sinon.stub() + + describe "with logged in user", -> + beforeEach -> + @req.session = + user: _id: @user_id + + describe "when user has permission", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin + .withArgs(@user_id) + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin + .withArgs(@user_id) + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "with anonymous user", -> + describe "when user has permission", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin + .withArgs(null) + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin + .withArgs(null) + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "ensureUserCanReadMultipleProjects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject = sinon.stub() + @AuthorizationMiddlewear.redirectToRestricted = sinon.stub() + @req.query = + project_ids: "project1,project2" + + describe "with logged in user", -> + beforeEach -> + @req.session = + user: _id: @user_id + + describe "when user has permission to access all projects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject + .withArgs(@user_id, "project1") + .yields(null, true) + @AuthorizationManager.canUserReadProject + .withArgs(@user_id, "project2") + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission to access one of the projects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject + .withArgs(@user_id, "project1") + .yields(null, true) + @AuthorizationManager.canUserReadProject + .withArgs(@user_id, "project2") + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true + + describe "with anonymous user", -> + describe "when user has permission", -> + describe "when user has permission to access all projects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject + .withArgs(null, "project1") + .yields(null, true) + @AuthorizationManager.canUserReadProject + .withArgs(null, "project2") + .yields(null, true) + + it "should return next", -> + @AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next + @next.called.should.equal true + + describe "when user doesn't have permission to access one of the projects", -> + beforeEach -> + @AuthorizationManager.canUserReadProject + .withArgs(null, "project1") + .yields(null, true) + @AuthorizationManager.canUserReadProject + .withArgs(null, "project2") + .yields(null, false) + + it "should redirect to redirectToRestricted", -> + @AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next + @next.called.should.equal false + @AuthorizationMiddlewear.redirectToRestricted + .calledWith(@req, @res, @next) + .should.equal true diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 391b533cdf..f659cfebe6 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -36,7 +36,7 @@ describe "CollaboratorsHandler", -> it "should return an array of member ids with their privilege levels", -> @callback .calledWith(null, [ - { id: "owner-ref", privilegeLevel: "admin" } + { id: "owner-ref", privilegeLevel: "owner" } { id: "read-only-ref-1", privilegeLevel: "readOnly" } { id: "read-only-ref-2", privilegeLevel: "readOnly" } { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } @@ -83,6 +83,26 @@ describe "CollaboratorsHandler", -> ]) .should.equal true + describe "getMemberIdPrivilegeLevel", -> + beforeEach -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels + .withArgs(@project_id) + .yields(null, [ + {id: "member-id-1", privilegeLevel: "readAndWrite"} + {id: "member-id-2", privilegeLevel: "readOnly"} + ]) + + it "should return the privilege level if it exists", (done) -> + @CollaboratorHandler.getMemberIdPrivilegeLevel "member-id-2", @project_id, (error, level) -> + expect(level).to.equal "readOnly" + done() + + it "should return false if the member has no privilege level", (done) -> + @CollaboratorHandler.getMemberIdPrivilegeLevel "member-id-3", @project_id, (error, level) -> + expect(level).to.equal false + done() + describe "isUserMemberOfProject", -> beforeEach -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee index 1160c81c37..6a594f31ee 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee @@ -99,7 +99,7 @@ describe "EditorHttpController", -> describe "when authorized", -> beforeEach -> @AuthorizationManager.getPrivilegeLevelForProject = - sinon.stub().callsArgWith(2, null, true, "owner") + sinon.stub().callsArgWith(2, null, "owner") @EditorHttpController._buildJoinProjectView(@project_id, @user_id, @callback) it "should find the project without doc lines", -> @@ -128,7 +128,7 @@ describe "EditorHttpController", -> describe "when not authorized", -> beforeEach -> @AuthorizationManager.getPrivilegeLevelForProject = - sinon.stub().callsArgWith(2, null, false, null) + sinon.stub().callsArgWith(2, null, null) @EditorHttpController._buildJoinProjectView(@project_id, @user_id, @callback) it "should return false in the callback", -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 1dbdc8539e..54ea831e05 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -299,7 +299,7 @@ describe "ProjectController", -> @ProjectModel.findOne.callsArgWith 1, null, @project @UserModel.findById.callsArgWith(1, null, @user) @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) - @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 2, null, true, "owner" + @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 2, null, "owner" @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() @InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1) @ProjectUpdateHandler.markAsOpened.callsArgWith(1) @@ -332,7 +332,7 @@ describe "ProjectController", -> @ProjectController.loadEditor @req, @res it "should not render the page if the project can not be accessed", (done)-> - @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub().callsArgWith 2, null, false + @AuthorizationManager.getPrivilegeLevelForProject = sinon.stub().callsArgWith 2, null, null @res.sendStatus = (resCode, opts)=> resCode.should.equal 401 done() diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee index 7d71f6c89f..eede50bfd3 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee @@ -49,7 +49,7 @@ describe "ProjectEditorHandler", -> last_name : "ShareLaTeX" email : "owner@sharelatex.com" }, - privilegeLevel: "admin" + privilegeLevel: "owner" },{ user: { _id: "read-only-id" diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index a9533e9533..bed1a56f29 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -44,6 +44,8 @@ class User projectName: name }, (error, response, body) -> return callback(error) if error? + if !body?.project_id? + console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body callback(null, body.project_id) addUserToProject: (project_id, email, privileges, callback = (error, user) ->) -> @@ -240,7 +242,7 @@ describe "Authorization", -> expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user read access to it", (done) -> - expect_no_read_access @anon, @project_id, redirect_to: "/login", done + expect_no_read_access @anon, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user write access to its settings", (done) -> expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done From b556d57f40d74ac904a7dcc644895b1e88b4cf01 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 14 Mar 2016 17:11:23 +0000 Subject: [PATCH 028/208] Remove missed console.log debugging lines in AuthenticationController.coffee --- .../Features/Authentication/AuthenticationController.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 24bc5743f3..e7db5d9f65 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -99,7 +99,6 @@ module.exports = AuthenticationController = _redirectToLoginPage: (req, res) -> logger.log url: req.url, "user not logged in so redirecting to login page" - console.log req.session req.query.redir = req.path url = "/login?#{querystring.stringify(req.query)}" res.redirect url @@ -142,5 +141,4 @@ module.exports = AuthenticationController = req.session[key] = value req.session.user = lightUser - console.log "LOGGED IN", req.session callback() From ba9bc3a2e8ca47274105c2323b1d5b196ee78a7e Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 15 Mar 2016 11:29:59 +0000 Subject: [PATCH 029/208] check that element being inserted has an _id --- .../coffee/Features/Project/ProjectEntityHandler.coffee | 2 +- .../coffee/Project/ProjectEntityHandlerTests.coffee | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index 9b771625b3..7d3f7df6e2 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -501,7 +501,7 @@ module.exports = ProjectEntityHandler = elementType = "fileRefs" return elementType - if !element? + if !element? or !element._id? e = new Error("no element passed to be inserted") logger.err project_id:project._id, folder_id:folder_id, element:element, type:type, "failed trying to insert element as it was null" return callback(e) diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee index a44a16c10b..0e2441cf8d 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee @@ -1048,6 +1048,13 @@ describe 'ProjectEntityHandler', -> @projectLocator.findElement.args[0][0].element_id.should.equal @project.rootFolder[0]._id done() + it "should error if the element has no _id", (done)-> + doc = + name:"something" + @ProjectEntityHandler._putElement @project, @folder._id, doc, "doc", (err)=> + @ProjectModel.update.called.should.equal false + done() + describe "_countElements", -> From 76b3a78988eb7b03403f03909e407e4e36bec4b0 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 15 Mar 2016 12:29:41 +0000 Subject: [PATCH 030/208] added lock around move element --- .../coffee/Features/Editor/EditorController.coffee | 13 +++++++++---- .../Features/Project/ProjectEntityHandler.coffee | 2 +- .../coffee/Editor/EditorControllerTests.coffee | 11 +++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index 38f788dd41..476ba96174 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -164,12 +164,17 @@ module.exports = EditorController = moveEntity: (project_id, entity_id, folder_id, entityType, callback)-> Metrics.inc "editor.move-entity" - ProjectEntityHandler.moveEntity project_id, entity_id, folder_id, entityType, => + LockManager.getLock project_id, (err)-> if err? - logger.err err:err, project_id:project_id, entity_id:entity_id, folder_id:folder_id, "error moving entity" + logger.err err:err, project_id:project_id, "could not get lock for move entity" return callback(err) - EditorRealTimeController.emitToRoom project_id, 'reciveEntityMove', entity_id, folder_id - callback?() + ProjectEntityHandler.moveEntity project_id, entity_id, folder_id, entityType, => + if err? + logger.err err:err, project_id:project_id, entity_id:entity_id, folder_id:folder_id, "error moving entity" + return callback(err) + LockManager.releaseLock project_id, -> + EditorRealTimeController.emitToRoom project_id, 'reciveEntityMove', entity_id, folder_id + callback?() renameProject: (project_id, newName, callback = (err) ->) -> ProjectDetailsHandler.renameProject project_id, newName, -> diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index 7d3f7df6e2..8544ed7884 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -520,13 +520,13 @@ module.exports = ProjectEntityHandler = newPath = fileSystem: "#{path.fileSystem}/#{element.name}" mongo: path.mongo - logger.log project_id: project._id, element_id: element._id, fileType: type, folder_id: folder_id, "adding element to project" id = element._id+'' element._id = require('mongoose').Types.ObjectId(id) conditions = _id:project._id mongopath = "#{path.mongo}.#{type}" update = "$push":{} update["$push"][mongopath] = element + logger.log project_id: project._id, element_id: element._id, fileType: type, folder_id: folder_id, mongopath:mongopath, "adding element to project" Project.update conditions, update, {}, (err)-> if err? logger.err err: err, project_id: project._id, 'error saving in putElement project' diff --git a/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee index a60830b226..fd56fbb021 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee @@ -518,12 +518,23 @@ describe "EditorController", -> @folder_id = "313dasd21dasdsa" @ProjectEntityHandler.moveEntity = sinon.stub().callsArgWith(4, @err) @EditorRealTimeController.emitToRoom = sinon.stub() + @LockManager.releaseLock.callsArgWith(1) + @LockManager.getLock.callsArgWith(1) it "should call the ProjectEntityHandler", (done)-> @EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, => @ProjectEntityHandler.moveEntity.calledWith(@project_id, @entity_id, @folder_id, @entityType).should.equal true done() + it "should take the lock", (done)-> + @EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, => + @LockManager.getLock.calledWith(@project_id).should.equal true + done() + + it "should release the lock", (done)-> + @EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, => + @LockManager.releaseLock.calledWith(@project_id).should.equal true + done() it "should emit the update to the room", (done)-> @EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, => From 57818944537e4b36a6d54971eab16acc9429010b Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:05:59 +0000 Subject: [PATCH 031/208] Do array null check in callback args --- .../Collaborators/CollaboratorsHandler.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 4f49505af7..3f76a6bedb 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -26,9 +26,9 @@ module.exports = CollaboratorsHandler = return callback null, members.map (m) -> m.id getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> return callback(error) if error? - async.mapLimit (members or []), 3, + async.mapLimit members, 3, (member, cb) -> UserGetter.getUser member.id, (error, user) -> return cb(error) if error? @@ -38,9 +38,9 @@ module.exports = CollaboratorsHandler = getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) -> # In future if the schema changes and getting all member ids is more expensive (multiple documents) # then optimise this. - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> return callback(error) if error? - for member in members or [] + for member in members if member.id == user_id?.toString() return callback null, member.privilegeLevel return callback null, false @@ -56,9 +56,9 @@ module.exports = CollaboratorsHandler = return callback null, count - 1 # Don't count project owner isUserMemberOfProject: (user_id, project_id, callback = (error, isMember, privilegeLevel) ->) -> - CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> + CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> return callback(error) if error? - for member in members or [] + for member in members if member.id.toString() == user_id.toString() return callback null, true, member.privilegeLevel return callback null, false, null From d09705142e9b86cfd6654aa50744a81492687af4 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:07:34 +0000 Subject: [PATCH 032/208] Add in missing error checks --- .../coffee/Features/Collaborators/CollaboratorsHandler.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 3f76a6bedb..7f0cd6ebca 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -65,8 +65,10 @@ module.exports = CollaboratorsHandler = getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) -> Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=> + return callback(err) if err? Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=> - callback(err, readAndWriteProjects, readOnlyProjects) + return callback(err) if err? + callback(null, readAndWriteProjects, readOnlyProjects) removeUserFromProject: (project_id, user_id, callback = (error) ->)-> logger.log user_id: user_id, project_id: project_id, "removing user" From fe1f71413e09a25c6ec2e57706002e48485aea67 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:12:43 +0000 Subject: [PATCH 033/208] Use ProjectGetter, not Project, in ProjectController.loadEditor --- .../app/coffee/Features/Project/ProjectController.coffee | 3 +-- .../UnitTests/coffee/Project/ProjectControllerTests.coffee | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index ae90f291b2..4e1a6acf2c 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -5,7 +5,6 @@ projectDuplicator = require("./ProjectDuplicator") projectCreationHandler = require("./ProjectCreationHandler") editorController = require("../Editor/EditorController") metrics = require('../../infrastructure/Metrics') -Project = require('../../models/Project').Project User = require('../../models/User').User TagsHandler = require("../Tags/TagsHandler") SubscriptionLocator = require("../Subscription/SubscriptionLocator") @@ -195,7 +194,7 @@ module.exports = ProjectController = async.parallel { project: (cb)-> - Project.findOne { _id: project_id }, cb + ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1}, cb user: (cb)-> if user_id == 'openUser' cb null, defaultSettingsForAnonymousUser(user_id) diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 54ea831e05..4db3648988 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -35,8 +35,6 @@ describe "ProjectController", -> getAllTags: sinon.stub() @NotificationsHandler = getUserNotifications: sinon.stub() - @ProjectModel = - findOne: sinon.stub() @UserModel = findById: sinon.stub() @AuthorizationManager = @@ -51,6 +49,7 @@ describe "ProjectController", -> indexProjectReferences: sinon.stub() @ProjectGetter = findAllUsersProjects: sinon.stub() + getProject: sinon.stub() @ProjectController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings "logger-sharelatex": @@ -64,7 +63,6 @@ describe "ProjectController", -> "../Subscription/LimitationsManager": @LimitationsManager "../Tags/TagsHandler":@TagsHandler "../Notifications/NotificationsHandler":@NotificationsHandler - '../../models/Project': Project:@ProjectModel "../../models/User":User:@UserModel "../Authorization/AuthorizationManager":@AuthorizationManager "../InactiveData/InactiveProjectManager":@InactiveProjectManager @@ -296,7 +294,7 @@ describe "ProjectController", -> fontSize:"massive" theme:"sexy" email: "bob@bob.com" - @ProjectModel.findOne.callsArgWith 1, null, @project + @ProjectGetter.getProject.callsArgWith 2, null, @project @UserModel.findById.callsArgWith(1, null, @user) @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) @AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 2, null, "owner" From 724e6b52634d887b5db163058badffac7ca9f098 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:14:33 +0000 Subject: [PATCH 034/208] Require explicit value of true for ENV variables in config --- 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 b45837193b..ffa91e31b9 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -256,7 +256,7 @@ module.exports = # 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"]? then true else false + allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false # Maximum size of text documents in the real-time editing system. max_doc_length: 2 * 1024 * 1024 # 2mb From 398d43e2d1cba21d89a3c6862dbfff76f89e9782 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:15:25 +0000 Subject: [PATCH 035/208] Add missing ? check --- .../coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee index 88397c9ef6..747a763e51 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee @@ -121,7 +121,7 @@ getProjectsUsersIds = (project_id, callback = (err, owner_id, allUserIds)->)-> return callback(err) if err? CollaboratorsHandler.getMemberIds project_id, (err, member_ids) -> return callback(err) if err? - callback err, project.owner_ref, member_ids + callback err, project?.owner_ref, member_ids mergeProjectNameAndPath = (project_name, path)-> if(path.indexOf('/') == 0) From 261466b042bc616b65028426526d984708e28909 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:35:01 +0000 Subject: [PATCH 036/208] Convert privilege levels to an enum --- .../Authorization/AuthorizationManager.coffee | 22 +++++++++++-------- .../Authorization/PrivilegeLevels.coffee | 5 +++++ .../Authorization/PublicAccessLevels.coffee | 4 ++++ .../Collaborators/CollaboratorsHandler.coffee | 13 ++++++----- .../Features/Project/ProjectController.coffee | 3 ++- .../Project/ProjectDetailsHandler.coffee | 3 ++- 6 files changed, 33 insertions(+), 17 deletions(-) create mode 100644 services/web/app/coffee/Features/Authorization/PrivilegeLevels.coffee create mode 100644 services/web/app/coffee/Features/Authorization/PublicAccessLevels.coffee diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee index 81d62c8b8e..db49881bbf 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee @@ -1,6 +1,8 @@ CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") Project = require("../../models/Project").Project User = require("../../models/User").User +PrivilegeLevels = require("./PrivilegeLevels") +PublicAccessLevels = require("./PublicAccessLevels") module.exports = AuthorizationManager = # Get the privilege level that the user has for the project @@ -12,17 +14,19 @@ module.exports = AuthorizationManager = getPublicAccessLevel = () -> Project.findOne { _id: project_id }, { publicAccesLevel: 1 }, (error, project) -> return callback(error) if error? - if project.publicAccesLevel in ["readOnly", "readAndWrite"] - return callback null, project.publicAccesLevel, true + if project.publicAccesLevel == PublicAccessLevels.READ_ONLY + return callback null, PrivilegeLevels.READ_ONLY + else if project.publicAccesLevel == PublicAccessLevels.READ_AND_WRITE + return callback null, PrivilegeLevels.READ_AND_WRITE, true else - return callback null, false, false + return callback null, PrivilegeLevels.NONE, false if !user_id? getPublicAccessLevel() else CollaboratorsHandler.getMemberIdPrivilegeLevel user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - if privilegeLevel? and privilegeLevel + if privilegeLevel? and privilegeLevel != PrivilegeLevels.NONE # The user has direct access callback null, privilegeLevel, false else @@ -31,19 +35,19 @@ module.exports = AuthorizationManager = canUserReadProject: (user_id, project_id, callback = (error, canRead) ->) -> AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - return callback null, (privilegeLevel in ["owner", "readAndWrite", "readOnly"]) + return callback null, (privilegeLevel in [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE, PrivilegeLevels.READ_ONLY]) canUserWriteProjectContent: (user_id, project_id, callback = (error, canWriteContent) ->) -> AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - return callback null, (privilegeLevel in ["owner", "readAndWrite"]) + return callback null, (privilegeLevel in [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE]) canUserWriteProjectSettings: (user_id, project_id, callback = (error, canWriteSettings) ->) -> AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel, becausePublic) -> return callback(error) if error? - if privilegeLevel == "owner" + if privilegeLevel == PrivilegeLevels.OWNER return callback null, true - else if privilegeLevel == "readAndWrite" and !becausePublic + else if privilegeLevel == PrivilegeLevels.READ_AND_WRITE and !becausePublic return callback null, true else return callback null, false @@ -51,7 +55,7 @@ module.exports = AuthorizationManager = canUserAdminProject: (user_id, project_id, callback = (error, canAdmin) ->) -> AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - return callback null, (privilegeLevel == "owner") + return callback null, (privilegeLevel == PrivilegeLevels.OWNER) isUserSiteAdmin: (user_id, callback = (error, isAdmin) ->) -> if !user_id? diff --git a/services/web/app/coffee/Features/Authorization/PrivilegeLevels.coffee b/services/web/app/coffee/Features/Authorization/PrivilegeLevels.coffee new file mode 100644 index 0000000000..682ae08a02 --- /dev/null +++ b/services/web/app/coffee/Features/Authorization/PrivilegeLevels.coffee @@ -0,0 +1,5 @@ +module.exports = + NONE: false + READ_ONLY: "readOnly" + READ_AND_WRITE: "readAndWrite" + OWNER: "owner" \ No newline at end of file diff --git a/services/web/app/coffee/Features/Authorization/PublicAccessLevels.coffee b/services/web/app/coffee/Features/Authorization/PublicAccessLevels.coffee new file mode 100644 index 0000000000..8e63a64a33 --- /dev/null +++ b/services/web/app/coffee/Features/Authorization/PublicAccessLevels.coffee @@ -0,0 +1,4 @@ +module.exports = + READ_ONLY: "readOnly" + READ_AND_WRITE: "readAndWrite" + PRIVATE: "private" \ No newline at end of file diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 7f0cd6ebca..cb6459c85f 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -6,6 +6,7 @@ UserGetter = require "../User/UserGetter" ContactManager = require "../Contacts/ContactManager" CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" async = require "async" +PrivilegeLevels = require "../Authorization/PrivilegeLevels" module.exports = CollaboratorsHandler = getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> @@ -13,11 +14,11 @@ module.exports = CollaboratorsHandler = return callback(error) if error? return callback null, null if !project? members = [] - members.push { id: project.owner_ref.toString(), privilegeLevel: "owner" } + members.push { id: project.owner_ref.toString(), privilegeLevel: PrivilegeLevels.OWNER } for member_id in project.readOnly_refs or [] - members.push { id: member_id.toString(), privilegeLevel: "readOnly" } + members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_ONLY } for member_id in project.collaberator_refs or [] - members.push { id: member_id.toString(), privilegeLevel: "readAndWrite" } + members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_AND_WRITE } return callback null, members getMemberIds: (project_id, callback = (error, member_ids) ->) -> @@ -43,7 +44,7 @@ module.exports = CollaboratorsHandler = for member in members if member.id == user_id?.toString() return callback null, member.privilegeLevel - return callback null, false + return callback null, PrivilegeLevels.NONE getMemberCount: (project_id, callback = (error, count) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> @@ -100,10 +101,10 @@ module.exports = CollaboratorsHandler = if existing_users.indexOf(user_id.toString()) > -1 return callback null # User already in Project - if privilegeLevel == 'readAndWrite' + if privilegeLevel == PrivilegeLevels.READ_AND_WRITE level = {"collaberator_refs":user_id} logger.log {privileges: "readAndWrite", user_id, project_id}, "adding user" - else if privilegeLevel == 'readOnly' + else if privilegeLevel == PrivilegeLevels.READ_ONLY level = {"readOnly_refs":user_id} logger.log {privileges: "readOnly", user_id, project_id}, "adding user" else diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 4e1a6acf2c..d6a3e69ce4 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -17,6 +17,7 @@ fs = require "fs" InactiveProjectManager = require("../InactiveData/InactiveProjectManager") ProjectUpdateHandler = require("./ProjectUpdateHandler") ProjectGetter = require("./ProjectGetter") +PrivilegeLevels = require("../Authorization/PrivilegeLevels") module.exports = ProjectController = @@ -226,7 +227,7 @@ module.exports = ProjectController = AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel)-> return next(error) if error? - if !privilegeLevel + if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE return res.sendStatus 401 if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt? diff --git a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee index a0f8cca509..80eb67d88e 100644 --- a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee @@ -4,6 +4,7 @@ Project = require('../../models/Project').Project logger = require("logger-sharelatex") tpdsUpdateSender = require '../ThirdPartyDataStore/TpdsUpdateSender' _ = require("underscore") +PublicAccessLevels = require("../Authorization/PublicAccessLevels") module.exports = @@ -49,6 +50,6 @@ module.exports = setPublicAccessLevel : (project_id, newAccessLevel, callback = ->)-> logger.log project_id: project_id, level: newAccessLevel, "set public access level" - if project_id? && newAccessLevel? and _.include ['readOnly', 'readAndWrite', 'private'], newAccessLevel + if project_id? && newAccessLevel? and _.include [PublicAccessLevels.READ_ONLY, PublicAccessLevels.READ_AND_WRITE, PublicAccessLevels.PRIVATE], newAccessLevel Project.update {_id:project_id},{publicAccesLevel:newAccessLevel}, (err)-> callback() \ No newline at end of file From b7d226f43476c0db8bed936b4474fcef76994a34 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:39:27 +0000 Subject: [PATCH 037/208] Make privilege level check in EditorHttpController more explicit --- .../web/app/coffee/Features/Editor/EditorHttpController.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index ab252472bd..ebe1351110 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -9,6 +9,7 @@ AuthorizationManager = require("../Authorization/AuthorizationManager") ProjectEditorHandler = require('../Project/ProjectEditorHandler') Metrics = require('../../infrastructure/Metrics') CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") +PrivilegeLevels = require "../Authorization/PrivilegeLevels" module.exports = EditorHttpController = joinProject: (req, res, next) -> @@ -36,7 +37,7 @@ module.exports = EditorHttpController = return callback(error) if error? AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? - if !privilegeLevel + if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE callback null, null, false else callback(null, From 75d9912449b69d656d8cc2171ed6f973bcebc345 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 14:44:06 +0000 Subject: [PATCH 038/208] Use _.defaults to simplify assigning default features --- .../Project/ProjectEditorHandler.coffee | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index fb4e072672..a3d8319424 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -15,15 +15,6 @@ module.exports = ProjectEditorHandler = deletedByExternalDataSource : project.deletedByExternalDataSource || false deletedDocs: project.deletedDocs members: [] - - result.features = # defaults - collaborators: -1 # Infinite - versioning: false - dropbox:false - compileTimeout: 60 - compileGroup:"standard" - templates: false - references: false owner = null for member in members @@ -34,21 +25,15 @@ module.exports = ProjectEditorHandler = if owner? result.owner = @buildUserModelView owner, "owner" - if owner?.features? - if owner.features.collaborators? - result.features.collaborators = owner.features.collaborators - if owner.features.versioning? - result.features.versioning = owner.features.versioning - if owner.features.dropbox? - result.features.dropbox = owner.features.dropbox - if owner.features.compileTimeout? - result.features.compileTimeout = owner.features.compileTimeout - if owner.features.compileGroup? - result.features.compileGroup = owner.features.compileGroup - if owner.features.templates? - result.features.templates = owner.features.templates - if owner.features.references? - result.features.references = owner.features.references + result.features = _.defaults(owner?.features or {}, { + collaborators: -1 # Infinite + versioning: false + dropbox:false + compileTimeout: 60 + compileGroup:"standard" + templates: false + references: false + }) return result From 60a39f82c1782d5a3518502524e864514c3ebf52 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 15 Mar 2016 15:14:54 +0000 Subject: [PATCH 039/208] Fix off by one bug in moving folders from using an out of date project structure --- .../Project/ProjectEntityHandler.coffee | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index 8544ed7884..fff91d2ce3 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -344,15 +344,18 @@ module.exports = ProjectEntityHandler = return callback(error) if error? self._removeElementFromMongoArray Project, project_id, path.mongo, (err)-> return callback(err) if err? - ProjectEntityHandler._putElement project, destinationFolder_id, entity, entityType, (err, result)-> + # We've updated the project structure by removing the element, so must refresh it. + ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project)=> return callback(err) if err? - opts = - project_id:project_id - project_name:project.name - startPath:path.fileSystem - endPath:result.path.fileSystem, - rev:entity.rev - tpdsUpdateSender.moveEntity opts, callback + ProjectEntityHandler._putElement project, destinationFolder_id, entity, entityType, (err, result)-> + return callback(err) if err? + opts = + project_id:project_id + project_name:project.name + startPath:path.fileSystem + endPath:result.path.fileSystem, + rev:entity.rev + tpdsUpdateSender.moveEntity opts, callback deleteEntity: (project_id, entity_id, entityType, callback = (error) ->)-> self = @ From cff00c09b05ef5f893fb39e959f1e860d3227932 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 15 Mar 2016 16:02:48 +0000 Subject: [PATCH 040/208] Add a '}' at the end of citation autocomplete if required. --- .../auto-complete/AutoCompleteManager.coffee | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index 7b4889a81e..862e420067 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -45,12 +45,15 @@ define [ references = @$scope.$root._references ReferencesCompleter = getCompletions: (editor, session, pos, prefix, callback) -> - range = new Range(pos.row, 0, pos.row, pos.column) - lineUpToCursor = editor.getSession().getTextRange(range) + upToCursorRange = new Range(pos.row, 0, pos.row, pos.column) + lineUpToCursor = editor.getSession().getTextRange(upToCursorRange) commandFragment = getLastCommandFragment(lineUpToCursor) if commandFragment - citeMatch = commandFragment.match(/^~?\\([a-z]*cite[a-z]?){(.*,)?(\w*)/) + citeMatch = commandFragment.match(/^~?\\([a-z]*cite[a-z]?){([^}]*,)?(\w*)/) if citeMatch + beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999) + lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange) + needsClosingBrace = !lineBeyondCursor.match(/\w*}/) commandName = citeMatch[1] previousArgs = citeMatch[2] currentArg = citeMatch[3] @@ -59,8 +62,8 @@ define [ previousArgsCaption = if previousArgs.length > 8 then "…," else previousArgs result = [] result.push { - caption: "\\#{commandName}{", - snippet: "\\#{commandName}{", + caption: "\\#{commandName}{}", + snippet: "\\#{commandName}{}", meta: "reference", score: 11000 } @@ -68,8 +71,8 @@ define [ references.keys.forEach (key) -> if !(key in [null, undefined]) result.push({ - caption: "\\#{commandName}{#{previousArgsCaption}#{key}", - value: "\\#{commandName}{#{previousArgs}#{key}", + caption: "\\#{commandName}{#{previousArgsCaption}#{key}#{if needsClosingBrace then '}' else ''}", + value: "\\#{commandName}{#{previousArgs}#{key}#{if needsClosingBrace then '}' else ''}", meta: "reference", score: 10000 }) From 2985a0b0b212f670f3b9667d4d5bf703580f3a58 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 16 Mar 2016 11:42:30 +0000 Subject: [PATCH 041/208] Account for later commands on the same line. --- .../aceEditor/auto-complete/AutoCompleteManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index 862e420067..d2341f04f8 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -53,7 +53,7 @@ define [ if citeMatch beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999) lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange) - needsClosingBrace = !lineBeyondCursor.match(/\w*}/) + needsClosingBrace = !lineBeyondCursor.match(/^[^{]*}/) commandName = citeMatch[1] previousArgs = citeMatch[2] currentArg = citeMatch[3] From 7cc3f7613e148360be7bdc8a6c6d7ff26dc5491c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 17 Mar 2016 10:00:07 +0000 Subject: [PATCH 042/208] Move the `getCompletions` call for Snippets into the Snippets module. --- .../aceEditor/auto-complete/AutoCompleteManager.coffee | 6 ++---- .../directives/aceEditor/auto-complete/Snippets.coffee | 8 ++++++-- .../aceEditor/auto-complete/SuggestionManager.coffee | 3 +-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index d2341f04f8..27f5b15a3a 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -3,7 +3,7 @@ define [ "ide/editor/directives/aceEditor/auto-complete/Snippets" "ace/ace" "ace/ext-language_tools" -], (SuggestionManager, Snippets) -> +], (SuggestionManager, SnippetManager) -> Range = ace.require("ace/range").Range getLastCommandFragment = (lineUpToCursor) -> @@ -38,9 +38,7 @@ define [ enableLiveAutocompletion: false }) - SnippetCompleter = - getCompletions: (editor, session, pos, prefix, callback) -> - callback null, Snippets + SnippetCompleter = new SnippetManager() references = @$scope.$root._references ReferencesCompleter = diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/Snippets.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/Snippets.coffee index 4bc3d16ef4..a637005711 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/Snippets.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/Snippets.coffee @@ -1,6 +1,6 @@ define () -> environments = [ - "abstract", + "abstract", "align", "align*", "equation", "equation*", "gather", "gather*", @@ -96,5 +96,9 @@ define () -> """ meta: "env" }] + class SnippetManager + getCompletions: (editor, session, pos, prefix, callback) -> + console.log ">> get snippet completions" + callback null, snippets - return snippets \ No newline at end of file + return SnippetManager diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee index 559a2c5981..e0c3710358 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee @@ -28,7 +28,7 @@ define [], () -> # Ignore single letter commands since auto complete is moot then. commandRegex: /\\([a-zA-Z][a-zA-Z]+)/ - + nextCommand: () -> i = @doc.search(@commandRegex) if i == -1 @@ -123,4 +123,3 @@ define [], () -> completionBeforeCursor: completionBeforeCursor completionAfterCursor: completionAfterCursor } - From 115734f82e69f274678ae0084deeb3b0aec729cf Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 17 Mar 2016 10:04:14 +0000 Subject: [PATCH 043/208] Rename Snippets -> SnippetManager --- .../aceEditor/auto-complete/AutoCompleteManager.coffee | 2 +- .../{Snippets.coffee => SnippetManager.coffee} | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) rename services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/{Snippets.coffee => SnippetManager.coffee} (93%) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index 27f5b15a3a..32a60370c1 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -1,6 +1,6 @@ define [ "ide/editor/directives/aceEditor/auto-complete/SuggestionManager" - "ide/editor/directives/aceEditor/auto-complete/Snippets" + "ide/editor/directives/aceEditor/auto-complete/SnippetManager" "ace/ace" "ace/ext-language_tools" ], (SuggestionManager, SnippetManager) -> diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/Snippets.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee similarity index 93% rename from services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/Snippets.coffee rename to services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee index a637005711..5cf040f9d6 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/Snippets.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee @@ -9,7 +9,7 @@ define () -> "verbatim" ] - snippets = for env in environments + staticSnippets = for env in environments { caption: "\\begin{#{env}}..." snippet: """ @@ -20,7 +20,7 @@ define () -> meta: "env" } - snippets = snippets.concat [{ + staticSnippets = staticSnippets.concat [{ caption: "\\begin{array}..." snippet: """ \\begin{array}{${1:cc}} @@ -96,9 +96,10 @@ define () -> """ meta: "env" }] + class SnippetManager getCompletions: (editor, session, pos, prefix, callback) -> console.log ">> get snippet completions" - callback null, snippets + callback null, staticSnippets return SnippetManager From c25c2b676a4144733b8098d3d22cc2c1a7548a98 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 17 Mar 2016 11:14:05 +0000 Subject: [PATCH 044/208] Parse `\newenvironment{}` commands. --- .../auto-complete/SnippetManager.coffee | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee index 5cf040f9d6..d09c8d9013 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee @@ -97,9 +97,24 @@ define () -> meta: "env" }] + CUSTOM_ENVIRONMENT_REGEX = /^\\newenvironment{(\w+)}.*$/gm + + parseCustomEnvironmentNames = (text) -> + names = [] + iterations = 0 + while match = CUSTOM_ENVIRONMENT_REGEX.exec(text) + names.push match[1] + iterations += 1 + if iterations >= 1000 + return names + return names + class SnippetManager getCompletions: (editor, session, pos, prefix, callback) -> - console.log ">> get snippet completions" + # console.log ">> get snippet completions", editor, session, pos, prefix + docText = session.getValue() + customEnvironmentNames = parseCustomEnvironmentNames(docText) + # console.log customEnvironmentNames callback null, staticSnippets return SnippetManager From eebd25351db9e6057183b1f16fadd3d174a28282 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 17 Mar 2016 11:23:53 +0000 Subject: [PATCH 045/208] Add the custom environments to the list of snippets --- .../aceEditor/auto-complete/SnippetManager.coffee | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee index d09c8d9013..cc894ad39d 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee @@ -115,6 +115,19 @@ define () -> docText = session.getValue() customEnvironmentNames = parseCustomEnvironmentNames(docText) # console.log customEnvironmentNames - callback null, staticSnippets + snippets = staticSnippets.concat( + customEnvironmentNames.map (name) -> + { + caption: "\\begin{#{name}}..." + snippet: """ + \\begin{#{name}} + \t$1 + \\end{#{name}} + """ + meta: "env" + + } + ) + callback null, snippets return SnippetManager From d655e99439b227ecfede1a480651c2a6094d0364 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 17 Mar 2016 12:07:21 +0000 Subject: [PATCH 046/208] Parse existing `begin` commands too. --- .../auto-complete/SnippetManager.coffee | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee index cc894ad39d..438e83fd6b 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee @@ -109,14 +109,28 @@ define () -> return names return names + BEGIN_COMMAND_REGEX = /^\\begin{(\w+)}.*$/gm + + parseBeginCommandNames = (text) -> + names = [] + iterations = 0 + while match = BEGIN_COMMAND_REGEX.exec(text) + names.push match[1] + iterations += 1 + if iterations >= 1000 + return names + return names + class SnippetManager getCompletions: (editor, session, pos, prefix, callback) -> # console.log ">> get snippet completions", editor, session, pos, prefix docText = session.getValue() customEnvironmentNames = parseCustomEnvironmentNames(docText) + beginCommandNames = parseBeginCommandNames(docText) # console.log customEnvironmentNames + parsedNames = _.union(customEnvironmentNames, beginCommandNames) snippets = staticSnippets.concat( - customEnvironmentNames.map (name) -> + parsedNames.map (name) -> { caption: "\\begin{#{name}}..." snippet: """ @@ -125,7 +139,6 @@ define () -> \\end{#{name}} """ meta: "env" - } ) callback null, snippets From c980382196ee045332257d8149bda3d9dea10b02 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 17 Mar 2016 12:52:25 +0000 Subject: [PATCH 047/208] make calls to learn wiki run in parallel, only saves about 150ms but ever little helps --- .../Features/Wiki/WikiController.coffee | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/services/web/app/coffee/Features/Wiki/WikiController.coffee b/services/web/app/coffee/Features/Wiki/WikiController.coffee index 9016a4dd41..c75ee06f86 100644 --- a/services/web/app/coffee/Features/Wiki/WikiController.coffee +++ b/services/web/app/coffee/Features/Wiki/WikiController.coffee @@ -4,7 +4,7 @@ logger = require("logger-sharelatex") ErrorController = require "../Errors/ErrorController" _ = require("underscore") AuthenticationController = require("../Authentication/AuthenticationController") - +async = require("async") other_lngs = ["es"] module.exports = WikiController = @@ -28,19 +28,22 @@ module.exports = WikiController = lngPage = "#{page}_#{req.lng}" else lngPage = page - - WikiController._getPageContent "Contents", (error, contents) -> + jobs = + contents: (cb)-> + WikiController._getPageContent "Contents", cb + pageData: (cb)-> + WikiController._getPageContent lngPage, cb + async.parallel jobs, (error, results)-> return next(error) if error? - WikiController._getPageContent lngPage, (error, pageData) -> - return next(error) if error? - if pageData.content?.length > 280 - if _.include(other_lngs, req.lng) - pageData.title = pageData.title.slice(0, pageData.title.length - (req.lng.length+1) ) + {pageData, contents} = results + if pageData.content?.length > 280 + if _.include(other_lngs, req.lng) + pageData.title = pageData.title.slice(0, pageData.title.length - (req.lng.length+1) ) + WikiController._renderPage(pageData, contents, res) + else + WikiController._getPageContent page, (error, pageData) -> + return next(error) if error? WikiController._renderPage(pageData, contents, res) - else - WikiController._getPageContent page, (error, pageData) -> - return next(error) if error? - WikiController._renderPage(pageData, contents, res) @@ -62,7 +65,6 @@ module.exports = WikiController = result = content: data?.parse?.text?['*'] title: data?.parse?.title - callback null, result From 95a6e1900e913418c6872e2155dc8b9ac22de7e7 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 17 Mar 2016 14:09:45 +0000 Subject: [PATCH 048/208] Add autocomplete entries for the `end{}` commands --- .../aceEditor/auto-complete/SnippetManager.coffee | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee index 438e83fd6b..d4d8e4ca86 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee @@ -140,6 +140,16 @@ define () -> """ meta: "env" } + ).concat( + # arguably these `end` commands shouldn't be here, as they're not snippets + # but this is where we have access to the `begin` environment names + # *shrug* + parsedNames.map (name) -> + { + caption: "\\end{#{name}}" + value: "\\end{#{name}}" + meta: "env" + } ) callback null, snippets From 229ced6f2f51276ad76bac341a9e9304aadcfcaf Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 17 Mar 2016 14:43:34 +0000 Subject: [PATCH 049/208] Remove Indentation. --- .../directives/aceEditor/auto-complete/SnippetManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee index d4d8e4ca86..dce8f96d0e 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee @@ -135,7 +135,7 @@ define () -> caption: "\\begin{#{name}}..." snippet: """ \\begin{#{name}} - \t$1 + $1 \\end{#{name}} """ meta: "env" From 53b46e42cde5d6cf302849cbeb5c5eac836d897d Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 17 Mar 2016 15:27:20 +0000 Subject: [PATCH 050/208] Refactor, take indentation into account. --- .../auto-complete/SnippetManager.coffee | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee index dce8f96d0e..3b3d4baf3b 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee @@ -97,46 +97,44 @@ define () -> meta: "env" }] - CUSTOM_ENVIRONMENT_REGEX = /^\\newenvironment{(\w+)}.*$/gm - parseCustomEnvironmentNames = (text) -> - names = [] + parseCustomEnvironments = (text) -> + re = /^\\newenvironment{(\w+)}.*$/gm + result = [] iterations = 0 - while match = CUSTOM_ENVIRONMENT_REGEX.exec(text) - names.push match[1] + while match = re.exec(text) + result.push {name: match[1], whitespace: null} iterations += 1 if iterations >= 1000 - return names - return names + return result + return result - BEGIN_COMMAND_REGEX = /^\\begin{(\w+)}.*$/gm - parseBeginCommandNames = (text) -> - names = [] + parseBeginCommands = (text) -> + re = /^\\begin{(\w+)}.*\n([\t ]*).*$/gm + result = [] iterations = 0 - while match = BEGIN_COMMAND_REGEX.exec(text) - names.push match[1] + while match = re.exec(text) + result.push {name: match[1], whitespace: match[2]} iterations += 1 if iterations >= 1000 - return names - return names + return result + return result class SnippetManager getCompletions: (editor, session, pos, prefix, callback) -> - # console.log ">> get snippet completions", editor, session, pos, prefix docText = session.getValue() - customEnvironmentNames = parseCustomEnvironmentNames(docText) - beginCommandNames = parseBeginCommandNames(docText) - # console.log customEnvironmentNames - parsedNames = _.union(customEnvironmentNames, beginCommandNames) + customEnvironments = parseCustomEnvironments(docText) + beginCommands = parseBeginCommands(docText) + parsedItems = _.union(customEnvironments, beginCommands) snippets = staticSnippets.concat( - parsedNames.map (name) -> + parsedItems.map (item) -> { - caption: "\\begin{#{name}}..." + caption: "\\begin{#{item.name}}..." snippet: """ - \\begin{#{name}} - $1 - \\end{#{name}} + \\begin{#{item.name}} + #{item.whitespace || ''}$1 + \\end{#{item.name}} """ meta: "env" } @@ -144,10 +142,10 @@ define () -> # arguably these `end` commands shouldn't be here, as they're not snippets # but this is where we have access to the `begin` environment names # *shrug* - parsedNames.map (name) -> + parsedItems.map (item) -> { - caption: "\\end{#{name}}" - value: "\\end{#{name}}" + caption: "\\end{#{item.name}}" + value: "\\end{#{item.name}}" meta: "env" } ) From 020fd2e88dfa9411807cd05ebb6751a5779f0305 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 17 Mar 2016 15:58:57 +0000 Subject: [PATCH 051/208] De-dupe autocomplete entries --- .../aceEditor/auto-complete/SnippetManager.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee index 3b3d4baf3b..6dc605e8ea 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee @@ -126,7 +126,12 @@ define () -> docText = session.getValue() customEnvironments = parseCustomEnvironments(docText) beginCommands = parseBeginCommands(docText) - parsedItems = _.union(customEnvironments, beginCommands) + parsedItemsMap = {} + for environment in customEnvironments + parsedItemsMap[environment.name] = environment + for command in beginCommands + parsedItemsMap[command.name] = command + parsedItems = _.values(parsedItemsMap) snippets = staticSnippets.concat( parsedItems.map (item) -> { From 11d77df898d9ecb13da4347dee069ddb9a1bfb46 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 17 Mar 2016 16:18:46 +0000 Subject: [PATCH 052/208] Fix weird tab behaviour --- .../directives/aceEditor/auto-complete/SnippetManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee index 6dc605e8ea..7b7593565d 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee @@ -138,7 +138,7 @@ define () -> caption: "\\begin{#{item.name}}..." snippet: """ \\begin{#{item.name}} - #{item.whitespace || ''}$1 + #{item.whitespace || ''}$0 \\end{#{item.name}} """ meta: "env" From f3db1146547d5487fde52a19e5089ca604306977 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 18 Mar 2016 11:20:33 +0000 Subject: [PATCH 053/208] Use the mongojs based apis to get Project and User information. --- .../Features/References/ReferencesHandler.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index b64600fdb1..bd31345f87 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -1,8 +1,8 @@ logger = require("logger-sharelatex") request = require("request") settings = require("settings-sharelatex") -Project = require("../../models/Project").Project -User = require("../../models/User").User +ProjectGetter = require "../Project/ProjectGetter" +UserGetter = require "../User/UserGetter" DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') U = require('underscore') Async = require('async') @@ -32,12 +32,12 @@ module.exports = ReferencesHandler = return ids _isFullIndex: (project, callback = (err, result) ->) -> - User.findOne { _id: project.owner_ref }, { features: true }, (err, owner) -> + UserGetter.getUser project.owner_ref, { features: true }, (err, owner) -> return callback(err) if err? callback(null, owner?.features?.references == true) indexAll: (projectId, callback=(err, data)->) -> - Project.findOne { _id: projectId }, (err, project) -> + ProjectGetter.getProject projectId, {rootFolder: true, owner_ref: 1}, (err, project) -> if err logger.err {err, projectId}, "error finding project" return callback(err) @@ -46,7 +46,7 @@ module.exports = ReferencesHandler = ReferencesHandler._doIndexOperation(projectId, project, docIds, callback) index: (projectId, docIds, callback=(err, data)->) -> - Project.findOne { _id: projectId }, (err, project) -> + ProjectGetter.getProject projectId, {rootFolder: true, owner_ref: 1}, (err, project) -> if err logger.err {err, projectId}, "error finding project" return callback(err) From 209c8ebbfc4b78172454b88b115cee35dbc7fee9 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 18 Mar 2016 11:31:50 +0000 Subject: [PATCH 054/208] wip --- services/web/app/views/wiki/page.jade | 27 ++++++++++- .../coffee/analytics/AbTestingManager.coffee | 48 +++++++++++++++++-- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/wiki/page.jade b/services/web/app/views/wiki/page.jade index 054697cee9..e93206310a 100644 --- a/services/web/app/views/wiki/page.jade +++ b/services/web/app/views/wiki/page.jade @@ -1,9 +1,32 @@ extends ../layout block content - .content.content-alt(ng-non-bindable) + .content.content-alt(ng-cloak) .container.wiki - .row + .row.template-page-header(ng-controller="SearchWikiController") + .col-xs-3 + .col-md-8 + form.project-search.form-horizontal(role="form") + .form-group.has-feedback.has-feedback-left.col-md-12 + input.form-control.col-md-12(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....") + i.fa.fa-search.form-control-feedback-left + i.fa.fa-times.form-control-feedback( + ng-click="clearSearchText()", + style="cursor: pointer;", + ng-show="searchQueryText.length > 0" + ) + .col-md-8(ng-cloak) + ul.list-unstyled + li(ng-repeat='hit in hits') + .thumbnail.searchResult + .row + a(ng-href='{{hit.url}}') + .col-md-3 + img(ng-src='{{hit.image_url}}') + .col-md-7 + h1(ng-bind-html='hit.name') + p(ng-bind-html='hit.description') + .row(ng-non-bindable) .col-xs-3.contents | !{contents.content} .col-xs-9.page diff --git a/services/web/public/coffee/analytics/AbTestingManager.coffee b/services/web/public/coffee/analytics/AbTestingManager.coffee index 53524c8d5d..bc40f0ce6f 100644 --- a/services/web/public/coffee/analytics/AbTestingManager.coffee +++ b/services/web/public/coffee/analytics/AbTestingManager.coffee @@ -22,24 +22,20 @@ define [ _buildCookieKey = (testName, bucket)-> key = "sl_abt_#{testName}_#{bucket}" - #console.log key return key _getTestCookie = (testName, bucket)-> cookieKey = _buildCookieKey(testName, bucket) cookie = ipCookie(cookieKey) - #console.log cookieKey, cookie return cookie _persistCookieStep = (testName, bucket, newStep)-> cookieKey = _buildCookieKey(testName, bucket) ipCookie(cookieKey, {step:newStep}, {expires:100, path:"/"}) - #console.log("persisting", cookieKey, {step:newStep}) ga('send', 'event', 'ab_tests', "#{testName}:#{bucket}", "step-#{newStep}") _checkIfStepIsNext = (cookieStep, newStep)-> - #console.log cookieStep, newStep, "checking if step is next" if !cookieStep? and newStep != 0 return false else if newStep == 0 @@ -69,6 +65,50 @@ define [ return buckets[bucketIndex] + App.factory "algoliawiki", -> + client = new AlgoliaSearch("SK53GL4JLY", "e398f35d3074fde57ca6d6c88d8be37c") + index = client.initIndex("lean-wiki-index") + return index + + App.controller "SearchWikiController", ($scope, algoliawiki, _) -> + algolia = algoliawiki + $scope.hits = [] + + $scope.clearSearchText = -> + $scope.searchQueryText = "" + updateHits [] + + $scope.safeApply = (fn)-> + phase = $scope.$root.$$phase + if(phase == '$apply' || phase == '$digest') + $scope.$eval(fn) + else + $scope.$apply(fn) + + buildHitViewModel = (hit)-> + page_underscored = hit.title.replace(/\s/g,'_') + result = + name : hit._highlightResult.title.value + url :"/learn/#{page_underscored}" + console.log result + return result + + updateHits = (hits)-> + $scope.safeApply -> + $scope.hits = hits + + $scope.search = -> + query = $scope.searchQueryText + if !query? or query.length == 0 + updateHits [] + return + + algolia.search query, (err, response)-> + if response.hits.length == 0 + updateHits [] + else + hits = _.map response.hits, buildHitViewModel + updateHits hits App.controller "AbTestController", ($scope, abTestManager)-> testKeys = _.keys(window.ab) From 35b5d3cc0581961f3c3721f4bda528b7bc5dcd86 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 18 Mar 2016 11:43:39 +0000 Subject: [PATCH 055/208] Update ReferencesHandlerTests. --- .../References/ReferencesHandlerTests.coffee | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee index 1666a2def1..53e064d821 100644 --- a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee @@ -39,13 +39,11 @@ describe 'ReferencesHandler', -> get: sinon.stub() post: sinon.stub() } - '../../models/Project': { - Project: @Project = { - findOne: sinon.stub().callsArgWith(1, null, @fakeProject) - } + '../Project/ProjectGetter': @ProjectGetter = { + getProject: sinon.stub().callsArgWith(2, null, @fakeProject) } - '../../models/User': { - User: @User = {} + '../User/UserGetter': @UserGetter = { + getUser: sinon.stub() } '../DocumentUpdater/DocumentUpdaterHandler': @DocumentUpdaterHandler = { flushDocToMongo: sinon.stub().callsArgWith(2, null) @@ -73,10 +71,10 @@ describe 'ReferencesHandler', -> @handler._findBibDocIds.callCount.should.equal 0 done() - it 'should call Project.findOne', (done) -> + it 'should call ProjectGetter.getProject', (done) -> @call (err, data) => - @Project.findOne.callCount.should.equal 1 - @Project.findOne.calledWith(_id: @projectId).should.equal true + @ProjectGetter.getProject.callCount.should.equal 1 + @ProjectGetter.getProject.calledWith(@projectId).should.equal true done() it 'should not call _findBibDocIds', (done) -> @@ -112,10 +110,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.findOne produces an error', -> + describe 'when ProjectGetter.getProject produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, new Error('woops')) + @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -132,7 +130,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -150,7 +148,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) @@ -170,7 +168,7 @@ describe 'ReferencesHandler', -> describe 'when request produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, null, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, null) @request.post.callsArgWith(1, new Error('woops')) @@ -185,7 +183,7 @@ describe 'ReferencesHandler', -> describe 'when request responds with error status', -> beforeEach -> - @Project.findOne.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, null, false) @request.post.callsArgWith(1, null, {statusCode: 500}, null) @@ -237,10 +235,10 @@ describe 'ReferencesHandler', -> expect(data).to.equal @fakeResponseData done() - describe 'when Project.findOne produces an error', -> + describe 'when ProjectGetter.getProject produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, new Error('woops')) + @ProjectGetter.getProject.callsArgWith(2, new Error('woops')) it 'should produce an error', (done) -> @call (err, data) => @@ -257,7 +255,7 @@ describe 'ReferencesHandler', -> describe 'when _isFullIndex produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, new Error('woops')) it 'should produce an error', (done) -> @@ -275,7 +273,7 @@ describe 'ReferencesHandler', -> describe 'when flushDocToMongo produces an error', -> beforeEach -> - @Project.findOne.callsArgWith(1, null, @fakeProject) + @ProjectGetter.getProject.callsArgWith(2, null, @fakeProject) @handler._isFullIndex.callsArgWith(1, false) @DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops')) @@ -319,8 +317,8 @@ describe 'ReferencesHandler', -> @owner = features: references: false - @User.findOne = sinon.stub() - @User.findOne.withArgs({_id: @owner_ref}, {features: true}).yields(null, @owner) + @UserGetter.getUser = sinon.stub() + @UserGetter.getUser.withArgs(@owner_ref, {features: true}).yields(null, @owner) @call = (callback) => @handler._isFullIndex @fakeProject, callback From 882297c3530297970f5ff76c9cd8cb38270c4ccb Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 18 Mar 2016 12:55:35 +0000 Subject: [PATCH 056/208] working framework. not happy with groove. Can't set mailbox --- services/web/app/views/contact-us-modal.jade | 31 +++++++++ services/web/app/views/layout.jade | 14 +++-- services/web/app/views/layout/navbar.jade | 2 +- services/web/public/coffee/base.coffee | 9 --- services/web/public/coffee/main.coffee | 2 +- .../web/public/coffee/main/contact-us.coffee | 63 +++++++++++++++++++ .../coffee/main/universties-site.coffee | 26 -------- 7 files changed, 106 insertions(+), 41 deletions(-) create mode 100644 services/web/app/views/contact-us-modal.jade create mode 100644 services/web/public/coffee/main/contact-us.coffee delete mode 100644 services/web/public/coffee/main/universties-site.coffee diff --git a/services/web/app/views/contact-us-modal.jade b/services/web/app/views/contact-us-modal.jade new file mode 100644 index 0000000000..f4b03ece2d --- /dev/null +++ b/services/web/app/views/contact-us-modal.jade @@ -0,0 +1,31 @@ +script(type='text/ng-template', id='supportModalTemplate') + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="close()" + ) × + h3 Contact Us + .modal-body + span(ng-show="sent == false") + .form-group + label + | Subject + .form-group + input.field.text.medium.span8.form-control(ng-model="form.subject", maxlength='255', tabindex='1', onkeyup='') + label.desc + | Email + .form-group + input.field.text.medium.span8.form-control(ng-model="form.email", type='email', spellcheck='false', value='', maxlength='255', tabindex='2') + label#title12.desc + | Project URL (optional) + .form-group + input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='') + label.desc + | Message + .form-group + textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', maxlength='255', tabindex='4', onkeyup='') + .form-group.text-center + input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value='Get in Touch') + span(ng-show="sent") + p Request Sent, Thank you. \ No newline at end of file diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index 2d3d13c227..a0e8383a7b 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -65,10 +65,6 @@ html(itemscope, itemtype='http://schema.org/Product') window.ab = {}; window.user_id = '#{getLoggedInUserId()}'; - //- script. - //- (function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true; s.src=('https:'==document.location.protocol?'https':'http') + '://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket.js'; var q = document.getElementsByTagName('script')[0];q.parentNode.insertBefore(s, q);})(); - - - if (typeof(settings.algolia) != "undefined") script. window.sharelatex.algolia = { @@ -86,6 +82,9 @@ html(itemscope, itemtype='http://schema.org/Product') } body + div(ng-controller="ContactModal") + a(ng-click="contactUsModal()") contact us + - if(typeof(suppressSystemMessages) == "undefined") .system-messages( ng-cloak @@ -131,5 +130,12 @@ html(itemscope, itemtype='http://schema.org/Product') src=jsPath+'libs/require.js?fingerprint='+fingerprint(jsPath + 'libs/require.js') ) + script(type='text/javascript'). + (function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true; + s.src=('https:'==document.location.protocol?'https':'http') + '://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket/api.js'; + var q = document.getElementsByTagName('script')[0];q.parentNode.insertBefore(s, q);})(); + + include contact-us-modal + diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.jade index 5f8538cb31..89ff4e1dc2 100644 --- a/services/web/app/views/layout/navbar.jade +++ b/services/web/app/views/layout/navbar.jade @@ -27,7 +27,7 @@ nav.navbar.navbar-default a.dropdown-toggle(href, dropdown-toggle) | !{translate(item.text)} b.caret - ul.dropdown-menu(ng-controller="NavController") + ul.dropdown-menu each child in item.dropdown if child.divider li.divider diff --git a/services/web/public/coffee/base.coffee b/services/web/public/coffee/base.coffee index bc040a16cc..98cdb3a871 100644 --- a/services/web/public/coffee/base.coffee +++ b/services/web/public/coffee/base.coffee @@ -24,13 +24,4 @@ define [ client_id: window.user_id }) - App.controller "NavController", ($scope) -> - - $scope.toggleGroove = -> - $scope['is-open'] = false - $scope['isOpen'] = false - GrooveWidget.toggle() - - - return App diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index 1d59eb19c8..40c6bd753d 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -15,7 +15,7 @@ define [ "main/annual-upgrade" "main/register-users" "main/subscription/group-subscription-invite-controller" - "main/universties-site" + "main/contact-us" "analytics/AbTestingManager" "directives/asyncForm" "directives/stopPropagation" diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee new file mode 100644 index 0000000000..5519e2aa5a --- /dev/null +++ b/services/web/public/coffee/main/contact-us.coffee @@ -0,0 +1,63 @@ +define [ + "base" +], (App) -> + + + App.controller 'ContactModal', ($scope, $modal) -> + $scope.contactUsModal = () -> + modalInstance = $modal.open( + templateUrl: "supportModalTemplate" + controller: "SupportModalController" + ) + + App.controller 'SupportModalController', ($scope, $modal) -> + + $scope.form = {} + $scope.sent = false + $scope.sending = false + $scope.contactUs = -> + if !$scope.form.email? + console.log "email not set" + return + $scope.sending = true + ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32) + params = + email: $scope.form.email + message: $scope.form.message + subject: $scope.form.subject + " - [#{ticketNumber}]" + about : $scope.form.project_url + + Groove.createTicket params, (err, json)-> + $scope.sent = true + $scope.$apply() + + + $scope.close = () -> + $modalInstance.close() + + + + + App.controller 'UniverstiesContactController', ($scope, $modal) -> + + $scope.form = {} + $scope.sent = false + $scope.sending = false + $scope.contactUs = -> + if !$scope.form.email? + console.log "email not set" + return + $scope.sending = true + ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32) + params = + name: $scope.form.name || $scope.form.email + email: $scope.form.email + labels: $scope.form.source + message: "Please contact me with more details" + subject: $scope.form.subject + " - [#{ticketNumber}]" + about : "#{$scope.form.position || ''} #{$scope.form.university || ''}" + to: "support@sharelatex.com" + + Groove.createTicket params, (err, json)-> + $scope.sent = true + $scope.$apply() diff --git a/services/web/public/coffee/main/universties-site.coffee b/services/web/public/coffee/main/universties-site.coffee deleted file mode 100644 index c62f4adc50..0000000000 --- a/services/web/public/coffee/main/universties-site.coffee +++ /dev/null @@ -1,26 +0,0 @@ -define [ - "base" -], (App) -> - - App.controller 'UniverstiesContactController', ($scope, $modal) -> - - $scope.form = {} - $scope.sent = false - $scope.sending = false - $scope.contactUs = -> - if !$scope.form.email? - console.log "email not set" - return - $scope.sending = true - ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32) - params = - name: $scope.form.name || $scope.form.email - email: $scope.form.email - labels: $scope.form.source - message: "Please contact me with more details" - subject: $scope.form.subject + " - [#{ticketNumber}]" - about : "#{$scope.form.position || ''} #{$scope.form.university || ''}" - - Groove.createTicket params, (err, json)-> - $scope.sent = true - $scope.$apply() From 88b8ce1f80bd0a47884945d1da8cde4410718421 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 18 Mar 2016 12:23:13 +0000 Subject: [PATCH 057/208] Enable working settings acceptance tests --- .../web/test/acceptance/coffee/AuthorizationTests.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index bed1a56f29..0cacffb5cf 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -300,8 +300,8 @@ describe "Authorization", -> it "should allow a user write access to its content", (done) -> expect_content_write_access @other1, @project_id, done - it "should not allow a user write access to its settings"#, (done) -> - # expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done + it "should not allow a user write access to its settings", (done) -> + expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow a user admin access to it", (done) -> expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done @@ -331,8 +331,8 @@ describe "Authorization", -> it "should not allow a user write access to its content", (done) -> expect_no_content_write_access @other1, @project_id, done - it "should not allow a user write access to its settings"#, (done) -> - # expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done + it "should not allow a user write access to its settings", (done) -> + expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done it "should not allow a user admin access to it", (done) -> expect_no_admin_access @other1, @project_id, redirect_to: "/restricted", done From e7d67668e9a673bd4eee987cdb33900730ce3357 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 18 Mar 2016 15:59:03 +0000 Subject: [PATCH 058/208] Improve error reporting and show 404 when project ids are malformed --- services/web/app.coffee | 12 --- .../Authorization/AuthorizationManager.coffee | 5 +- .../AuthorizationMiddlewear.coffee | 4 + .../Collaborators/CollaboratorsHandler.coffee | 3 +- .../Features/Errors/ErrorController.coffee | 23 ++++- .../app/coffee/Features/Errors/Errors.coffee | 9 ++ .../app/coffee/infrastructure/Server.coffee | 4 + services/web/app/views/general/500.jade | 16 ++++ services/web/public/img/lion-sad-128.png | Bin 0 -> 14098 bytes .../AuthorizationManagerTests.coffee | 12 +++ .../AuthorizationMiddlewearTests.coffee | 16 ++++ .../CollaboratorsHandlerTests.coffee | 48 ++++++---- .../coffee/AuthorizationTests.coffee | 83 +----------------- .../acceptance/coffee/ProjectCRUDTests.coffee | 21 +++++ .../acceptance/coffee/helpers/User.coffee | 74 ++++++++++++++++ .../acceptance/coffee/helpers/request.coffee | 5 ++ 16 files changed, 222 insertions(+), 113 deletions(-) create mode 100644 services/web/app/coffee/Features/Errors/Errors.coffee create mode 100644 services/web/app/views/general/500.jade create mode 100644 services/web/public/img/lion-sad-128.png create mode 100644 services/web/test/acceptance/coffee/ProjectCRUDTests.coffee create mode 100644 services/web/test/acceptance/coffee/helpers/User.coffee create mode 100644 services/web/test/acceptance/coffee/helpers/request.coffee diff --git a/services/web/app.coffee b/services/web/app.coffee index 3d45307e5d..8496af8f17 100644 --- a/services/web/app.coffee +++ b/services/web/app.coffee @@ -18,18 +18,6 @@ argv = require("optimist") .usage("Usage: $0") .argv -Server.app.use (error, req, res, next) -> - if error?.code is 'EBADCSRFTOKEN' - logger.log err: error,url:req.url, method:req.method, user:req?.sesson?.user, "invalid csrf" - res.sendStatus(403) - return - logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear" - res.statusCode = error.status or 500 - if res.statusCode == 500 - res.end("Oops, something went wrong with your request, sorry. If this continues, please contact us at #{Settings.adminEmail}") - else - res.end() - if Settings.catchErrors process.removeAllListeners "uncaughtException" process.on "uncaughtException", (error) -> diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee index db49881bbf..eb6f564fea 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee @@ -3,6 +3,7 @@ Project = require("../../models/Project").Project User = require("../../models/User").User PrivilegeLevels = require("./PrivilegeLevels") PublicAccessLevels = require("./PublicAccessLevels") +Errors = require("../Errors/Errors") module.exports = AuthorizationManager = # Get the privilege level that the user has for the project @@ -14,8 +15,10 @@ module.exports = AuthorizationManager = getPublicAccessLevel = () -> Project.findOne { _id: project_id }, { publicAccesLevel: 1 }, (error, project) -> return callback(error) if error? + if !project? + return callback new Errors.NotFoundError("no project found with id #{project_id}") if project.publicAccesLevel == PublicAccessLevels.READ_ONLY - return callback null, PrivilegeLevels.READ_ONLY + return callback null, PrivilegeLevels.READ_ONLY, true else if project.publicAccesLevel == PublicAccessLevels.READ_AND_WRITE return callback null, PrivilegeLevels.READ_AND_WRITE, true else diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee index 2db1632ecf..4888db0c8a 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee @@ -1,6 +1,8 @@ AuthorizationManager = require("./AuthorizationManager") async = require "async" logger = require "logger-sharelatex" +ObjectId = require("mongojs").ObjectId +Errors = require "../Errors/Errors" module.exports = AuthorizationMiddlewear = ensureUserCanReadMultipleProjects: (req, res, next) -> @@ -83,6 +85,8 @@ module.exports = AuthorizationMiddlewear = project_id = req.params?.project_id or req.params?.Project_id if !project_id? return callback(new Error("Expected project_id in request parameters")) + if !ObjectId.isValid(project_id) + return callback(new Errors.NotFoundError("invalid project_id: #{project_id}")) AuthorizationMiddlewear._getUserId req, (error, user_id) -> return callback(error) if error? callback(null, user_id, project_id) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index cb6459c85f..71737eecff 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -7,12 +7,13 @@ ContactManager = require "../Contacts/ContactManager" CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" async = require "async" PrivilegeLevels = require "../Authorization/PrivilegeLevels" +Errors = require "../Errors/Errors" module.exports = CollaboratorsHandler = getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) -> return callback(error) if error? - return callback null, null if !project? + return callback new Errors.NotFoundError("no project found with id #{project_id}") if !project? members = [] members.push { id: project.owner_ref.toString(), privilegeLevel: PrivilegeLevels.OWNER } for member_id in project.readOnly_refs or [] diff --git a/services/web/app/coffee/Features/Errors/ErrorController.coffee b/services/web/app/coffee/Features/Errors/ErrorController.coffee index d0589ba5ed..a0ce6c85c9 100644 --- a/services/web/app/coffee/Features/Errors/ErrorController.coffee +++ b/services/web/app/coffee/Features/Errors/ErrorController.coffee @@ -1,5 +1,24 @@ +Errors = require "./Errors" +logger = require "logger-sharelatex" + module.exports = ErrorController = notFound: (req, res)-> - res.statusCode = 404 + res.status(404) res.render 'general/404', - title: "page_not_found" \ No newline at end of file + title: "page_not_found" + + serverError: (req, res)-> + res.status(500) + res.render 'general/500', + title: "Server Error" + + handleError: (error, req, res, next) -> + if error?.code is 'EBADCSRFTOKEN' + logger.log err: error,url:req.url, method:req.method, user:req?.sesson?.user, "invalid csrf" + res.sendStatus(403) + return + logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear" + if error instanceof Errors.NotFoundError + ErrorController.notFound req, res + else + ErrorController.serverError req, res \ No newline at end of file diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee new file mode 100644 index 0000000000..0bbff1f19b --- /dev/null +++ b/services/web/app/coffee/Features/Errors/Errors.coffee @@ -0,0 +1,9 @@ +NotFoundError = (message) -> + error = new Error(message) + error.name = "NotFoundError" + error.__proto__ = NotFoundError.prototype + return error +NotFoundError.prototype.__proto__ = Error.prototype + +module.exports = Errors = + NotFoundError: NotFoundError \ No newline at end of file diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index 4a035ba007..fea8752bb2 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -30,6 +30,8 @@ OldAssetProxy = require("./OldAssetProxy") translations = require("translations-sharelatex").setup(Settings.i18n) Modules = require "./Modules" +ErrorController = require "../Features/Errors/ErrorController" + 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) @@ -136,6 +138,8 @@ app.use(webRouter) router = new Router(webRouter, apiRouter) +app.use ErrorController.handleError + module.exports = app: app server: server diff --git a/services/web/app/views/general/500.jade b/services/web/app/views/general/500.jade new file mode 100644 index 0000000000..12983b923b --- /dev/null +++ b/services/web/app/views/general/500.jade @@ -0,0 +1,16 @@ +extends ../layout + +block content + .content + .container + .row + .col-md-8.col-md-offset-2.text-center + .page-header + h2 Oh dear, something went wrong. + p: img(src="/img/lion-sad-128.png", alt="Sad Lion") + p + | Something went wrong with your request, sorry. Our staff are probably looking into this, but it continues, please contact us at #{settings.adminEmail} + p + a(href="/") + i.fa.fa-arrow-circle-o-left + | #{translate("take_me_home")} diff --git a/services/web/public/img/lion-sad-128.png b/services/web/public/img/lion-sad-128.png new file mode 100644 index 0000000000000000000000000000000000000000..e5e58c42b0e574cb98c55f4b141cad01eff6f6d4 GIT binary patch literal 14098 zcmZ{L1ymf%w)WsKxVyU!?iM__yK8WFcXxLJ0fI{i?oM!r;O_4J=bU@rd*{FFy;-ZP zyLNqFeS7b!-D~wsPo$!}Bq9(G2mk;OKTC-z|2YTzb-+UZY5N3?M*p0^oRuX-0M!!& zM}InS4pLgq002DtUk4Z+EW5WoF{?kM;VqYJcy35hDxGKg|4~{}s*uXD{%|*_&FLdx{ykmw@6MBBxzkcviRPw!C**59XF?x5{yZPF53And6U}S{ z+Yc$KDRk(|-pAM3%n>pl#2R{#6B5_}r@#!Dx@lbrDZkfV` zl2Qb0bdv38GoMJ3$^VR?j|;tW1wm3%MeL_QjPmbky8t<1d%NJa-!&w2Y&em( zhw^_BM{WAOVAweI%JCKff@_c4G|b20B43;Xl=hDa+eG!B}}IK|qnOemwn+?pbvLDj)MV*sX9VXq?%y zzR&vD$WpZC57_YVe8Y1-AO$6su`QCKPv5v=E?f}AzVj05#B)|mX_(kDO;AI`wavNa z5|Gc=4sc!$DgIs`2U)auf7Ojd7&B!P?yjs|azZW4#~c^Aus^G>&VQ zvZ8<-_zgblG-*2t?JqHGLDWuHO5yRyN=?4w!)`@&T^d+T5Q1;0xX@c~17`-A25Pt6 zyNBIRMwiY5(OMl?Bagj6(tS7==pck)h)pHcIapPj{XA)&biq7WYueq%%$0pVM^w&Q zPQ5}V9eSc?Zpg4~w0MD9x5wM+pu=fkDrW$+_mCn;TYoAQ?-SA|o9UQ}x?-cQE+f!$ zLr)q^4r30I3izT~0MR#A1M#){?Z)1qqlY5y)jlK$wfyDh#dz+C)F$f(hYv+V8 zf>7~hi#PZazV2ynfNnMA4s23H?+C2e2q4r)1)$P$vA6+^_<|05z%jRda7&`!XHC5z z?I@mT=#fwzXLt1FEFlh^ur(=z*`$4UckfG;{uN63lbyQ$g&ww#n{T{%%%%cEq-6zO zjQi9{7j%iQUS^4IKe9Kk)%^E8W*wvY1_3=ny?W;zCQH8GgPxgNJlDAD+@Zbk#e*a_ z4%(|&G>#jE{{DFKb+-KyG&4zV#rn$Nh_>LRB?WL=hR3k%1?2H%;j3+sHpy3FcNsGE+^%V+W;gu z-WkL!wy&^Io|%d+#?0D)84$f=+-h`4m(j5lcm8{EY3pL=55t`7=@-u0Y&iS~N^)(5 zTdNDM(32AR4Bo-EEsa0G%n+5FKfgZkxTId7sBULxy;9iZWv^gVO%9 zNTby|xP-(%F{0-J3{tXRDRFjR|E(0h)KF>zAqxB#!JJEFm82KXW#95IIoaeC2$-hR zw74BxeQ*I0l;6)0%e`ZQk@6R)wYG^R1D0|WY;*TY3 zRt;)G%QF!i!ZfnrMoWHU_RI|&Hor*)a@VL3k8+S*jnFD5=x0~R{f*FOUhtLr*yn}c<4jqW2D>JN}s zGMv$_qE)m8PFGMBZIQuB*M0hnV+iz_nGV0b@{c_84A|gscP?ozoJul@pmFte()kFYC~%q2&}OF4&zPM- z-a?tnnaXVIu&$E}9d!Kuoro4i)bd#zs0;l~^tNmQ`O*W?(u+>t)EcY36`Z+?S<*&m zXjdkpe@$TATjNFEo7gI|bgBb?MI*NF(?>CP5Qt>J;lazvd-;=dQWM@ODys+KamGYe zl_Q$B?oF|v*m`q~;%ai#m|`%I`>_2=Q$Kpk*HnEQEQ3Y2kWaWeaA2d#I-Mz3&h(q+ zfdC3&u~ffo`AoE@D`yQIZB&0%2SkYbNV zVrW<$D$0LBu8p~5qlHEH{c-?irXCWh3dyP}bLba{%IA(WLLyoI%<>kTsHmkuyHgSi zA3PsZlla42q1U5^k9F%-44e{kU>!OIl|x6yS~x#W(3yKg#R4rvrndvXNk(!sl9}NK z5yKA*8Oeb)#VuVOO85lXlU;@iX;RchDb6!?<=)t)M9%_ic+mc|_(+QV=uy0a=Lo;N z;yYvdZ#WJs#6j6=d^$W_jz($xfhRt*O6)q%EoM@wLgP)O5;}ySjO^RtcXzjP)>?um zlTjAp%gG*@Hklgm=JT3VIFjJr$-(9`_o3$99WP=6lIcVP^eEW87GQ~k+Tnngw2Dgj zy^6bf?i3!vklBIhXZ^%XhUaj)B9sEKaZc&2vd&1HiomGPdtV7*ZUw{rp}IMHhO2Pp zSI`)<7e135en3b)5tNa_d!UDm5(3k7I>y`ZQa{-+c#7gg{nin-%8k-v5anjO4Aahhhs75e zz}CQQh>4eGt=a+!4rXb|8>QDnvJWLNgd(>Q;YT=o(dC^V}w$bUt z8)mPn>3%DYER}xS$@z!re#|thnbXg8t(e_gVqPSu-?w(b?Oh$Tz%PCH-fCa>$ExF% zE3c20m&<@CP(*RH<1XJ~@%+H)1E($GQ-KUz@#pqwMTl}}u%7O`n94@}PCfN^S6qbr z8LFNmlRin`pYrq9HtsYu=JtcnWj=p&p2gPh6-EBM-TZQuwd#U?Un3`Hn6GU;iR* z9NFrlZkUio(0Y1WUV<#-ai7Pt*_ejp*HYH9RJYXnUg2o}6pONtrVQbO}8T`(Wv0E-U)L$&yPJM>F7<(@}<cF#>ijC@Ps#YC3Iq*O-E*m4?nr%THWMaM^9`ks< z`lq6Ln9>xEQxSwjXFR9fO6Nf-r2A6^@u8>QK!Ne!>@_2UXdXb@FQSc-X)LG`a6w-c z=PPz{O-9{YDkQTS4|6lw=igI9gV<&QOC8ePFq*`Rm|!#a6Dz0ISV4A%F-NAY!6SN( zmLFVJGcztl-f&-;XhCA59l3%80lR4&Raiufi)MCGvrq%wqsxJdsMF0A-iYsYtl=`vR1Vkaw}`7w68f&{fZ<;P|yx)TO> zP<=#VvM zYS+(-u1+8Zab@#1!bLuIY0s1XV0voS^&YY}J^s0oWDDbwFiTSjJ*GgcN&tO0HU5YCbJEZa`}UU?Zyl$|A*(M1I|_k@bY1TWV1u~ZBbei& zg$B3`$tl*uJ4*(N87HZ+cL>VmJ=2W|?NNIkS99^hOHfV2=fTvbLp_JK3rjOs+u8Sb z!-?dQ%ooE+W97Klm+-b$sP}|n0>a`1Y1=Kvl?R?FF}?HtC>CFuz#ET<)~KvPE<#1o zo<;aqkEEj&J_D`Wyua#hLg&= z3!rUovK8wdNN8$fwMXRct|l*;{>P9cM)(kpopu7~4s&{i3RNv)6GzX$=&-v!KBUgo z%|j*Y%0%17C(R|Xgc&bvCM2iWU5^YtGu;k5KCEHxDHSSk=#nf#r!aA-g6Pz!zA0N- zHHSJHl#i-s6QGGKzj$tkB>gFl{HNJ8IDUcVYUO@f0)YT(5(|otzFT|aAR|?m|GDid zPs60U@~U(KxFlyVIS>UIUe!Y7-TJ)JY%12{sZ1K5C>+gZV1og{2mOWHfOUV?gyKnL z@UiGOS3)JfDDjhZCd-?ua_jPXo#@+yzozGnDmZlI?QE0=lNp+qXbAvYm$uiKIg=c=vB)2iudo7id zlF;wly^A5t1RwUV`NMGqm7CARk>TIrZ=WNc;zMO9I_)QomTR#lIq$(Fq))$1K`7_Z zv@;g_^z^}e<$0(|IrCL4rXj7u+Pr^KM(WvdC{i&UsoyDd8v2!&sjqne28@Axw8&Qp zfBX0fA0pG_?Y~BLae9Fh%+s6>5h!DC?sNl%rkZjGwoOl4kDn5C;9(>m z(g@{1mn87ynH3 z&N>L+F+~`daOrFCS+ypvHW}w)92VBpQThU?V)k%HA$DF;C>44z+Z@p^sDZ|qyb$3cX zKNIG>?RfIS+;BFX64o{1a<%0-UilWYC ztSMSoPK7Cf7J*wi82v|?%Ym`;LH0+L*!p0|WhmDb5Ks15kXb+yD?gdMNE)4h9L+{i zNvYcIT&x>W?%VnfLcPz}CBTy8>cc(a-SeBxvq)b(plLicK0HVs0@e!`2H!5URx(*u zU)A>P3@>uQ3Et$b7D^a=Rx=Jp7@BWz+$zbnWofGAveJgfzA$soIUF?KYmwa?C@$$W zhCIJERbpBG9-)&Zut5`WWAU*g^7$MPVyy6Pp$WX)Jh`U$-nM*itX=~thH5fXn379C z665}qJN~ym|Qz#c`%);-=3rLuCkYl?CmLFb%vm z5VUDi+!nPwu0KqnBH+$;}ac z%yXiHn%eFaNa*wkgHwGh!*Ax?B$~8OrXZBrA5{~u%lqVH-FhS!30|9sT%EXg*mJP; z)=Y0xqe0)D7wg0J!UZ{lpovp63lUZUp=*UF_Uo7Y1!(~PmWc_S$CQ{?8Mwp}EUA_9 zFu{!6Gh$6;{O4`Grio;h{#2%PRwzRVVo$w$!FKtyfM$Bft@W_E_PneSMC{7m*j*Fk)=+nOI<52-$ zG0+hIaoEdqE7A3OOEFEhd#&k2czRJ7c>;WjHuaSwPH7`wPH>Gn@LN$m8W6CdK(8T* zQ7gypX#|TEe8W&2wiaENeg7SL_A(g@s`kvsCH2(bCHNJ|o#k?kF;n(zfhT~q`{W7G zh;WsZ`VQdQQ*g%iLrE2(&TYRe>R8nl-gTT4eV%}g*+Z-fyxv03&!-oXf#D15KxNk~ z5_Avr0Z;h|p7%I$hGnivV8g+&AktDx?8dq@yvw6|^7Qa%=pYOirO`X7v^%LXqQIN+ z4_dFo&HwrYYpRbNx@mIf;haV=_@%CABq9IzcG>N0IRK|^Fem`#twFMP5N{Jg;TK?( zbpK?nK~>ok%@GrGPZ+Fq8ho~*Y_kD;kgC>5;u&^T{l};BG`I)#SEgPTf2;CdOG0qr z-xgDfz7ct*kDu#|va6Ft;u$^H-~2n(azN>v=@o(s7nj>B!k!xfuy)tm$E2)*tgg$v zg>*y_&RgIRV3g^l3eeuY7JLoB$x^bA6$a&XW=X+Zg8MDeQZ+E)GwicgAqGPfL@n`# zK*H|9qrNN31eak}Z15bk%i>?0a?TmhIHBgy&g1|*Q!9#sx_5sJ$&Y2x5n84ZwP)2O zvCV0m`%J7dcQIn1(Y&P!Ambym;HN5_8rY5`<+v3hu`F_}W0ustF~&7g;P-CQ+km|U zAht9hUm*Gj;U?TMjN=zYaO0%C4mCdd*&~9m3=E^Rix-`-8H>*EUqF($Yt$=@-D`!( zo%C6WiDFx%03w~Q0=}>fmuvjTG>T5rP$!Y9ePeg5-ywlEen=u27h6izSU$-o-14{Q zmgn;j1#8@l1e3m7S4CGzp+jTV4&k-c{GuM*DerTwvhBwc20E*SN|BMPGxwOoj;&@$ z+mp{TGkCJu;0UGjTI)J|mSC!_r=+`lK7D>Zu8`nsp%8iAj*zc<{J&grO1+V{8){tJ zFD6a4ZmHvbrS$6h6hdi`%735PatV_l9vWkF_^GI+ZNk6s1qWG|+0l;psszz%^cptGxrgZT>o{qO=2i4yw*0i_4 zgR(>vN_Q;v@8@_>OnD|y^7O*FJho`ueGV*9Sw*?~QM!NA8GkmGF7UI~_K3=u#Hsfq zA}=izxbq4}!fk+spP0ZB3}i=iwNAEe3k+K6`P|wGB9}$`#Jc&piAia93CphCEH2Mf z?)aOyJ`vGyKYjx@-+uFY6w%WpsHtqzvoKx23h8{$K#*S2o0#}FP83_O#penP!Lnsj zWEO9MNx0`pU}LZLX^@B_i+a~0BR!{U*+7epdG%Phh^7W)vFzYiOxKZaWNc1b;3mhZ z(B?*hEIE&P{f(AJ%E2Mdgnp-sTlJWj!j8pW*#2wA8&1;D5R8I(++Hf>;^cTTbPki3 ztxS6RwVg!@_|n#x!HTfa@tg2d*@R3JczX==pC69cUryf!arIa4w#af*5Mw>IYaheH zNE4NF?0szffaiw1PBe+n%22Z#hM8!uOMBi#5lQ=O$d z7`bw@kwX7dx#h6;2N%}8M?7^r03Dc6=|ZqZ=QGF`ilO9im2g)E|7@aQnQ#~BYYPIB z74ZYsSgI_aG912hVzG{rPX7Wg?Bj+FcV7JX_o}Oz+0-;Q zC=Z)pzehv4LP%4SL_0`1QhZX&8ffqm6?LDy^<7*e71WG^Jxx?})k{@y2V|<)WxrH455W9^C|Ki8=#@0Ab)7Fp%eH6 zLe$|%A37}Cr9q}sKaKn*v6dny0`U0EhVs7gX374va@7+?=po?EgA^{Cp-73ztv`Ux zR^i~=y-HTXPfM(ZerK*i$M}Qj}c&CUXrWF=sk|X5M-}CzCg^vPhv0@c&rWq|9 zhnU$#fG=;(dcfW9mYdqsbze>SLmoi#&dVN`h=%w4bCQ|Ae{Yvbr|f_;xWOl#8i`aN z4Zmgr5k&d)SMHnyQ^S6YP3-J%`0lVZQ1R*ovCg)9Jxh{_jj&f<+2+18Bk^31LYvH9 zE@c?T?y(loPtOm={DCH1I=6|eDOaXY#Ke{C%F4xtyrHsCb=O5@$U{ELEWn_z-J&Hh z8j4YU5UC~Ty|z7Eki1uK5Pl{5!_#idUHSQ)@5>U4e!VT0mHS!s=NC`kB`%M-K8 zqVUnla}uf;RqTMk8=nIOK0!$t+FFaIpRShr^+GsdNc@fZaVAo`UK{5GErEGopqfqm zHi>f!1hJVn$!9!h_iB=DgNgmbFat0@zN8YgIJPp-O(y0CrDM*mPv*d*r?E7HKjNPw zH#NFRTUQ9}%^|jdDUAmncL1MjZneEOitHub03YU^H~cd-I4MBMtP2Co*Rui#2tdp* z9Z91f5{xF%ddU3r_?Y)AqbKru#AJZU3J3TVQVfoLEfMTSC1f*&3q`wz(-62I`XK_7 z&+L)`Len_4IG9b*6GStyIRmbGPXvf=9GvX4#Ss_ks27mI>Y8@#O#%E#J^ z2Cg+L>}tSrX$<)52A3=0%Nv-Ex0i(zA};4$Ms{NlaP>LbKy3YeuQfASxG=K=NupxM z$AURQ>{`+Xja#vZ)oT}~hvA9WP`=C!ac)24M%VK&9G~ps#oHy9DZD6Z2kZqGF3R6P zgZZ)Id#`9r4l7o`*+-8APn5pn28tyQ?Esojh%J62=>=Y8UZjE4r#3kCu zR-1}6m=sb*HwgaCrRxD)5eymIA7S5b%KbyA%SN4JJ?eSH<3ghU+a^itpgnlt z)apg0>=_KD{74;^g~<8Qaavcu2e;LdvNUCcm@L{Tm&-0q z9rHYaXi?nVaW$vBc;SU@WYRZ2iwOsGLZz)n-uOKo21?L5OoUdy+>vCQ4rBn^f z!-p(xyuK0Q`!>(u@$ZEJWEj1W^23Q21e*p@BZQ1Zf^I;_%OO{o9+$YQe9aF%e zZZ2bm9UBBQ2;j#Ewf&gKKK_9(QLh0^3WlJ|V8>f|+s5Xn->A-|mEZ{wTA5Dp&AqnU zpY-6)fxi`6k%0s-hNnyYUG)9y{_CD5mtT(p954ggyr1R!qOs+Z@a?#pjt$lmO1RZR zlJhzb+8xC&Y+o1!3$9?%##=WWiM5}*Ht?=|6I8Upi1ee=$szZ&*Y63(Vu|#~ERY4s zr8Uw;1I|0LO+4WR7}y@$`qm#;aQJsj5~mGK=*{(Udae+gUo{%fCpRFp>QWr=Nr`@>AqOXzmBBQQIWnGMM;N8b6}?`X~5 zhg^O=f|9c2o_ryP8?$9%AjeWe1ABx+V=@6eha~am>S@0N5~YC-aaYa&RDueppLN9D^N{G_FVt(_japNo(0I8#_b2@e zg8WU;SZ+j_qY&8yr8Qc-0_Prt-EDIOvcCpnXk)V#jCWd=aV9O_b+n)T^^1U`?bg*< z)hE8T@XcZ6iW5CzD4=p-&#cH$-s;QAu#c`WNI?2h| z`fl>9}?+6#;1kml6Qsvc~H8oM!X20 zl@IJLs{+L7iV%6)ETDLk7$E)FEQDi4l^oKjS*8sq;TNPTad^y(;NbPPRdF`>s-1VR zZ~6-cTj8thPu=i~Lr(8Yt&C*@=2Cs+f-FuNw^=-$MDan@0TZL<5VZ;i3O=@4dLgcx z1CJB9MwpysYvN1;jRl=nuWQ>$x2LH52E%UyzCxxvhTYSy=zaVYsJx6?@A} z47O}ebimNE-qCMPjpQ;yp_3$&TJpFCN&%D(;GdlYx zh*BcIN=WwUSwM(J^)h9)PaotD9#tQQ5ez-XOWib<<7xu_IXX;_CXfP(WSkoWwqXMS zKRWBY($cT?OJrx)0O3R_bJbZ4mHhltuizdXP^;~Dryvh`Lcut3pjSiG9rAb=ngN@5pXEtNU2?y>8`Sago)qZbO@?07lRTUK4zD6>w)^%*iCw*Sty zO;u7(h1_A-R1lL1Y!TOCZ-&QUmuYagHAFi-imJ-rqsOAVh#0WAx#9&Vw2Z&)r_eDB z1v_5)k?&`oOxFBm-Gc`vKD&593+uOZQQeIz7hLOo!j8W07Pfjv5{Zv=x6O%4P^A!s zb(jD2P+iQ6dvd;J>Y8SH_5=i5EYo1T;rh4 z3{$h~e8d`Jj9sR!6!6Uzc|0GuRTYLJGd#nb-m6-~>QA2_KRbB<(HHlmpF&jRlZyZ8 zwGR?^{H{fy#BBPRc|d)hdV<%MC+x^3SCw1Gz=jZ-bprG|1&$JyJQAvIIJ4U5p{ixP z;(&YfmbOvzFl7jxbx5>oUXLoUqs6rgQqRIUhr+iUGDdUA#UX8M9{xyg>D;W8<Bn;{|<0U#Fqq0Xt&S$X=PcIS>$T2QRgv!B* z>UG^d!R{XCysWopG!nehvT$HCKi(=bzpt{uk{CzE`*K7HjxV8Og?Bx8EBJoeDHO6b zi*xyz?Cxi!*JWWBw9$vLrcpUraEuhfiiwGhIN!qVvK9efY@}LP@RKSyYqIsJO-}!G z-T-)>3RKTNd};`=*Xsvhl2rm%_N)<+VX)RQ@&0@LrxqGh-o{Xm!I*F#z z1{-vHbe+Y}*C5?{w#GpNHc}h@ZSP=dNfL5Zp^IqijY^&Wnh>1p*Im&= z#g130c7&Hs2R@I8ib)#+nDGGbWGKL$S*#*s_!HbQX~Uyd9C&3LC~{$ropjk!koSwS zsVj2^qaD@mjpofy#LLXnDc=*xEX7^YdQ3j)EfegVf{WO^7JaYYi^#AKgQ1nd=j!5*te=SD$VIN$9r@Aj5ym+AWbE8=$B5CVqZP4AOWOw`tENFLX$cC$}oUJ15}ICFumP zy<3N1zQM&{o%nv)FVke%$aJCthh#KNZmY`L8211?)%{*wt(sM_|^dy{iz9#zORX#Bd01)g(gHHe9YZ15@>Z z2=QeLz}|{#t07+fsAu>%f?!2-U!-%~$gRe0ve?*IpulF#0CWbMq74NQ=VET%hKmub#*2Lb1cfr#9 zST?9Afm@a)CQZB$rUndTd$SPC8vB6C9#82>U9V+N_(EMjb_G^8!$Ei!GQvd-YvWib z5Hez5A{u?~H^>8HW$9e(bstVybPplMp0E>dsc6@iwZ7bWrau)gk$=q~H66LXUj=9kzvyX?o;P@HtHd zB@0u2B~pGlt|@$+w@H4S0@dO4b>|nM(_6SX7*h<27R`6*L9c)@l?G#3VEjXtO82sd ziHtnwIGa&>{jJTKKHAO+*D0zUwR4FO2-_pg!Tf-eE*;jcnK1->1wOde zDs0*o;}=92N4zqJcJAeYBo%%oE6MS2EomwC9n$DY@No;HNdM&g{0|FwEQUiv%*fUd zt1HqM{@!5VM(;sNEJSGYWy!GNiCre|PthP-?*V~2{z0^9bIz2B1@>#qe$T>K zTA(V@k~}cd3%8rY>geQn6^yAB>a<14Jv4k{+UHOUx#;fV!T=p*k^Ct+1zecs10FNA zFerW-ILrGbl!P53ODphujnr1fD#hywl!0IdMf!tNi7lKkOq zv=Iw7)jMpCt3ZdrymPr~lK|M7g>o(*5U|S^U;&DcoS>Lp2!BQS@K7(ntO?s0B4NC8 z0p!8|vpoMBatsiaX@Fce= zHhkvDDA0v3)|3@1bFj1f2|45=X1ZZ-1l=l-l2%5_EVdJrX8&`CdoaGZ6y6x zn*V7x85a{1^9Dr#fAQxRRSvv?7LiVvq;~An0AbF^mG)2HOuCu{&Z2^yBRDfQpIOYp z#`RB{+E*grjD#n8m8!qv7NuY8&POB*;SuAsD&`b^zFHAInT;8`)ozSl=|uLZ6)a}x z$Q!#HjD5KZ`o&*Xb6{eMy1>El^SJM;#R^}|RsHFz>^BvUe77=Q4-W@z_}v6kHN zZ8!Gra8#hU5b50Pc(|1#F~HiFqXkDJSC{`Z(IA?o@r%Rj6$!JX1Xj6mboFmcRs{hyZ=?fKaDmR47Jy|c9rI`?{U(|>N@L;x literal 0 HcmV?d00001 diff --git a/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee index b1a464829c..1e3b6b0ebe 100644 --- a/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee @@ -4,6 +4,7 @@ should = chai.should() expect = chai.expect modulePath = "../../../../app/js/Features/Authorization/AuthorizationManager.js" SandboxedModule = require('sandboxed-module') +Errors = require "../../../../app/js/Features/Errors/Errors.js" describe "AuthorizationManager", -> beforeEach -> @@ -11,6 +12,7 @@ describe "AuthorizationManager", -> "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} "../../models/Project": Project: @Project = {} "../../models/User": User: @User = {} + "../Errors/Errors": Errors @user_id = "user-id-1" @project_id = "project-id-1" @callback = sinon.stub() @@ -91,6 +93,16 @@ describe "AuthorizationManager", -> it "should return the public privilege level", -> @callback.calledWith(null, "readAndWrite", true).should.equal true + + describe "when the project doesn't exist", -> + beforeEach -> + @Project.findOne + .withArgs({ _id: @project_id }, { publicAccesLevel: 1 }) + .yields(null, null) + + it "should return a NotFoundError", -> + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, (error) -> + error.should.be.instanceof Errors.NotFoundError describe "canUserReadProject", -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee index 7e1ca2d5ef..bc62e603de 100644 --- a/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee @@ -4,16 +4,21 @@ should = chai.should() expect = chai.expect modulePath = "../../../../app/js/Features/Authorization/AuthorizationMiddlewear.js" SandboxedModule = require('sandboxed-module') +Errors = require "../../../../app/js/Features/Errors/Errors.js" describe "AuthorizationMiddlewear", -> beforeEach -> @AuthorizationMiddlewear = SandboxedModule.require modulePath, requires: "./AuthorizationManager": @AuthorizationManager = {} "logger-sharelatex": {log: () ->} + "mongojs": ObjectId: @ObjectId = {} + "../Errors/Errors": Errors @user_id = "user-id-123" @project_id = "project-id-123" @req = {} @res = {} + @ObjectId.isValid = sinon.stub() + @ObjectId.isValid.withArgs(@project_id).returns true @next = sinon.stub() METHODS_TO_TEST = { @@ -90,6 +95,17 @@ describe "AuthorizationMiddlewear", -> @AuthorizationMiddlewear.redirectToRestricted .calledWith(@req, @res, @next) .should.equal true + + describe "with malformed project id", -> + beforeEach -> + @req.params = + project_id: "blah" + @ObjectId.isValid = sinon.stub().returns false + + it "should return a not found error", (done) -> + @AuthorizationMiddlewear[middlewearMethod] @req, @res, (error) -> + error.should.be.instanceof Errors.NotFoundError + done() describe "ensureUserIsSiteAdmin", -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index f659cfebe6..632b43e7f2 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -5,6 +5,7 @@ path = require('path') sinon = require('sinon') modulePath = path.join __dirname, "../../../../app/js/Features/Collaborators/CollaboratorsHandler" expect = require("chai").expect +Errors = require "../../../../app/js/Features/Errors/Errors.js" describe "CollaboratorsHandler", -> beforeEach -> @@ -16,6 +17,7 @@ describe "CollaboratorsHandler", -> "../../models/Project": Project: @Project = {} "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} "./CollaboratorsEmailHandler": @CollaboratorsEmailHandler = {} + "../Errors/Errors": Errors @project_id = "mock-project-id" @user_id = "mock-user-id" @@ -24,25 +26,35 @@ describe "CollaboratorsHandler", -> @callback = sinon.stub() describe "getMemberIdsWithPrivilegeLevels", -> - beforeEach -> - @Project.findOne = sinon.stub() - @Project.findOne.withArgs({_id: @project_id}, {owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1}).yields(null, @project = { - owner_ref: [ "owner-ref" ] - readOnly_refs: [ "read-only-ref-1", "read-only-ref-2" ] - collaberator_refs: [ "read-write-ref-1", "read-write-ref-2" ] - }) - @CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, @callback + describe "with project", -> + beforeEach -> + @Project.findOne = sinon.stub() + @Project.findOne.withArgs({_id: @project_id}, {owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1}).yields(null, @project = { + owner_ref: [ "owner-ref" ] + readOnly_refs: [ "read-only-ref-1", "read-only-ref-2" ] + collaberator_refs: [ "read-write-ref-1", "read-write-ref-2" ] + }) + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, @callback - it "should return an array of member ids with their privilege levels", -> - @callback - .calledWith(null, [ - { id: "owner-ref", privilegeLevel: "owner" } - { id: "read-only-ref-1", privilegeLevel: "readOnly" } - { id: "read-only-ref-2", privilegeLevel: "readOnly" } - { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } - { id: "read-write-ref-2", privilegeLevel: "readAndWrite" } - ]) - .should.equal true + it "should return an array of member ids with their privilege levels", -> + @callback + .calledWith(null, [ + { id: "owner-ref", privilegeLevel: "owner" } + { id: "read-only-ref-1", privilegeLevel: "readOnly" } + { id: "read-only-ref-2", privilegeLevel: "readOnly" } + { id: "read-write-ref-1", privilegeLevel: "readAndWrite" } + { id: "read-write-ref-2", privilegeLevel: "readAndWrite" } + ]) + .should.equal true + + describe "with a missing project", -> + beforeEach -> + @Project.findOne = sinon.stub().yields(null, null) + + it "should return a NotFoundError", (done) -> + @CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, (error) -> + error.should.be.instanceof Errors.NotFoundError + done() describe "getMemberIds", -> beforeEach -> diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index 0cacffb5cf..5d54151483 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -1,83 +1,8 @@ -request = require("request") expect = require("chai").expect -async = require "async" -settings = require("settings-sharelatex") -{db} = require("../../../app/js/infrastructure/mongojs") - -count = 0 -BASE_URL = "http://localhost:3000" - -request = request.defaults({ - baseUrl: BASE_URL, - followRedirect: false -}) - -class User - constructor: (options = {}) -> - @email = "acceptance-test-#{count}@example.com" - @password = "acceptance-test-#{count}-password" - count++ - @jar = request.jar() - @request = request.defaults({ - jar: @jar - }) - - login: (callback = (error) ->) -> - @getCsrfToken (error) => - return callback(error) if error? - @request.post { - url: "/register" # 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() - callback() - - createProject: (name, callback = (error, project_id) ->) -> - @request.post { - url: "/project/new", - json: - projectName: name - }, (error, response, body) -> - return callback(error) if error? - if !body?.project_id? - console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body - callback(null, body.project_id) - - addUserToProject: (project_id, email, privileges, callback = (error, user) ->) -> - @request.post { - url: "/project/#{project_id}/users", - json: {email, privileges} - }, (error, response, body) -> - return callback(error) if error? - callback(null, body.user) - - makePublic: (project_id, level, callback = (error) ->) -> - @request.post { - url: "/project/#{project_id}/settings/admin", - json: - publicAccessLevel: level - }, (error, response, body) -> - return callback(error) if error? - callback(null) - - getCsrfToken: (callback = (error) ->) -> - @request.get { - url: "/register" - }, (err, response, body) => - return callback(error) if error? - csrfMatches = body.match("window.csrfToken = \"(.*?)\";") - if !csrfMatches? - return callback(new Error("no csrf token found")) - @request = @request.defaults({ - headers: - "x-csrf-token": csrfMatches[1] - }) - callback() +async = require("async") +User = require "./helpers/User" +request = require "./helpers/request" +settings = require "settings-sharelatex" try_read_access = (user, project_id, test, callback) -> async.series [ diff --git a/services/web/test/acceptance/coffee/ProjectCRUDTests.coffee b/services/web/test/acceptance/coffee/ProjectCRUDTests.coffee new file mode 100644 index 0000000000..19e5f445f7 --- /dev/null +++ b/services/web/test/acceptance/coffee/ProjectCRUDTests.coffee @@ -0,0 +1,21 @@ +expect = require("chai").expect +async = require("async") +User = require "./helpers/User" + +describe "Project CRUD", -> + before (done) -> + @user = new User() + @user.login done + + describe "when project doesn't exist", -> + it "should return 404", (done) -> + @user.request.get "/project/aaaaaaaaaaaaaaaaaaaaaaaa", (err, res, body) -> + expect(res.statusCode).to.equal 404 + done() + + describe "when project has malformed id", -> + it "should return 404", (done) -> + @user.request.get "/project/blah", (err, res, body) -> + expect(res.statusCode).to.equal 404 + done() + \ No newline at end of file diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee new file mode 100644 index 0000000000..f1c38029d7 --- /dev/null +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -0,0 +1,74 @@ +request = require("./request") +settings = require("settings-sharelatex") +{db} = require("../../../../app/js/infrastructure/mongojs") + +count = 0 + +class User + constructor: (options = {}) -> + @email = "acceptance-test-#{count}@example.com" + @password = "acceptance-test-#{count}-password" + count++ + @jar = request.jar() + @request = request.defaults({ + jar: @jar + }) + + login: (callback = (error) ->) -> + @getCsrfToken (error) => + return callback(error) if error? + @request.post { + url: "/register" # 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() + callback() + + createProject: (name, callback = (error, project_id) ->) -> + @request.post { + url: "/project/new", + json: + projectName: name + }, (error, response, body) -> + return callback(error) if error? + if !body?.project_id? + console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body + callback(null, body.project_id) + + addUserToProject: (project_id, email, privileges, callback = (error, user) ->) -> + @request.post { + url: "/project/#{project_id}/users", + json: {email, privileges} + }, (error, response, body) -> + return callback(error) if error? + callback(null, body.user) + + makePublic: (project_id, level, callback = (error) ->) -> + @request.post { + url: "/project/#{project_id}/settings/admin", + json: + publicAccessLevel: level + }, (error, response, body) -> + return callback(error) if error? + callback(null) + + getCsrfToken: (callback = (error) ->) -> + @request.get { + url: "/register" + }, (err, response, body) => + return callback(error) if error? + csrfMatches = body.match("window.csrfToken = \"(.*?)\";") + if !csrfMatches? + return callback(new Error("no csrf token found")) + @request = @request.defaults({ + headers: + "x-csrf-token": csrfMatches[1] + }) + callback() + +module.exports = User \ No newline at end of file diff --git a/services/web/test/acceptance/coffee/helpers/request.coffee b/services/web/test/acceptance/coffee/helpers/request.coffee new file mode 100644 index 0000000000..879acd843a --- /dev/null +++ b/services/web/test/acceptance/coffee/helpers/request.coffee @@ -0,0 +1,5 @@ +BASE_URL = "http://localhost:3000" +module.exports = require("request").defaults({ + baseUrl: BASE_URL, + followRedirect: false +}) \ No newline at end of file From a704289099fe5d9fd4654f5fa2e86e296a84a804 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 21 Mar 2016 10:26:39 +0000 Subject: [PATCH 059/208] Show the error view, even when the pdf is in an error state. --- services/web/app/views/project/editor/pdf.jade | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index bd611020bd..7b4039b92e 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -32,7 +32,7 @@ div.full-size.pdf(ng-controller="PdfController") a.log-btn( href ng-click="toggleLogs()" - ng-class="{ 'active': (pdf.view == 'logs' || pdf.failure) && !pdf.error && !pdf.timeout && !pdf.uncompiled }" + ng-class="{ 'active': (pdf.view == 'logs' || pdf.failure) && !pdf.timeout && !pdf.uncompiled }" tooltip="#{translate('logs_and_output_files')}" tooltip-placement="bottom" ) @@ -162,7 +162,7 @@ div.full-size.pdf(ng-controller="PdfController") - .pdf-logs(ng-show="(pdf.view == 'logs' || pdf.failure) && !pdf.error && !pdf.timeout && !pdf.uncompiled") + .pdf-logs(ng-show="(pdf.view == 'logs' || pdf.failure) && !pdf.timeout && !pdf.uncompiled") .alert.alert-success(ng-show="pdf.logEntries.all.length == 0") | #{translate("no_errors_good_job")} From 4e78e34cdf8458aaae2cc7176c7748250846fc8f Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 21 Mar 2016 11:41:05 +0000 Subject: [PATCH 060/208] finished contact us with groove for settings file: Contact --- .../infrastructure/ExpressLocals.coffee | 6 ++ services/web/app/views/contact-us-modal.jade | 5 +- services/web/app/views/layout.jade | 7 -- .../web/public/coffee/main/contact-us.coffee | 7 +- services/web/public/js/libs/groove.js | 84 +++++++++++++++++++ .../public/stylesheets/app/contact-us.less | 6 ++ services/web/public/stylesheets/style.less | 1 + 7 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 services/web/public/js/libs/groove.js create mode 100644 services/web/public/stylesheets/app/contact-us.less diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 1fd1aa10a2..faba1e4623 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -64,6 +64,12 @@ module.exports = (app, webRouter, apiRouter)-> Settings.siteUrl.substring(Settings.siteUrl.indexOf("//")+2) next() + webRouter.use (req, res, next)-> + res.locals.getUserEmail = -> + email = req?.session?.user?.email or "" + return email + next() + webRouter.use (req, res, next)-> res.locals.formatProjectPublicAccessLevel = (privilegeLevel)-> formatedPrivileges = private:"Private", readOnly:"Public: Read Only", readAndWrite:"Public: Read and Write" diff --git a/services/web/app/views/contact-us-modal.jade b/services/web/app/views/contact-us-modal.jade index f4b03ece2d..dade3a7730 100644 --- a/services/web/app/views/contact-us-modal.jade +++ b/services/web/app/views/contact-us-modal.jade @@ -6,9 +6,8 @@ script(type='text/ng-template', id='supportModalTemplate') ng-click="close()" ) × h3 Contact Us - .modal-body + .modal-body.contact-us-modal span(ng-show="sent == false") - .form-group label | Subject .form-group @@ -16,7 +15,7 @@ script(type='text/ng-template', id='supportModalTemplate') label.desc | Email .form-group - input.field.text.medium.span8.form-control(ng-model="form.email", type='email', spellcheck='false', value='', maxlength='255', tabindex='2') + input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '#{getUserEmail()}'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2') label#title12.desc | Project URL (optional) .form-group diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index a0e8383a7b..f407143e9d 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -82,8 +82,6 @@ html(itemscope, itemtype='http://schema.org/Product') } body - div(ng-controller="ContactModal") - a(ng-click="contactUsModal()") contact us - if(typeof(suppressSystemMessages) == "undefined") .system-messages( @@ -130,11 +128,6 @@ html(itemscope, itemtype='http://schema.org/Product') src=jsPath+'libs/require.js?fingerprint='+fingerprint(jsPath + 'libs/require.js') ) - script(type='text/javascript'). - (function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true; - s.src=('https:'==document.location.protocol?'https':'http') + '://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket/api.js'; - var q = document.getElementsByTagName('script')[0];q.parentNode.insertBefore(s, q);})(); - include contact-us-modal diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee index 5519e2aa5a..b5e52800fe 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -11,7 +11,6 @@ define [ ) App.controller 'SupportModalController', ($scope, $modal) -> - $scope.form = {} $scope.sent = false $scope.sending = false @@ -26,6 +25,7 @@ define [ message: $scope.form.message subject: $scope.form.subject + " - [#{ticketNumber}]" about : $scope.form.project_url + labels: "support" Groove.createTicket params, (err, json)-> $scope.sent = true @@ -36,8 +36,6 @@ define [ $modalInstance.close() - - App.controller 'UniverstiesContactController', ($scope, $modal) -> $scope.form = {} @@ -52,11 +50,10 @@ define [ params = name: $scope.form.name || $scope.form.email email: $scope.form.email - labels: $scope.form.source + labels: "#{$scope.form.source} accounts" message: "Please contact me with more details" subject: $scope.form.subject + " - [#{ticketNumber}]" about : "#{$scope.form.position || ''} #{$scope.form.university || ''}" - to: "support@sharelatex.com" Groove.createTicket params, (err, json)-> $scope.sent = true diff --git a/services/web/public/js/libs/groove.js b/services/web/public/js/libs/groove.js new file mode 100644 index 0000000000..983b2a73dc --- /dev/null +++ b/services/web/public/js/libs/groove.js @@ -0,0 +1,84 @@ +!function(window) { + + window.Groove = { + + init: function(options) { + this._options = options; + if (typeof grooveOnReady != 'undefined') {grooveOnReady();} + }, + + createTicket: function(params, callback) { + var postData = serialize({ + "ticket[enduser_name]": params["name"], + "ticket[enduser_email]": params["email"], + "ticket[title]": params["subject"], + "ticket[enduser_about]": params["about"], + "ticket[label_string]": params["labels"], + "ticket[comments_attributes][0][body]": params["message"] + }); + + sendRequest(this._options.widget_ticket_url, function(req) { + if (callback) {callback(req);} + }, postData); + } + }; + + // http://www.quirksmode.org/js/xmlhttp.html + function sendRequest(url, callback, postData) { + var req = createXMLHTTPObject(); + if (!req) return; + var method = (postData) ? "POST" : "GET"; + req.open(method, url, true); + if (postData){ + try { + req.setRequestHeader('Content-type','application/x-www-form-urlencoded'); + } + catch(e) { + req.contentType = 'application/x-www-form-urlencoded'; + }; + }; + req.onreadystatechange = function () { + if (req.readyState != 4) return; + callback(req); + } + if (req.readyState == 4) return; + req.send(postData); + } + + var XMLHttpFactories = [ + function () {return new XDomainRequest()}, + function () {return new XMLHttpRequest()}, + function () {return new ActiveXObject("Msxml2.XMLHTTP")}, + function () {return new ActiveXObject("Msxml3.XMLHTTP")}, + function () {return new ActiveXObject("Microsoft.XMLHTTP")} + ]; + + function createXMLHTTPObject() { + var xmlhttp = false; + for (var i = 0; i < XMLHttpFactories.length; i++) { + try { + xmlhttp = XMLHttpFactories[i](); + } + catch (e) { + continue; + } + break; + } + return xmlhttp; + } + + function serialize(obj) { + var str = []; + for(var p in obj) { + if (obj[p]) { + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + } + } + return str.join("&"); +} + +if (typeof grooveOnLoad != 'undefined') {grooveOnLoad();} +}(window); + +Groove.init({"widget_ticket_url":"https://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket.json"}); + diff --git a/services/web/public/stylesheets/app/contact-us.less b/services/web/public/stylesheets/app/contact-us.less new file mode 100644 index 0000000000..06747b5412 --- /dev/null +++ b/services/web/public/stylesheets/app/contact-us.less @@ -0,0 +1,6 @@ +.contact-us-modal { + + textarea { + height: 120px; + } +} \ No newline at end of file diff --git a/services/web/public/stylesheets/style.less b/services/web/public/stylesheets/style.less index be7367a08b..4e9823631c 100755 --- a/services/web/public/stylesheets/style.less +++ b/services/web/public/stylesheets/style.less @@ -71,3 +71,4 @@ @import "app/templates.less"; @import "app/wiki.less"; @import "app/translations.less"; +@import "app/contact-us.less"; From 2a9e451876c69b56ef9c19970dc68b5105f6f28a Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 11:55:59 +0000 Subject: [PATCH 061/208] Refine error page to in all situations, and don't send sentry errors on not found errors --- .../Features/Errors/ErrorController.coffee | 3 +- services/web/app/coffee/router.coffee | 6 +-- services/web/app/views/general/500.jade | 37 +++++++++++-------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/services/web/app/coffee/Features/Errors/ErrorController.coffee b/services/web/app/coffee/Features/Errors/ErrorController.coffee index a0ce6c85c9..255c747bea 100644 --- a/services/web/app/coffee/Features/Errors/ErrorController.coffee +++ b/services/web/app/coffee/Features/Errors/ErrorController.coffee @@ -17,8 +17,9 @@ module.exports = ErrorController = logger.log err: error,url:req.url, method:req.method, user:req?.sesson?.user, "invalid csrf" res.sendStatus(403) return - logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear" if error instanceof Errors.NotFoundError + logger.warn {err: error, url: req.url}, "not found error" ErrorController.notFound req, res else + logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear" ErrorController.serverError req, res \ No newline at end of file diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 305b3bd564..60f1d6b7c4 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -221,9 +221,9 @@ module.exports = class Router headers: req.headers }) - apiRouter.get '/oops-express', (req, res, next) -> next(new Error("Test error")) - apiRouter.get '/oops-internal', (req, res, next) -> throw new Error("Test error") - apiRouter.get '/oops-mongo', (req, res, next) -> + webRouter.get '/oops-express', (req, res, next) -> next(new Error("Test error")) + webRouter.get '/oops-internal', (req, res, next) -> throw new Error("Test error") + webRouter.get '/oops-mongo', (req, res, next) -> require("./models/Project").Project.findOne {}, () -> throw new Error("Test error") diff --git a/services/web/app/views/general/500.jade b/services/web/app/views/general/500.jade index 12983b923b..e045a7d457 100644 --- a/services/web/app/views/general/500.jade +++ b/services/web/app/views/general/500.jade @@ -1,16 +1,21 @@ -extends ../layout - -block content - .content - .container - .row - .col-md-8.col-md-offset-2.text-center - .page-header - h2 Oh dear, something went wrong. - p: img(src="/img/lion-sad-128.png", alt="Sad Lion") - p - | Something went wrong with your request, sorry. Our staff are probably looking into this, but it continues, please contact us at #{settings.adminEmail} - p - a(href="/") - i.fa.fa-arrow-circle-o-left - | #{translate("take_me_home")} +doctype html +html(itemscope, itemtype='http://schema.org/Product') + head + title Something went wrong + link(rel="icon", href="/favicon.ico") + link(rel='stylesheet', href='/stylesheets/style.css') + link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet") + body + .content + .container + .row + .col-md-8.col-md-offset-2.text-center + .page-header + h2 Oh dear, something went wrong. + p: img(src="/img/lion-sad-128.png", alt="Sad Lion") + p + | Something went wrong with your request, sorry. Our staff are probably looking into this, but if it continues, please contact us at #{settings.adminEmail} + p + a(href="/") + i.fa.fa-arrow-circle-o-left + | Take me home From 0ba70e7ccc83670215dd44141989c10b4043bf0c Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 13:15:57 +0000 Subject: [PATCH 062/208] Remove missing parameter in log lines --- .../web/app/coffee/Features/Uploads/ArchiveManager.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee index a615810fe6..41d041109d 100644 --- a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee +++ b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee @@ -25,7 +25,7 @@ module.exports = ArchiveManager = error += chunk unzip.on "error", (err) -> - logger.error {err, source, destination}, "unzip failed" + logger.error {err, source}, "unzip failed" if err.code == "ENOENT" logger.error "unzip command not found. Please check the unzip command is installed" callback(err) @@ -33,7 +33,7 @@ module.exports = ArchiveManager = unzip.on "exit", () -> if error? error = new Error(error) - logger.error err:error, source: source, destination: destination, "error checking zip size" + logger.error err:error, source: source, "error checking zip size" lines = output.split("\n") lastLine = lines[lines.length - 2]?.trim() From 9b22efbd98a9e075d78de10f63d5a06d0c833330 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 21 Mar 2016 13:20:03 +0000 Subject: [PATCH 063/208] split errors into normal error, and renderingError. --- services/web/app/views/project/editor/pdf.jade | 10 +++++----- .../coffee/ide/pdf/controllers/PdfController.coffee | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index 7b4039b92e..d07f5be611 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -32,7 +32,7 @@ div.full-size.pdf(ng-controller="PdfController") a.log-btn( href ng-click="toggleLogs()" - ng-class="{ 'active': (pdf.view == 'logs' || pdf.failure) && !pdf.timeout && !pdf.uncompiled }" + ng-class="{ 'active': (pdf.view == 'logs' || pdf.failure) && !pdf.error && !pdf.timeout && !pdf.uncompiled }" tooltip="#{translate('logs_and_output_files')}" tooltip-placement="bottom" ) @@ -75,7 +75,7 @@ div.full-size.pdf(ng-controller="PdfController") i.split-screen i.split-screen - .pdf-viewer(ng-show="pdf.url && pdf.view == 'pdf' && !pdf.failure && !pdf.timeout && !pdf.error") + .pdf-viewer(ng-show="pdf.url && pdf.view == 'pdf' && !pdf.failure && !pdf.timeout && !pdf.error && !pdf.renderingError") div( pdfng ng-if="settings.pdfViewer == 'pdfjs'" @@ -97,8 +97,8 @@ div.full-size.pdf(ng-controller="PdfController") i.fa.fa-level-up.fa-flip-horizontal.fa-2x |   #{translate('click_here_to_preview_pdf')} - .pdf-errors(ng-show="pdf.timedout || pdf.error") - .alert.alert-danger(ng-show="pdf.error") + .pdf-errors(ng-show="pdf.timedout || pdf.error || pdf.renderingError") + .alert.alert-danger(ng-show="pdf.error || pdf.renderingError") strong #{translate("server_error")} span #{translate("somthing_went_wrong_compiling")} @@ -162,7 +162,7 @@ div.full-size.pdf(ng-controller="PdfController") - .pdf-logs(ng-show="(pdf.view == 'logs' || pdf.failure) && !pdf.timeout && !pdf.uncompiled") + .pdf-logs(ng-show="(pdf.view == 'logs' || pdf.failure) && !pdf.error && !pdf.timeout && !pdf.uncompiled") .alert.alert-success(ng-show="pdf.logEntries.all.length == 0") | #{translate("no_errors_good_job")} diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 8586d218a7..10d6a1f51b 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -12,7 +12,7 @@ define [ $scope.hasPremiumCompile = $scope.project.features.compileGroup == "priority" $scope.$on "pdf:error:display", () -> - $scope.pdf.error = true + $scope.pdf.renderingError = true $scope.draft = localStorage("draft:#{$scope.project_id}") or false $scope.$watch "draft", (new_value, old_value) -> From 8fb3e629e86e306a531d494f463f0f6d6bd861bd Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 13:23:14 +0000 Subject: [PATCH 064/208] Require logins for all uploads to projects --- services/web/app/coffee/Features/Uploads/UploadsRouter.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee index fdff8d0ea3..d87d271a7f 100644 --- a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee +++ b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee @@ -16,6 +16,7 @@ module.exports = maxRequests: 200 timeInterval: 60 * 30 }), + AuthenticationController.requireLogin(), AuthorizationMiddlewear.ensureUserCanWriteProjectContent, ProjectUploadController.uploadFile From 6beb29f4498b9885357f016e56d571756b84ec7c Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 13:28:53 +0000 Subject: [PATCH 065/208] Don't treat no root resource as a fatal error --- .../Features/Compile/ClsiManager.coffee | 26 +++++++++---------- .../coffee/Compile/ClsiManagerTests.coffee | 8 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 8b7ef023bd..174ebe830b 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -86,6 +86,9 @@ module.exports = ClsiManager = rootResourcePathOverride = path rootResourcePath = rootResourcePathOverride if rootResourcePathOverride? + if !rootResourcePath? + logger.warn {project_id}, "no root document found, setting to main.tex" + rootResourcePath = "main.tex" for path, file of files path = path.replace(/^\//, "") # Remove leading / @@ -94,19 +97,16 @@ module.exports = ClsiManager = url: "#{Settings.apis.filestore.url}/project/#{project._id}/file/#{file._id}" modified: file.created?.getTime() - if !rootResourcePath? - callback new Error("no root document exists") - else - callback null, { - compile: - options: - compiler: project.compiler - timeout: options.timeout - imageName: project.imageName - draft: !!options.draft - rootResourcePath: rootResourcePath - resources: resources - } + callback null, { + compile: + options: + compiler: project.compiler + timeout: options.timeout + imageName: project.imageName + draft: !!options.draft + rootResourcePath: rootResourcePath + resources: resources + } wordCount: (project_id, file, options, callback = (error, response) ->) -> ClsiManager._buildRequest project_id, options, (error, req) -> diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee index baf2643c05..3aa5b80528 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee @@ -19,7 +19,7 @@ describe "ClsiManager", -> url: "https://clsipremium.example.com" "../../models/Project": Project: @Project = {} "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } + "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } "request": @request = {} @project_id = "project-id" @callback = sinon.stub() @@ -218,9 +218,9 @@ describe "ClsiManager", -> @project.rootDoc_id = "not-valid" @ClsiManager._buildRequest @project, null, (@error, @request) => done() - - it "should return an error", -> - expect(@error).to.exist + + it "should set to main.tex", -> + @request.compile.rootResourcePath.should.equal "main.tex" describe "with the draft option", -> it "should add the draft option into the request", (done) -> From 8bfc613bb3d8e908bae8127d293278cc968b34af Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 13:29:34 +0000 Subject: [PATCH 066/208] Log client side errors as warns so they don't show in Sentry --- services/web/app/coffee/router.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 60f1d6b7c4..5e16073ed3 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -232,7 +232,7 @@ module.exports = class Router res.send() webRouter.post '/error/client', (req, res, next) -> - logger.error err: req.body.error, meta: req.body.meta, "client side error" + logger.warn err: req.body.error, meta: req.body.meta, "client side error" res.sendStatus(204) webRouter.get '*', ErrorController.notFound \ No newline at end of file From 9a0ec9c292101fc02b1cad44fc882d57fa39e9ef Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 13:54:45 +0000 Subject: [PATCH 067/208] Don't throw fatal error when recently compiled --- .../coffee/Features/Compile/CompileManager.coffee | 3 ++- .../coffee/Compile/CompileManagerTests.coffee | 13 +++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/CompileManager.coffee b/services/web/app/coffee/Features/Compile/CompileManager.coffee index 7cac827789..c89e7107dd 100755 --- a/services/web/app/coffee/Features/Compile/CompileManager.coffee +++ b/services/web/app/coffee/Features/Compile/CompileManager.coffee @@ -26,7 +26,8 @@ module.exports = CompileManager = CompileManager._checkIfRecentlyCompiled project_id, user_id, (error, recentlyCompiled) -> return callback(error) if error? if recentlyCompiled - return callback new Error("project was recently compiled so not continuing") + logger.warn {project_id, user_id}, "project was recently compiled so not continuing" + return callback null, "too-recently-compiled", [] CompileManager._ensureRootDocumentIsSet project_id, (error) -> return callback(error) if error? diff --git a/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee index e68f5fa422..814b4f258b 100644 --- a/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee @@ -27,7 +27,7 @@ describe "CompileManager", -> Timer: class Timer done: sinon.stub() inc: sinon.stub() - "logger-sharelatex": @logger = { log: sinon.stub() } + "logger-sharelatex": @logger = { log: sinon.stub(), warn: sinon.stub() } @project_id = "mock-project-id-123" @user_id = "mock-user-id-123" @callback = sinon.stub() @@ -90,15 +90,12 @@ describe "CompileManager", -> .should.equal true describe "when the project has been recently compiled", -> - beforeEach -> + it "should return", (done)-> @CompileManager._checkIfAutoCompileLimitHasBeenHit = (_, cb)-> cb(null, true) @CompileManager._checkIfRecentlyCompiled = sinon.stub().callsArgWith(2, null, true) - @CompileManager.compile @project_id, @user_id, {}, @callback - - it "should return the callback with an error", -> - @callback - .calledWith(new Error("project was recently compiled so not continuing")) - .should.equal true + @CompileManager.compile @project_id, @user_id, {}, (err, status)-> + status.should.equal "too-recently-compiled" + done() describe "should check the rate limit", -> it "should return", (done)-> From 3b35605edcae8f9e399f399560623128ddba35bc Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 21 Mar 2016 14:05:26 +0000 Subject: [PATCH 068/208] Show logs, even if the compile timed out. --- services/web/app/views/project/editor/pdf.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index d07f5be611..843ff07c6c 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -97,7 +97,7 @@ div.full-size.pdf(ng-controller="PdfController") i.fa.fa-level-up.fa-flip-horizontal.fa-2x |   #{translate('click_here_to_preview_pdf')} - .pdf-errors(ng-show="pdf.timedout || pdf.error || pdf.renderingError") + .pdf-errors(ng-show="(pdf.timedout || pdf.error || pdf.renderingError) && pdf.view != 'logs'") .alert.alert-danger(ng-show="pdf.error || pdf.renderingError") strong #{translate("server_error")} span #{translate("somthing_went_wrong_compiling")} From 53dfa841cc3af4b594fd5eada084bd97edcde8c4 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 21 Mar 2016 15:00:25 +0000 Subject: [PATCH 069/208] Add the beginnings of a clsi-maintenance-mode message --- services/web/app/views/project/editor/pdf.jade | 16 ++++++++-------- .../ide/pdf/controllers/PdfController.coffee | 3 +++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index 843ff07c6c..a19e374736 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -74,8 +74,9 @@ div.full-size.pdf(ng-controller="PdfController") ) i.split-screen i.split-screen - - .pdf-viewer(ng-show="pdf.url && pdf.view == 'pdf' && !pdf.failure && !pdf.timeout && !pdf.error && !pdf.renderingError") + // end of toolbar + + .pdf-viewer(ng-show="pdf.url && pdf.view == 'pdf' && !pdf.failure && !pdf.timeout && !pdf.error && !pdf.renderingError && !pdf.clsiMaintenance") div( pdfng ng-if="settings.pdfViewer == 'pdfjs'" @@ -96,12 +97,15 @@ div.full-size.pdf(ng-controller="PdfController") |   i.fa.fa-level-up.fa-flip-horizontal.fa-2x |   #{translate('click_here_to_preview_pdf')} - - .pdf-errors(ng-show="(pdf.timedout || pdf.error || pdf.renderingError) && pdf.view != 'logs'") + + .pdf-errors(ng-show="(pdf.timedout || pdf.error || pdf.renderingError || pdf.clsiMaintenance) && pdf.view != 'logs'") .alert.alert-danger(ng-show="pdf.error || pdf.renderingError") strong #{translate("server_error")} span #{translate("somthing_went_wrong_compiling")} + .alert.alert-danger(ng-show="pdf.clsiMaintenance") + strong #{translate("server_error")} + span #{translate("clsi_maintenance")} .alert.alert-danger(ng-show="pdf.timedout") p @@ -158,10 +162,6 @@ div.full-size.pdf(ng-controller="PdfController") span #{translate("project_too_large_please_reduce")} - - - - .pdf-logs(ng-show="(pdf.view == 'logs' || pdf.failure) && !pdf.error && !pdf.timeout && !pdf.uncompiled") .alert.alert-success(ng-show="pdf.logEntries.all.length == 0") | #{translate("no_errors_good_job")} diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 10d6a1f51b..7dbeaee59f 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -37,6 +37,7 @@ define [ $scope.pdf.uncompiled = false $scope.pdf.projectTooLarge = false $scope.pdf.url = null + $scope.pdf.clsiMaintenance = false if response.status == "timedout" $scope.pdf.timedout = true @@ -47,6 +48,8 @@ define [ else if response.status == "failure" $scope.pdf.failure = true fetchLogs() + else if response.status == 'clsi-maintenance' + $scope.pdf.clsiMaintenance = true else if response.status == "success" # define the base url $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf?cache_bust=#{Date.now()}" From f58f1c3795536af5fdf54ba27e2e6edc1aec11be Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 21 Mar 2016 15:16:17 +0000 Subject: [PATCH 070/208] Account for the new too-recently-compiled error. --- services/web/app/views/project/editor/pdf.jade | 8 ++++++-- .../coffee/ide/pdf/controllers/PdfController.coffee | 3 +++ 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 a19e374736..1338cffa47 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -76,7 +76,7 @@ div.full-size.pdf(ng-controller="PdfController") i.split-screen // end of toolbar - .pdf-viewer(ng-show="pdf.url && pdf.view == 'pdf' && !pdf.failure && !pdf.timeout && !pdf.error && !pdf.renderingError && !pdf.clsiMaintenance") + .pdf-viewer(ng-show="pdf.url && pdf.view == 'pdf' && !pdf.failure && !pdf.timeout && !pdf.error && !pdf.renderingError && !pdf.clsiMaintenance && !pdf.tooRecentlyCompiled") div( pdfng ng-if="settings.pdfViewer == 'pdfjs'" @@ -98,7 +98,7 @@ div.full-size.pdf(ng-controller="PdfController") i.fa.fa-level-up.fa-flip-horizontal.fa-2x |   #{translate('click_here_to_preview_pdf')} - .pdf-errors(ng-show="(pdf.timedout || pdf.error || pdf.renderingError || pdf.clsiMaintenance) && pdf.view != 'logs'") + .pdf-errors(ng-show="(pdf.timedout || pdf.error || pdf.renderingError || pdf.clsiMaintenance || pdf.tooRecentlyCompiled) && pdf.view != 'logs'") .alert.alert-danger(ng-show="pdf.error || pdf.renderingError") strong #{translate("server_error")} span #{translate("somthing_went_wrong_compiling")} @@ -107,6 +107,10 @@ div.full-size.pdf(ng-controller="PdfController") strong #{translate("server_error")} span #{translate("clsi_maintenance")} + .alert.alert-danger(ng-show="pdf.tooRecentlyCompiled") + strong #{translate("server_error")} + span #{translate("too_recently_compiled")} + .alert.alert-danger(ng-show="pdf.timedout") p strong #{translate("timedout")}. diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 7dbeaee59f..413c59aa03 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -38,6 +38,7 @@ define [ $scope.pdf.projectTooLarge = false $scope.pdf.url = null $scope.pdf.clsiMaintenance = false + $scope.pdf.tooRecentlyCompiled = false if response.status == "timedout" $scope.pdf.timedout = true @@ -50,6 +51,8 @@ define [ fetchLogs() else if response.status == 'clsi-maintenance' $scope.pdf.clsiMaintenance = true + else if response.status == "too-recently-compiled" + $scope.pdf.tooRecentlyCompiled = true else if response.status == "success" # define the base url $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf?cache_bust=#{Date.now()}" From 4d7ed1cb17cc7f587c45eedf9a91d8eb62e6302a Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 21 Mar 2016 16:00:12 +0000 Subject: [PATCH 071/208] improved logging from unzip command --- .../web/app/coffee/Features/Uploads/ArchiveManager.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee index 41d041109d..5185c5d49c 100644 --- a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee +++ b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee @@ -39,11 +39,11 @@ module.exports = ArchiveManager = lastLine = lines[lines.length - 2]?.trim() totalSizeInBytes = lastLine?.split(" ")?[0] - totalSizeInBytes = parseInt(totalSizeInBytes) + totalSizeInBytesAsInt = parseInt(totalSizeInBytes) - if !totalSizeInBytes? or isNaN(totalSizeInBytes) - logger.err source:source, "error getting bytes of zip" - return callback(new Error("something went wrong")) + if !totalSizeInBytesAsInt? or isNaN(totalSizeInBytesAsInt) + logger.err source:source, totalSizeInBytes:totalSizeInBytes, totalSizeInBytesAsInt:totalSizeInBytesAsInt, lastLine:lastLine, "error getting bytes of zip" + return callback(new Error("error getting bytes of zip")) isTooLarge = totalSizeInBytes > (ONE_MEG * 300) From 840d3b75bb4cd8f93dc2435362653be8c332a2e1 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 21 Mar 2016 16:37:29 +0000 Subject: [PATCH 072/208] listen for close not exit when working with unzip command sometimes the command will fail because stout has not finished yet --- .../coffee/Features/Uploads/ArchiveManager.coffee | 8 +++----- .../coffee/Uploads/ArchiveManagerTests.coffee | 14 +++++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee index 5185c5d49c..645828ca6d 100644 --- a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee +++ b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee @@ -30,7 +30,7 @@ module.exports = ArchiveManager = logger.error "unzip command not found. Please check the unzip command is installed" callback(err) - unzip.on "exit", () -> + unzip.on "close", (exitCode) -> if error? error = new Error(error) logger.error err:error, source: source, "error checking zip size" @@ -42,7 +42,7 @@ module.exports = ArchiveManager = totalSizeInBytesAsInt = parseInt(totalSizeInBytes) if !totalSizeInBytesAsInt? or isNaN(totalSizeInBytesAsInt) - logger.err source:source, totalSizeInBytes:totalSizeInBytes, totalSizeInBytesAsInt:totalSizeInBytesAsInt, lastLine:lastLine, "error getting bytes of zip" + logger.err source:source, totalSizeInBytes:totalSizeInBytes, totalSizeInBytesAsInt:totalSizeInBytesAsInt, lastLine:lastLine, exitCode:exitCode, "error getting bytes of zip" return callback(new Error("error getting bytes of zip")) isTooLarge = totalSizeInBytes > (ONE_MEG * 300) @@ -50,8 +50,6 @@ module.exports = ArchiveManager = callback(error, isTooLarge) - - extractZipArchive: (source, destination, _callback = (err) ->) -> callback = (args...) -> @@ -87,7 +85,7 @@ module.exports = ArchiveManager = logger.error "unzip command not found. Please check the unzip command is installed" callback(err) - unzip.on "exit", () -> + unzip.on "close", () -> timer.done() if error? error = new Error(error) diff --git a/services/web/test/UnitTests/coffee/Uploads/ArchiveManagerTests.coffee b/services/web/test/UnitTests/coffee/Uploads/ArchiveManagerTests.coffee index 89b14c78c9..eab3d2fcbd 100644 --- a/services/web/test/UnitTests/coffee/Uploads/ArchiveManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Uploads/ArchiveManagerTests.coffee @@ -39,7 +39,7 @@ describe "ArchiveManager", -> describe "successfully", -> beforeEach (done) -> @ArchiveManager.extractZipArchive @source, @destination, done - @process.emit "exit" + @process.emit "close" it "should run unzip", -> @child.spawn.calledWithExactly("unzip", [@source, "-d", @destination]).should.equal true @@ -56,7 +56,7 @@ describe "ArchiveManager", -> @callback(error) done() @process.stderr.emit "data", "Something went wrong" - @process.emit "exit" + @process.emit "close" it "should return the callback with an error", -> @callback.calledWithExactly(new Error("Something went wrong")).should.equal true @@ -99,35 +99,35 @@ describe "ArchiveManager", -> isTooLarge.should.equal false done() @process.stdout.emit "data", @output("109042") - @process.emit "exit" + @process.emit "close" it "should return true with large bytes", (done)-> @ArchiveManager._isZipTooLarge @source, (error, isTooLarge) => isTooLarge.should.equal true done() @process.stdout.emit "data", @output("1090000000000000042") - @process.emit "exit" + @process.emit "close" it "should return error on no data", (done)-> @ArchiveManager._isZipTooLarge @source, (error, isTooLarge) => expect(error).to.exist done() @process.stdout.emit "data", "" - @process.emit "exit" + @process.emit "close" it "should return error if it didn't get a number", (done)-> @ArchiveManager._isZipTooLarge @source, (error, isTooLarge) => expect(error).to.exist done() @process.stdout.emit "data", @output("total_size_string") - @process.emit "exit" + @process.emit "close" it "should return error if the is only a bit of data", (done)-> @ArchiveManager._isZipTooLarge @source, (error, isTooLarge) => expect(error).to.exist done() @process.stdout.emit "data", " Length Date Time Name \n--------" - @process.emit "exit" + @process.emit "close" describe "findTopLevelDirectory", -> beforeEach -> From 77918059493fa0182efc89b4e561ac15c93a1132 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 21 Mar 2016 17:03:31 +0000 Subject: [PATCH 073/208] Allow admin access to projects --- .../Authorization/AuthorizationManager.coffee | 7 +++- .../AuthorizationManagerTests.coffee | 33 +++++++++++++++++++ .../coffee/AuthorizationTests.coffee | 29 +++++++++++++++- .../acceptance/coffee/helpers/User.coffee | 5 ++- 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee index eb6f564fea..ded0b6f979 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee @@ -33,7 +33,12 @@ module.exports = AuthorizationManager = # The user has direct access callback null, privilegeLevel, false else - getPublicAccessLevel() + AuthorizationManager.isUserSiteAdmin user_id, (error, isAdmin) -> + return callback(error) if error? + if isAdmin + callback null, PrivilegeLevels.OWNER, false + else + getPublicAccessLevel() canUserReadProject: (user_id, project_id, callback = (error, canRead) ->) -> AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> diff --git a/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee index 1e3b6b0ebe..fcacce5164 100644 --- a/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee @@ -20,6 +20,7 @@ describe "AuthorizationManager", -> describe "getPrivilegeLevelForProject", -> beforeEach -> @Project.findOne = sinon.stub() + @AuthorizationManager.isUserSiteAdmin = sinon.stub() @CollaboratorsHandler.getMemberIdPrivilegeLevel = sinon.stub() describe "with a private project", -> @@ -30,6 +31,7 @@ describe "AuthorizationManager", -> describe "with a user_id with a privilege level", -> beforeEach -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) @CollaboratorsHandler.getMemberIdPrivilegeLevel .withArgs(@user_id, @project_id) .yields(null, "readOnly") @@ -40,6 +42,7 @@ describe "AuthorizationManager", -> describe "with a user_id with no privilege level", -> beforeEach -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) @CollaboratorsHandler.getMemberIdPrivilegeLevel .withArgs(@user_id, @project_id) .yields(null, false) @@ -48,6 +51,17 @@ describe "AuthorizationManager", -> it "should return false", -> @callback.calledWith(null, false, false).should.equal true + describe "with a user_id who is an admin", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, true) + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, false) + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return the user as an owner", -> + @callback.calledWith(null, "owner", false).should.equal true + describe "with no user (anonymous)", -> beforeEach -> @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @callback @@ -55,6 +69,9 @@ describe "AuthorizationManager", -> it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false + it "should not call AuthorizationManager.isUserSiteAdmin", -> + @AuthorizationManager.isUserSiteAdmin.called.should.equal false + it "should return false", -> @callback.calledWith(null, false, false).should.equal true @@ -66,6 +83,7 @@ describe "AuthorizationManager", -> describe "with a user_id with a privilege level", -> beforeEach -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) @CollaboratorsHandler.getMemberIdPrivilegeLevel .withArgs(@user_id, @project_id) .yields(null, "readOnly") @@ -76,6 +94,7 @@ describe "AuthorizationManager", -> describe "with a user_id with no privilege level", -> beforeEach -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false) @CollaboratorsHandler.getMemberIdPrivilegeLevel .withArgs(@user_id, @project_id) .yields(null, false) @@ -84,6 +103,17 @@ describe "AuthorizationManager", -> it "should return the public privilege level", -> @callback.calledWith(null, "readAndWrite", true).should.equal true + describe "with a user_id who is an admin", -> + beforeEach -> + @AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, true) + @CollaboratorsHandler.getMemberIdPrivilegeLevel + .withArgs(@user_id, @project_id) + .yields(null, false) + @AuthorizationManager.getPrivilegeLevelForProject @user_id, @project_id, @callback + + it "should return the user as an owner", -> + @callback.calledWith(null, "owner", false).should.equal true + describe "with no user (anonymous)", -> beforeEach -> @AuthorizationManager.getPrivilegeLevelForProject null, @project_id, @callback @@ -91,6 +121,9 @@ describe "AuthorizationManager", -> it "should not call CollaboratorsHandler.getMemberIdPrivilegeLevel", -> @CollaboratorsHandler.getMemberIdPrivilegeLevel.called.should.equal false + it "should not call AuthorizationManager.isUserSiteAdmin", -> + @AuthorizationManager.isUserSiteAdmin.called.should.equal false + it "should return the public privilege level", -> @callback.calledWith(null, "readAndWrite", true).should.equal true diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index 5d54151483..177750c28d 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -134,11 +134,16 @@ describe "Authorization", -> @other1 = new User() @other2 = new User() @anon = new User() - async.series [ + @site_admin = new User({email: "admin@example.com"}) + async.parallel [ (cb) => @owner.login cb (cb) => @other1.login cb (cb) => @other2.login cb (cb) => @anon.getCsrfToken cb + (cb) => + @site_admin.login (err) => + return cb(err) if error? + @site_admin.ensure_admin cb ], done describe "private project", -> @@ -151,6 +156,9 @@ describe "Authorization", -> it "should allow the owner read access to it", (done) -> expect_read_access @owner, @project_id, done + it "should allow the owner write access to its content", (done) -> + expect_content_write_access @owner, @project_id, done + it "should allow the owner write access to its settings", (done) -> expect_settings_write_access @owner, @project_id, done @@ -160,6 +168,9 @@ describe "Authorization", -> it "should not allow another user read access to the project", (done) -> expect_no_read_access @other1, @project_id, redirect_to: "/restricted", done + it "should not allow another user write access to its content", (done) -> + expect_no_content_write_access @other1, @project_id, done + it "should not allow another user write access to its settings", (done) -> expect_no_settings_write_access @other1, @project_id, redirect_to: "/restricted", done @@ -169,11 +180,27 @@ describe "Authorization", -> it "should not allow anonymous user read access to it", (done) -> expect_no_read_access @anon, @project_id, redirect_to: "/restricted", done + it "should not allow anonymous user write access to its content", (done) -> + expect_no_content_write_access @anon, @project_id, done + it "should not allow anonymous user write access to its settings", (done) -> expect_no_settings_write_access @anon, @project_id, redirect_to: "/restricted", done it "should not allow anonymous user admin access to it", (done) -> expect_no_admin_access @anon, @project_id, redirect_to: "/restricted", done + + it "should allow site admin users read access to it", (done) -> + expect_read_access @site_admin, @project_id, done + + it "should allow site admin users write access to its content", (done) -> + expect_content_write_access @site_admin, @project_id, done + + it "should allow site admin users write access to its settings", (done) -> + expect_settings_write_access @site_admin, @project_id, done + + it "should allow site admin users admin access to it", (done) -> + expect_admin_access @site_admin, @project_id, done + describe "shared project", -> before (done) -> diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index f1c38029d7..c13a45499d 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -1,6 +1,6 @@ request = require("./request") settings = require("settings-sharelatex") -{db} = require("../../../../app/js/infrastructure/mongojs") +{db, ObjectId} = require("../../../../app/js/infrastructure/mongojs") count = 0 @@ -29,6 +29,9 @@ class User @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", From 2af2dd694e659d27797b01e6843ef5f962acd4fe Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 22 Mar 2016 09:39:25 +0000 Subject: [PATCH 074/208] Use null to represent anonymous user, as AuthorizationManager expects --- .../app/coffee/Features/Project/ProjectController.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index dc64f46bff..34a44994be 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -188,7 +188,7 @@ module.exports = ProjectController = anonymous = false else anonymous = true - user_id = 'openUser' + user_id = null project_id = req.params.Project_id logger.log project_id:project_id, "loading editor" @@ -197,14 +197,14 @@ module.exports = ProjectController = project: (cb)-> ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1}, cb user: (cb)-> - if user_id == 'openUser' + if !user_id? cb null, defaultSettingsForAnonymousUser(user_id) else User.findById user_id, (err, user)-> logger.log project_id:project_id, user_id:user_id, "got user" cb err, user subscription: (cb)-> - if user_id == 'openUser' + if !user_id? return cb() SubscriptionLocator.getUsersSubscription user_id, cb activate: (cb)-> From 59b131f251ae0d3f1ce5ecb5ac7a2766aeba7a5c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 Mar 2016 09:39:33 +0000 Subject: [PATCH 075/208] First pass at refactoring the output panel views. --- .../web/app/views/project/editor/pdf.jade | 181 +++++++++--------- .../ide/pdf/controllers/PdfController.coffee | 17 +- 2 files changed, 102 insertions(+), 96 deletions(-) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index 1338cffa47..dc5f5b43bf 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -32,7 +32,7 @@ div.full-size.pdf(ng-controller="PdfController") a.log-btn( href ng-click="toggleLogs()" - ng-class="{ 'active': (pdf.view == 'logs' || pdf.failure) && !pdf.error && !pdf.timeout && !pdf.uncompiled }" + ng-class="{ 'active': shouldShowLogs == true }" tooltip="#{translate('logs_and_output_files')}" tooltip-placement="bottom" ) @@ -75,98 +75,9 @@ div.full-size.pdf(ng-controller="PdfController") i.split-screen i.split-screen // end of toolbar - - .pdf-viewer(ng-show="pdf.url && pdf.view == 'pdf' && !pdf.failure && !pdf.timeout && !pdf.error && !pdf.renderingError && !pdf.clsiMaintenance && !pdf.tooRecentlyCompiled") - div( - pdfng - ng-if="settings.pdfViewer == 'pdfjs'" - pdf-src="pdf.url" - key="{{ project_id }}" - resize-on="layout:main:resize,layout:pdf:resize" - highlights="pdf.highlights" - position="pdf.position" - dbl-click-callback="syncToCode" - ) - iframe( - ng-src="{{ pdf.url }}" - ng-if="settings.pdfViewer == 'native'" - ) - - .pdf-uncompiled(ng-show="pdf.uncompiled && !pdf.compiling") - |   - i.fa.fa-level-up.fa-flip-horizontal.fa-2x - |   #{translate('click_here_to_preview_pdf')} - - .pdf-errors(ng-show="(pdf.timedout || pdf.error || pdf.renderingError || pdf.clsiMaintenance || pdf.tooRecentlyCompiled) && pdf.view != 'logs'") - .alert.alert-danger(ng-show="pdf.error || pdf.renderingError") - strong #{translate("server_error")} - span #{translate("somthing_went_wrong_compiling")} - - .alert.alert-danger(ng-show="pdf.clsiMaintenance") - strong #{translate("server_error")} - span #{translate("clsi_maintenance")} - - .alert.alert-danger(ng-show="pdf.tooRecentlyCompiled") - strong #{translate("server_error")} - span #{translate("too_recently_compiled")} - - .alert.alert-danger(ng-show="pdf.timedout") - p - strong #{translate("timedout")}. - span #{translate("proj_timed_out_reason")} - p - a.text-info(href="https://www.sharelatex.com/learn/Debugging_Compilation_timeout_errors", target="_blank") - | #{translate("learn_how_to_make_documents_compile_quickly")} - - .alert.alert-success(ng-show="pdf.timedout && !hasPremiumCompile") - p(ng-if="project.owner._id == user.id") - strong #{translate("upgrade_for_faster_compiles")} - p(ng-if="project.owner._id != user.id") - strong #{translate("ask_proj_owner_to_upgrade_for_faster_compiles")} - p #{translate("free_accounts_have_timeout_upgrade_to_increase")} - p Plus: - p - ul.list-unstyled - li - i.fa.fa-check   - | #{translate("unlimited_projects")} - - li - i.fa.fa-check   - | #{translate("collabs_per_proj", {collabcount:'Multiple'})} - - li - i.fa.fa-check   - | #{translate("full_doc_history")} - - li - i.fa.fa-check   - | #{translate("sync_to_dropbox")} - - li - i.fa.fa-check   - | #{translate("sync_to_github")} - - li - i.fa.fa-check   - |#{translate("compile_larger_projects")} - - p(ng-controller="FreeTrialModalController", ng-if="project.owner._id == user.id") - a.btn.btn-success.row-spaced-small( - href - ng-class="buttonClass" - sixpack-convert="track_changes_feature_info" - ng-click="startFreeTrial('compile-timeout')" - ) #{translate("start_free_trial")} - - .pdf-errors(ng-show="pdf.projectTooLarge") - .alert.alert-danger - strong #{translate("project_too_large")} - span #{translate("project_too_large_please_reduce")} - - - .pdf-logs(ng-show="(pdf.view == 'logs' || pdf.failure) && !pdf.error && !pdf.timeout && !pdf.uncompiled") + // views + .pdf-logs(ng-show="shouldShowLogs") .alert.alert-success(ng-show="pdf.logEntries.all.length == 0") | #{translate("no_errors_good_job")} @@ -218,6 +129,92 @@ div.full-size.pdf(ng-controller="PdfController") span(ng-show="pdf.showRawLog") #{translate("hide_raw_logs")} pre(ng-bind="pdf.rawLog", ng-show="pdf.showRawLog") + + + div(ng-show="!shouldShowLogs", ng-switch on="pdf.view") + .pdf-uncompiled(ng-switch-when="uncompiled" ng-show="!pdf.compiling") + |   + i.fa.fa-level-up.fa-flip-horizontal.fa-2x + |   #{translate('click_here_to_preview_pdf')} + + .pdf-viewer(ng-switch-when="pdf") + div( + pdfng + ng-if="settings.pdfViewer == 'pdfjs'" + pdf-src="pdf.url" + key="{{ project_id }}" + resize-on="layout:main:resize,layout:pdf:resize" + highlights="pdf.highlights" + position="pdf.position" + dbl-click-callback="syncToCode" + ) + iframe( + ng-src="{{ pdf.url }}" + ng-if="settings.pdfViewer == 'native'" + ) + + .pdf-errors(ng-switch-when="errors") + + .alert.alert-danger(ng-show="pdf.error || pdf.renderingError") + strong #{translate("server_error")} + span #{translate("somthing_went_wrong_compiling")} + + .alert.alert-danger(ng-show="pdf.clsiMaintenance") + strong #{translate("server_error")} + span #{translate("clsi_maintenance")} + + .alert.alert-danger(ng-show="pdf.tooRecentlyCompiled") + strong #{translate("server_error")} + span #{translate("too_recently_compiled")} + + .alert.alert-danger(ng-show="pdf.timedout") + p + strong #{translate("timedout")}. + span #{translate("proj_timed_out_reason")} + p + a.text-info(href="https://www.sharelatex.com/learn/Debugging_Compilation_timeout_errors", target="_blank") + | #{translate("learn_how_to_make_documents_compile_quickly")} + + .alert.alert-success(ng-show="pdf.timedout && !hasPremiumCompile") + p(ng-if="project.owner._id == user.id") + strong #{translate("upgrade_for_faster_compiles")} + p(ng-if="project.owner._id != user.id") + strong #{translate("ask_proj_owner_to_upgrade_for_faster_compiles")} + p #{translate("free_accounts_have_timeout_upgrade_to_increase")} + p Plus: + p + ul.list-unstyled + li + i.fa.fa-check   + | #{translate("unlimited_projects")} + li + i.fa.fa-check   + | #{translate("collabs_per_proj", {collabcount:'Multiple'})} + li + i.fa.fa-check   + | #{translate("full_doc_history")} + li + i.fa.fa-check   + | #{translate("sync_to_dropbox")} + li + i.fa.fa-check   + | #{translate("sync_to_github")} + li + i.fa.fa-check   + |#{translate("compile_larger_projects")} + p(ng-controller="FreeTrialModalController", ng-if="project.owner._id == user.id") + a.btn.btn-success.row-spaced-small( + href + ng-class="buttonClass" + sixpack-convert="track_changes_feature_info" + ng-click="startFreeTrial('compile-timeout')" + ) #{translate("start_free_trial")} + + .pdf-logs(ng-switch-when="logs") + + // // // // + + script(type='text/ng-template', id='clearCacheModalTemplate') .modal-header diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 413c59aa03..7c32e1f533 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -5,6 +5,8 @@ define [ ], (App, LogParser, BibLogParser) -> App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, localStorage) -> autoCompile = true + $scope.pdf.view = 'uncompiled' # uncompiled | pdf | errors + $scope.$on "project:joined", () -> return if !autoCompile autoCompile = false @@ -12,6 +14,7 @@ define [ $scope.hasPremiumCompile = $scope.project.features.compileGroup == "priority" $scope.$on "pdf:error:display", () -> + $scope.pdf.view = 'errors' $scope.pdf.renderingError = true $scope.draft = localStorage("draft:#{$scope.project_id}") or false @@ -41,19 +44,26 @@ define [ $scope.pdf.tooRecentlyCompiled = false if response.status == "timedout" + $scope.pdf.view = 'errors' $scope.pdf.timedout = true else if response.status == "autocompile-backoff" + $scope.pdf.view = 'errors' $scope.pdf.uncompiled = true else if response.status == "project-too-large" + $scope.pdf.view = 'errors' $scope.pdf.projectTooLarge = true else if response.status == "failure" + $scope.pdf.view = 'errors' $scope.pdf.failure = true fetchLogs() else if response.status == 'clsi-maintenance' + $scope.pdf.view = 'errors' $scope.pdf.clsiMaintenance = true else if response.status == "too-recently-compiled" + $scope.pdf.view = 'errors' $scope.pdf.tooRecentlyCompiled = true else if response.status == "success" + $scope.pdf.view = 'pdf' # define the base url $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf?cache_bust=#{Date.now()}" # add a query string parameter for the compile group @@ -176,14 +186,13 @@ define [ "X-Csrf-Token": window.csrfToken } + $scope.shouldShowLogs = false $scope.toggleLogs = () -> - if !$scope.pdf.view? or $scope.pdf.view == "pdf" - $scope.pdf.view = "logs" - else - $scope.pdf.view = "pdf" + $scope.shouldShowLogs = !$scope.shouldShowLogs $scope.showPdf = () -> $scope.pdf.view = "pdf" + $scope.shouldShowLogs = false $scope.toggleRawLog = () -> $scope.pdf.showRawLog = !$scope.pdf.showRawLog From f182fbf3963ebfd3a539edda52f1ecc52a1615fe Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 22 Mar 2016 09:53:47 +0000 Subject: [PATCH 076/208] Convert 'anonymous-user' from real-time api in 'null' internally --- .../Features/Editor/EditorHttpController.coffee | 2 ++ .../coffee/Editor/EditorHttpControllerTests.coffee | 11 +++++++++++ .../test/acceptance/coffee/AuthorizationTests.coffee | 6 +++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index ca4d70b1aa..379f20fe5b 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -15,6 +15,8 @@ module.exports = EditorHttpController = joinProject: (req, res, next) -> project_id = req.params.Project_id user_id = req.query.user_id + if user_id == "anonymous-user" + user_id = null logger.log {user_id, project_id}, "join project request" Metrics.inc "editor.join-project" EditorHttpController._buildJoinProjectView project_id, user_id, (error, project, privilegeLevel) -> diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee index 6a594f31ee..a98c34af28 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee @@ -77,6 +77,17 @@ describe "EditorHttpController", -> @ProjectDeleter.unmarkAsDeletedByExternalSource .calledWith(@project_id) .should.equal true + + describe "with an anonymous user", -> + beforeEach -> + @req.query = + user_id: "anonymous-user" + @EditorHttpController.joinProject @req, @res + + it "should pass the user id as null", -> + @EditorHttpController._buildJoinProjectView + .calledWith(@project_id, null) + .should.equal true describe "_buildJoinProjectView", -> beforeEach -> diff --git a/services/web/test/acceptance/coffee/AuthorizationTests.coffee b/services/web/test/acceptance/coffee/AuthorizationTests.coffee index 177750c28d..c6678656b9 100644 --- a/services/web/test/acceptance/coffee/AuthorizationTests.coffee +++ b/services/web/test/acceptance/coffee/AuthorizationTests.coffee @@ -56,9 +56,13 @@ try_admin_access = (user, project_id, test, callback) -> try_content_access = (user, project_id, test, callback) -> # The real-time service calls this end point to determine the user's # permissions. + if user.id? + user_id = user.id + else + user_id = "anonymous-user" request.post { url: "/project/#{project_id}/join" - qs: {user_id: user.id} + qs: {user_id} auth: user: settings.apis.web.user pass: settings.apis.web.pass From 4f3755318f8d16e7c01a982739c061dd156662c5 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 Mar 2016 10:24:58 +0000 Subject: [PATCH 077/208] Clean up. --- services/web/app/views/project/editor/pdf.jade | 6 ++---- .../public/coffee/ide/pdf/controllers/PdfController.coffee | 7 ++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index dc5f5b43bf..f633060aaf 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -76,7 +76,7 @@ div.full-size.pdf(ng-controller="PdfController") i.split-screen // end of toolbar - // views + // logs view .pdf-logs(ng-show="shouldShowLogs") .alert.alert-success(ng-show="pdf.logEntries.all.length == 0") | #{translate("no_errors_good_job")} @@ -131,6 +131,7 @@ div.full-size.pdf(ng-controller="PdfController") pre(ng-bind="pdf.rawLog", ng-show="pdf.showRawLog") + // non-log views (pdf and errors) div(ng-show="!shouldShowLogs", ng-switch on="pdf.view") .pdf-uncompiled(ng-switch-when="uncompiled" ng-show="!pdf.compiling") |   @@ -209,9 +210,6 @@ div.full-size.pdf(ng-controller="PdfController") sixpack-convert="track_changes_feature_info" ng-click="startFreeTrial('compile-timeout')" ) #{translate("start_free_trial")} - - .pdf-logs(ng-switch-when="logs") - // // // // diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 7c32e1f533..99add9a2d1 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -4,8 +4,10 @@ define [ "libs/bib-log-parser" ], (App, LogParser, BibLogParser) -> App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, localStorage) -> + autoCompile = true - $scope.pdf.view = 'uncompiled' # uncompiled | pdf | errors + $scope.pdf.view = 'uncompiled' # uncompiled | pdf | errors + $scope.shouldShowLogs = false $scope.$on "project:joined", () -> return if !autoCompile @@ -62,7 +64,7 @@ define [ else if response.status == "too-recently-compiled" $scope.pdf.view = 'errors' $scope.pdf.tooRecentlyCompiled = true - else if response.status == "success" + else if response.status == "success " $scope.pdf.view = 'pdf' # define the base url $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf?cache_bust=#{Date.now()}" @@ -186,7 +188,6 @@ define [ "X-Csrf-Token": window.csrfToken } - $scope.shouldShowLogs = false $scope.toggleLogs = () -> $scope.shouldShowLogs = !$scope.shouldShowLogs From 293cb3c73ea71c7f950b07dce3ca1a853e11cb07 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 Mar 2016 10:32:44 +0000 Subject: [PATCH 078/208] fix typo --- .../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 99add9a2d1..88fdbbc76a 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -64,7 +64,7 @@ define [ else if response.status == "too-recently-compiled" $scope.pdf.view = 'errors' $scope.pdf.tooRecentlyCompiled = true - else if response.status == "success " + else if response.status == "success" $scope.pdf.view = 'pdf' # define the base url $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf?cache_bust=#{Date.now()}" From 723031f7f3b569b01412e1fa13189c71e05e1e1e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 Mar 2016 10:32:55 +0000 Subject: [PATCH 079/208] un-show logs on successful run. --- .../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 88fdbbc76a..81df8e7053 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -66,6 +66,7 @@ define [ $scope.pdf.tooRecentlyCompiled = true else if response.status == "success" $scope.pdf.view = 'pdf' + $scope.shouldShowLogs = false # define the base url $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf?cache_bust=#{Date.now()}" # add a query string parameter for the compile group From 49bb4f07ea14fc9d63e4363fbb74b17888e73c31 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 22 Mar 2016 11:15:52 +0000 Subject: [PATCH 080/208] Move delete docs into view in file-tree element --- .../app/views/project/editor/file-tree.jade | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/services/web/app/views/project/editor/file-tree.jade b/services/web/app/views/project/editor/file-tree.jade index bb07ee9e88..6ca8ff68b8 100644 --- a/services/web/app/views/project/editor/file-tree.jade +++ b/services/web/app/views/project/editor/file-tree.jade @@ -70,23 +70,22 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected' ng-repeat="entity in rootFolder.children | orderBy:[orderByFoldersFirst, 'name']" ) - div(ng-show="deletedDocs.length > 0 && ui.view == 'track-changes'") - h3 #{translate("deleted_files")} - ul.list-unstyled.file-tree-list.deleted-docs - li( - ng-class="{ 'selected': entity.selected }", - ng-repeat="entity in deletedDocs | orderBy:'name'", - ng-controller="FileTreeEntityController" - ) - .entity - .entity-name( - ng-click="select($event)" - ) - //- Just a spacer to align with folders - i.fa.fa-fw.toggle - i.fa.fa-fw.fa-file + li(ng-show="deletedDocs.length > 0 && ui.view == 'track-changes'") + h3 #{translate("deleted_files")} + li( + ng-class="{ 'selected': entity.selected }", + ng-repeat="entity in deletedDocs | orderBy:'name'", + ng-controller="FileTreeEntityController" + ) + .entity + .entity-name( + ng-click="select($event)" + ) + //- Just a spacer to align with folders + i.fa.fa-fw.toggle + i.fa.fa-fw.fa-file - span {{ entity.name }} + span {{ entity.name }} script(type='text/ng-template', id='entityListItemTemplate') li( From aa1f2cc8a90aa73723e2e09f96ccf191a589d77f Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 22 Mar 2016 12:11:59 +0000 Subject: [PATCH 081/208] Remove #s in href on project list buttons --- .../app/views/project/list/project-list.jade | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/services/web/app/views/project/list/project-list.jade b/services/web/app/views/project/list/project-list.jade index 9e8c22a1e8..649c860b26 100644 --- a/services/web/app/views/project/list/project-list.jade +++ b/services/web/app/views/project/list/project-list.jade @@ -24,7 +24,7 @@ .btn-toolbar(ng-show="filter != 'archived'") .btn-group(ng-hide="selectedProjects.length < 1") a.btn.btn-default( - href='#', + href, tooltip="#{translate('download')}", tooltip-placement="bottom", tooltip-append-to-body="true", @@ -32,7 +32,7 @@ ) i.fa.fa-cloud-download a.btn.btn-default( - href='#', + href, tooltip="#{translate('delete')}", tooltip-placement="bottom", tooltip-append-to-body="true", @@ -42,7 +42,7 @@ .btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown) a.btn.btn-default.dropdown-toggle( - href="#", + href, data-toggle="dropdown", dropdown-toggle, tooltip="#{translate('add_to_folders')}", @@ -72,11 +72,11 @@ | {{tag.name}} li.divider li - a(href="#", ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")} + a(href, ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")} .btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown a.btn.btn-default.dropdown-toggle( - href='#', + href, data-toggle="dropdown", dropdown-toggle ) #{translate("more")} @@ -84,19 +84,19 @@ ul.dropdown-menu.dropdown-menu-right(role="menu") li(ng-show="getFirstSelectedProject().accessLevel == 'owner'") a( - href='#', + href, ng-click="openRenameProjectModal()" ) #{translate("rename")} li a( - href='#', + href, ng-click="openCloneProjectModal()" ) #{translate("make_copy")} .btn-toolbar(ng-show="filter == 'archived'") .btn-group(ng-hide="selectedProjects.length < 1") a.btn.btn-default( - href='#', + href, data-original-title="Restore", data-toggle="tooltip", data-placement="bottom", @@ -105,7 +105,7 @@ .btn-group(ng-hide="selectedProjects.length < 1") a.btn.btn-danger( - href='#', + href, data-original-title="Delete Forever", data-toggle="tooltip", data-placement="bottom", From 4595c9036363f3ec18494f726e27a4cfcb636b01 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 22 Mar 2016 12:12:11 +0000 Subject: [PATCH 082/208] Use translations for action in project delete modal --- services/web/app/views/project/list/modals.jade | 4 +++- .../coffee/main/project-list/modal-controllers.coffee | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/project/list/modals.jade b/services/web/app/views/project/list/modals.jade index e0fe24ad14..4f66507289 100644 --- a/services/web/app/views/project/list/modals.jade +++ b/services/web/app/views/project/list/modals.jade @@ -183,7 +183,9 @@ script(type='text/ng-template', id='deleteProjectsModalTemplate') data-dismiss="modal" ng-click="cancel()" ) × - h3 {{action}} #{translate("projects")} + h3(ng-if="action == 'delete'") #{translate("delete_projects")} + h3(ng-if="action == 'leave'") #{translate("leave_projects")} + h3(ng-if="action == 'delete-and-leave'") #{translate("delete_and_leave_projects")} .modal-body div(ng-show="projectsToDelete.length > 0") p #{translate("about_to_delete_projects")} diff --git a/services/web/public/coffee/main/project-list/modal-controllers.coffee b/services/web/public/coffee/main/project-list/modal-controllers.coffee index a841e369a8..dbf3c613ac 100644 --- a/services/web/public/coffee/main/project-list/modal-controllers.coffee +++ b/services/web/public/coffee/main/project-list/modal-controllers.coffee @@ -65,11 +65,11 @@ define [ $scope.projectsToLeave = projects.filter (project) -> project.accessLevel != "owner" if $scope.projectsToLeave.length > 0 and $scope.projectsToDelete.length > 0 - $scope.action = "Delete & Leave" + $scope.action = "delete-and-leave" else if $scope.projectsToLeave.length == 0 and $scope.projectsToDelete.length > 0 - $scope.action = "Delete" + $scope.action = "delete" else - $scope.action = "Leave" + $scope.action = "leave" $scope.delete = () -> $modalInstance.close() From 69a7a1508bb65bb146f08e88b16c915902f7c0fb Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 22 Mar 2016 12:28:37 +0000 Subject: [PATCH 083/208] modal != modal instance --- services/web/app/views/layout.jade | 1 - services/web/public/coffee/main/contact-us.coffee | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index f407143e9d..4346629ade 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -7,7 +7,6 @@ html(itemscope, itemtype='http://schema.org/Product') // Stop superfish from loading window.similarproducts = true style [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {display: none !important; display: none; } - style #groove-button { display:none;} -if (typeof(gaExperiments) != "undefined") |!{gaExperiments} diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee index b5e52800fe..2a3d49d4c4 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -10,7 +10,7 @@ define [ controller: "SupportModalController" ) - App.controller 'SupportModalController', ($scope, $modal) -> + App.controller 'SupportModalController', ($scope, $modalInstance) -> $scope.form = {} $scope.sent = false $scope.sending = false @@ -31,7 +31,6 @@ define [ $scope.sent = true $scope.$apply() - $scope.close = () -> $modalInstance.close() From fbfd160c6d5b6722fb85b81f9193046f6331ffe9 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 22 Mar 2016 14:02:15 +0000 Subject: [PATCH 084/208] default message to empty string --- services/web/public/coffee/main/contact-us.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee index 2a3d49d4c4..9596468ec5 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -22,9 +22,9 @@ define [ ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32) params = email: $scope.form.email - message: $scope.form.message + message: $scope.form.message or "" subject: $scope.form.subject + " - [#{ticketNumber}]" - about : $scope.form.project_url + about : $scope.form.project_url or "" labels: "support" Groove.createTicket params, (err, json)-> From a597ebfd90de1f423bc8ef970ffde21de9a2b60c Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 22 Mar 2016 14:13:50 +0000 Subject: [PATCH 085/208] Only show deleted files in track changes view --- services/web/app/views/project/editor/file-tree.jade | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/file-tree.jade b/services/web/app/views/project/editor/file-tree.jade index 6ca8ff68b8..630cd1aa17 100644 --- a/services/web/app/views/project/editor/file-tree.jade +++ b/services/web/app/views/project/editor/file-tree.jade @@ -75,7 +75,8 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected' li( ng-class="{ 'selected': entity.selected }", ng-repeat="entity in deletedDocs | orderBy:'name'", - ng-controller="FileTreeEntityController" + ng-controller="FileTreeEntityController", + ng-show="ui.view == 'track-changes'" ) .entity .entity-name( From 7fb0249b2bafc62957378a849d639e161ec3be92 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 22 Mar 2016 14:19:46 +0000 Subject: [PATCH 086/208] null check subscription for custom accounts page --- .../Features/Subscription/SubscriptionController.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index fb378e45f6..f0026bd7f5 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -85,7 +85,7 @@ module.exports = SubscriptionController = LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)-> groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user) if subscription?.customAccount - logger.log user: user, "redirecting to plans" + logger.log user: user, "redirecting to custom account page" res.redirect "/user/subscription/custom_account" else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember logger.log user:user, "redirecting to group subscription invite page" @@ -111,6 +111,10 @@ module.exports = SubscriptionController = userCustomSubscriptionPage: (req, res, next)-> AuthenticationController.getLoggedInUser req, (error, user) -> LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)-> + if !subscription? + err = new Error("subscription null for custom account, user:#{user?._id}") + logger.warn err:err, "subscription is null for custom accounts page" + return next(err) res.render "subscriptions/custom_account", title: "your_subscription" subscription: subscription From a89edde139263aa85c6d27245223a1471beffcc1 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 22 Mar 2016 14:27:05 +0000 Subject: [PATCH 087/208] subscription tests fixed --- .../coffee/Subscription/SubscriptionControllerTests.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee index 631e4b57b6..f2abbe0219 100644 --- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee @@ -68,7 +68,9 @@ describe "SubscriptionController sanboxed", -> "./LimitationsManager": @LimitationsManager "../../infrastructure/GeoIpLookup":@GeoIpLookup './RecurlyWrapper': @RecurlyWrapper - "logger-sharelatex": log:-> + "logger-sharelatex": + log:-> + warn:-> "settings-sharelatex": @settings "./SubscriptionDomainHandler":@SubscriptionDomainHandler @@ -273,7 +275,7 @@ describe "SubscriptionController sanboxed", -> describe "userCustomSubscriptionPage", -> beforeEach (done) -> @res.callback = done - @LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, true) + @LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, true, {}) @SubscriptionController.userCustomSubscriptionPage @req, @res it "should render the page", (done)-> From d5c280dc4d4d3dd4f93b5d9d1501dffd1f536aa3 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 Mar 2016 14:38:48 +0000 Subject: [PATCH 088/208] Show the pdf if it was already loaded. This ensures the pdf will be visible if the user switches to fullscreen-view. --- .../public/coffee/ide/pdf/controllers/PdfController.coffee | 4 +++- 1 file changed, 3 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 81df8e7053..fe37da4937 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -6,7 +6,9 @@ define [ App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, localStorage) -> autoCompile = true - $scope.pdf.view = 'uncompiled' # uncompiled | pdf | errors + + # pdf.view = uncompiled | pdf | errors + $scope.pdf.view = if $scope?.pdf?.url then 'pdf' else 'uncompiled' $scope.shouldShowLogs = false $scope.$on "project:joined", () -> From 12b0e982c8686255d51493db28ea9952f4ece313 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 Mar 2016 14:42:00 +0000 Subject: [PATCH 089/208] Don't raise an error if `output.blg` cannot be found. --- .../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 fe37da4937..003d2a45ba 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -137,7 +137,7 @@ define [ entries.warnings = entries.warnings.concat(biberLogEntries.warnings) proceed() .error (e) -> - console.error ">> error", e + # it's not an error for the output.blg file to not be present proceed() # # # # .error () -> From 9706585d21ebed9ac7db2a413c119446a7a9bee4 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 22 Mar 2016 14:41:31 +0000 Subject: [PATCH 090/208] Fix translate in delete modal confirm button --- services/web/app/views/project/list/modals.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/list/modals.jade b/services/web/app/views/project/list/modals.jade index 4f66507289..9111b2a347 100644 --- a/services/web/app/views/project/list/modals.jade +++ b/services/web/app/views/project/list/modals.jade @@ -203,7 +203,7 @@ script(type='text/ng-template', id='deleteProjectsModalTemplate') ) #{translate("cancel")} button.btn.btn-danger( ng-click="delete()" - ) {{action}} + ) #{translate("confirm")} script(type="text/ng-template", id="uploadProjectModalTemplate") .modal-header From 2c321a48035acc9623db46103aa8513a5a3d5119 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 Mar 2016 15:51:05 +0000 Subject: [PATCH 091/208] If the compile request errors out, set view to 'errors'. --- .../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 003d2a45ba..7a68850d6c 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -179,6 +179,7 @@ define [ .error () -> $scope.pdf.compiling = false $scope.pdf.error = true + $scope.pdf.view = 'errors' # This needs to be public. ide.$scope.recompile = $scope.recompile From a6859e22b3b358c0b3552f71860694b91e98b540 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 22 Mar 2016 16:04:33 +0000 Subject: [PATCH 092/208] if user is member of group on downgrade use the group subscription --- .../Subscription/SubscriptionLocator.coffee | 5 ++- .../Subscription/SubscriptionUpdater.coffee | 18 ++++++++-- .../SubscriptionUpdaterTests.coffee | 36 ++++++++++++++++++- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee index 9285f8e575..c728ea65cd 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee @@ -22,4 +22,7 @@ module.exports = Subscription.findOne _id:subscription_id, callback getSubscriptionByMemberIdAndId: (user_id, subscription_id, callback)-> - Subscription.findOne member_ids: user_id, _id:subscription_id, {_id:1}, callback + Subscription.findOne {member_ids: user_id, _id:subscription_id}, {_id:1}, callback + + getGroupSubscriptionMemberOf: (user_id, callback)-> + Subscription.findOne {member_ids: user_id}, {_id:1}, callback \ No newline at end of file diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index 18f9058b67..69d31d7277 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -16,10 +16,24 @@ module.exports = syncSubscription: (recurlySubscription, adminUser_id, callback) -> self = @ logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "syncSubscription, creating new if subscription does not exist" - SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)-> + jobs = + subscription: (cb)-> + SubscriptionLocator.getUsersSubscription adminUser_id, cb + groupSubscription: (cb)-> + SubscriptionLocator.getGroupSubscriptionMemberOf adminUser_id, cb + async.series jobs, (err, results)-> + {subscription, groupSubscription} = results if subscription? logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "subscription does exist" - self._updateSubscription recurlySubscription, subscription, callback + self._updateSubscription recurlySubscription, subscription, (err)-> + if err? + logger.err err:err, adminUser_id:adminUser_id, "error syncing subscription" + return callback(err) + if groupSubscription? and recurlySubscription.state == "expired" + logger.log adminUser_id:adminUser_id, "subscription does exist" + UserFeaturesUpdater.updateFeatures adminUser_id, groupSubscription.planCode, callback + else + callback() else logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "subscription does not exist, creating a new one" self._createNewSubscription adminUser_id, (err, subscription)-> diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee index 3fab075c30..924dc42ef2 100644 --- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee @@ -23,6 +23,14 @@ describe "Subscription Updater", -> freeTrial:{} plan_code:"student_or_something" + @groupSubscription = + admin_id: @adminUser._id + members_id: @allUserIds + save: sinon.stub().callsArgWith(0) + freeTrial:{} + plan_code:"group_subscription" + + @updateStub = sinon.stub().callsArgWith(2, null) @findAndModifyStub = sinon.stub().callsArgWith(2, null, @subscription) @SubscriptionModel = class @@ -34,6 +42,7 @@ describe "Subscription Updater", -> @SubscriptionLocator = getUsersSubscription: sinon.stub() + getGroupSubscriptionMemberOf:sinon.stub() @Settings = freeTrialPlanCode: "collaborator" @@ -58,16 +67,41 @@ describe "Subscription Updater", -> describe "syncSubscription", -> - it "should update the subscription if the user already is admin of one", (done)-> + + beforeEach -> + @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) + @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, @groupSubscription) @SubscriptionUpdater._updateSubscription = sinon.stub().callsArgWith(2) + + it "should update the subscription if the user already is admin of one", (done)-> @SubscriptionUpdater._createNewSubscription = sinon.stub() + @SubscriptionUpdater.syncSubscription @recurlySubscription, @adminUser._id, (err)=> @SubscriptionLocator.getUsersSubscription.calledWith(@adminUser._id).should.equal true @SubscriptionUpdater._updateSubscription.called.should.equal true @SubscriptionUpdater._updateSubscription.calledWith(@recurlySubscription, @subscription).should.equal true done() + it "should sync with the group subscription if the recurly subscription is expired", (done)-> + @recurlySubscription.state = "expired" + + @SubscriptionUpdater.syncSubscription @recurlySubscription, @adminUser._id, (err)=> + @SubscriptionLocator.getUsersSubscription.calledWith(@adminUser._id).should.equal true + @SubscriptionUpdater._updateSubscription.called.should.equal true + @SubscriptionUpdater._updateSubscription.calledWith(@recurlySubscription, @subscription).should.equal true + @UserFeaturesUpdater.updateFeatures.calledWith(@adminUser._id, @groupSubscription.planCode).should.equal true + done() + + it "should not call updateFeatures with group subscription if recurly subscription is not expired", (done)-> + + @SubscriptionUpdater.syncSubscription @recurlySubscription, @adminUser._id, (err)=> + @SubscriptionLocator.getUsersSubscription.calledWith(@adminUser._id).should.equal true + @SubscriptionUpdater._updateSubscription.called.should.equal true + @SubscriptionUpdater._updateSubscription.calledWith(@recurlySubscription, @subscription).should.equal true + @UserFeaturesUpdater.updateFeatures.called.should.equal false + done() + describe "_updateSubscription", -> From 62ececeef814fc5a253ae7b495259d014f3af421 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 Mar 2016 16:59:40 +0000 Subject: [PATCH 093/208] show logs on failure. --- .../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 7a68850d6c..5847b58e3e 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -59,6 +59,7 @@ define [ else if response.status == "failure" $scope.pdf.view = 'errors' $scope.pdf.failure = true + $scope.shouldShowLogs = true fetchLogs() else if response.status == 'clsi-maintenance' $scope.pdf.view = 'errors' From c554b0630d8bf610a05982e05656a0072ab5e846 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 22 Mar 2016 16:59:47 +0000 Subject: [PATCH 094/208] if user has subscription before joining group is given option to cancel personal one --- .../SubscriptionGroupController.coffee | 16 +++++++++--- .../app/views/subscriptions/group/invite.jade | 23 ++++++++++++++--- ...roup-subscription-invite-controller.coffee | 25 ++++++++++++++++--- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee index af8b2414f3..5b20608eea 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee @@ -4,6 +4,7 @@ SubscriptionLocator = require("./SubscriptionLocator") ErrorsController = require("../Errors/ErrorController") SubscriptionDomainHandler = require("./SubscriptionDomainHandler") _ = require("underscore") +async = require("async") module.exports = @@ -53,19 +54,26 @@ module.exports = subscription: subscription renderGroupInvitePage: (req, res)-> - subscription_id = req.params.subscription_id + group_subscription_id = req.params.subscription_id user_id = req.session.user._id - licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id) + licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(group_subscription_id) if !licence? return ErrorsController.notFound(req, res) - SubscriptionGroupHandler.isUserPartOfGroup user_id, licence.subscription_id, (err, partOfGroup)-> + jobs = + partOfGroup: (cb)-> + SubscriptionGroupHandler.isUserPartOfGroup user_id, licence.group_subscription_id, cb + subscription: (cb)-> + SubscriptionLocator.getUsersSubscription user_id, cb + async.series jobs, (err, results)-> + {partOfGroup, subscription} = results if partOfGroup return res.redirect("/user/subscription/custom_account") else res.render "subscriptions/group/invite", title: "Group Invitation" - subscription_id:subscription_id + group_subscription_id:group_subscription_id licenceName:licence.name + has_personal_subscription: subscription? beginJoinGroup: (req, res)-> subscription_id = req.params.subscription_id diff --git a/services/web/app/views/subscriptions/group/invite.jade b/services/web/app/views/subscriptions/group/invite.jade index 1885ea20b8..3ea26aa0a1 100644 --- a/services/web/app/views/subscriptions/group/invite.jade +++ b/services/web/app/views/subscriptions/group/invite.jade @@ -2,7 +2,8 @@ extends ../../layout block scripts script(type='text/javascript'). - window.subscription_id = '#{subscription_id}' + window.group_subscription_id = '#{group_subscription_id}' + window.has_personal_subscription = '#{has_personal_subscription}' block content .content.content-alt @@ -11,12 +12,26 @@ block content .col-md-8.col-md-offset-2 -if (query.expired) .alert.alert-warning #{translate("email_link_expired")} + + .row + div   .row .col-md-8.col-md-offset-2(ng-cloak) .card(ng-controller="GroupSubscriptionInviteController") .page-header h1.text-centered #{translate("you_are_invited_to_group", {groupName:licenceName})} - div(ng-show="!requestSent").row.text-centered + + div(ng-show="view =='personalSubscription'").row.text-centered + div #{translate("cancel_personal_subscription_first")} + .row + .col-md-12   + .row + .col-md-12 + a.btn.btn.btn-default(ng-click="keepPersonalSubscription()", ng-disabled="inflight") #{translate("not_now")} + span   + a.btn.btn.btn-primary(ng-click="cancelSubscription()", ng-disabled="inflight") #{translate("cancel_your_subscription")} + + div(ng-show="view =='groupSubscriptionInvite'").row.text-centered .row .col-md-12 #{translate("group_provides_you_with_premium_account", {groupName:licenceName})} .row @@ -26,10 +41,10 @@ block content .text-center a.btn.btn-default(href="/project") #{translate("not_now")} span   - a.btn.btn.btn-primary(ng-click="joinGroup()") #{translate("verify_email_address")} + a.btn.btn.btn-primary(ng-click="joinGroup()", ng-disabled="inflight") #{translate("verify_email_address")} - span(ng-show="requestSent").row.text-centered.text-center + span(ng-show="view =='requestSent'").row.text-centered.text-center .row .col-md-12 #{translate("check_email_to_complete_the_upgrade")} .row diff --git a/services/web/public/coffee/main/subscription/group-subscription-invite-controller.coffee b/services/web/public/coffee/main/subscription/group-subscription-invite-controller.coffee index ccfae4a3cf..eadd9e2d83 100644 --- a/services/web/public/coffee/main/subscription/group-subscription-invite-controller.coffee +++ b/services/web/public/coffee/main/subscription/group-subscription-invite-controller.coffee @@ -3,12 +3,31 @@ define [ ], (App) -> App.controller "GroupSubscriptionInviteController", ($scope, $http) -> - $scope.requestSent = false + $scope.inflight = false + + if has_personal_subscription + $scope.view = "personalSubscription" + else + $scope.view = "groupSubscriptionInvite" + + $scope.keepPersonalSubscription = -> + $scope.view = "groupSubscriptionInvite" + + $scope.cancelSubscription = -> + $scope.inflight = true + request = $http.post "/user/subscription/cancel", {_csrf:window.csrfToken} + request.success (data, status)-> + $scope.inflight = false + $scope.view = "groupSubscriptionInvite" + request.error (data, status)-> + console.log "the request failed" $scope.joinGroup = -> - $scope.requestSent = true - request = $http.post "/user/subscription/#{subscription_id}/group/begin-join", {_csrf:window.csrfToken} + $scope.view = "requestSent" + $scope.inflight = true + request = $http.post "/user/subscription/#{group_subscription_id}/group/begin-join", {_csrf:window.csrfToken} request.success (data, status)-> + $scope.inflight = false if status != 200 # assume request worked $scope.requestSent = false request.error (data, status)-> From 9cfbbfa504c9d3053bcb2e74dbf536cedfd98b81 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 22 Mar 2016 22:16:38 +0000 Subject: [PATCH 095/208] template search works nicely, needs styling --- services/web/app/views/wiki/page.jade | 53 ++++++++++--------- .../coffee/analytics/AbTestingManager.coffee | 8 +-- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/services/web/app/views/wiki/page.jade b/services/web/app/views/wiki/page.jade index e93206310a..a18b49b6ec 100644 --- a/services/web/app/views/wiki/page.jade +++ b/services/web/app/views/wiki/page.jade @@ -3,34 +3,35 @@ extends ../layout block content .content.content-alt(ng-cloak) .container.wiki - .row.template-page-header(ng-controller="SearchWikiController") - .col-xs-3 - .col-md-8 - form.project-search.form-horizontal(role="form") - .form-group.has-feedback.has-feedback-left.col-md-12 - input.form-control.col-md-12(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....") - i.fa.fa-search.form-control-feedback-left - i.fa.fa-times.form-control-feedback( - ng-click="clearSearchText()", - style="cursor: pointer;", - ng-show="searchQueryText.length > 0" - ) + .row.template-page-header .col-md-8(ng-cloak) - ul.list-unstyled - li(ng-repeat='hit in hits') - .thumbnail.searchResult - .row - a(ng-href='{{hit.url}}') - .col-md-3 - img(ng-src='{{hit.image_url}}') - .col-md-7 - h1(ng-bind-html='hit.name') - p(ng-bind-html='hit.description') - .row(ng-non-bindable) - .col-xs-3.contents + + .row + .col-xs-3.contents(ng-non-bindable) | !{contents.content} - .col-xs-9.page - .card + .col-xs-9.page(ng-controller="SearchWikiController") + .row + .col-md-12 + form.project-search.form-horizontal(role="form") + .form-group.has-feedback.has-feedback-left.col-md-12 + input.form-control.col-md-12(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....") + i.fa.fa-search.form-control-feedback-left + i.fa.fa-times.form-control-feedback( + ng-click="clearSearchText()", + style="cursor: pointer;", + ng-show="searchQueryText.length > 0" + ) + .row + .col-md-12(ng-cloak) + ul.list-unstyled + li(ng-repeat='hit in hits') + .searchResult + .row + .col.-md-12 + a(ng-href='{{hit.url}}') + span(ng-bind-html='hit.name') + + .card(ng-non-bindable) .page-header h1 #{title} diff --git a/services/web/public/coffee/analytics/AbTestingManager.coffee b/services/web/public/coffee/analytics/AbTestingManager.coffee index bc40f0ce6f..23741dbaa9 100644 --- a/services/web/public/coffee/analytics/AbTestingManager.coffee +++ b/services/web/public/coffee/analytics/AbTestingManager.coffee @@ -86,10 +86,11 @@ define [ $scope.$apply(fn) buildHitViewModel = (hit)-> - page_underscored = hit.title.replace(/\s/g,'_') + page_underscored = hit.pageName.replace(/\s/g,'_') + section_underscored = hit.sectionName.replace(/\s/g,'_') result = - name : hit._highlightResult.title.value - url :"/learn/#{page_underscored}" + name : hit._highlightResult.pageName.value + " - " + hit._highlightResult.sectionName.value + url :"/learn/#{page_underscored}##{section_underscored}" console.log result return result @@ -99,6 +100,7 @@ define [ $scope.search = -> query = $scope.searchQueryText + console.log query if !query? or query.length == 0 updateHits [] return From de0aa1035de239cda2bccf8a3ec5e43bb6e599fd Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 22 Mar 2016 22:29:18 +0000 Subject: [PATCH 096/208] pulled tempaltes search into its own file --- .../coffee/analytics/AbTestingManager.coffee | 48 ------------------- services/web/public/coffee/main.coffee | 1 + 2 files changed, 1 insertion(+), 48 deletions(-) diff --git a/services/web/public/coffee/analytics/AbTestingManager.coffee b/services/web/public/coffee/analytics/AbTestingManager.coffee index 23741dbaa9..950f4f89f8 100644 --- a/services/web/public/coffee/analytics/AbTestingManager.coffee +++ b/services/web/public/coffee/analytics/AbTestingManager.coffee @@ -64,54 +64,6 @@ define [ bucketIndex = parseInt(hash.toString().slice(0,2), 16) % (buckets?.length or 2) return buckets[bucketIndex] - - App.factory "algoliawiki", -> - client = new AlgoliaSearch("SK53GL4JLY", "e398f35d3074fde57ca6d6c88d8be37c") - index = client.initIndex("lean-wiki-index") - return index - - App.controller "SearchWikiController", ($scope, algoliawiki, _) -> - algolia = algoliawiki - $scope.hits = [] - - $scope.clearSearchText = -> - $scope.searchQueryText = "" - updateHits [] - - $scope.safeApply = (fn)-> - phase = $scope.$root.$$phase - if(phase == '$apply' || phase == '$digest') - $scope.$eval(fn) - else - $scope.$apply(fn) - - buildHitViewModel = (hit)-> - page_underscored = hit.pageName.replace(/\s/g,'_') - section_underscored = hit.sectionName.replace(/\s/g,'_') - result = - name : hit._highlightResult.pageName.value + " - " + hit._highlightResult.sectionName.value - url :"/learn/#{page_underscored}##{section_underscored}" - console.log result - return result - - updateHits = (hits)-> - $scope.safeApply -> - $scope.hits = hits - - $scope.search = -> - query = $scope.searchQueryText - console.log query - if !query? or query.length == 0 - updateHits [] - return - - algolia.search query, (err, response)-> - if response.hits.length == 0 - updateHits [] - else - hits = _.map response.hits, buildHitViewModel - updateHits hits - App.controller "AbTestController", ($scope, abTestManager)-> testKeys = _.keys(window.ab) diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index 40c6bd753d..de36e33c34 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -16,6 +16,7 @@ define [ "main/register-users" "main/subscription/group-subscription-invite-controller" "main/contact-us" + "main/templates" "analytics/AbTestingManager" "directives/asyncForm" "directives/stopPropagation" From 3d8ac9f2923ef0258f2ea6a092b617485c758f51 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 23 Mar 2016 10:54:36 +0000 Subject: [PATCH 097/208] Add in a few null checks These were discovered by using a project that was messed up with the moving folder bug where the folder ended up without an id, docs or fileRefs array --- .../Features/Project/ProjectEditorHandler.coffee | 6 +++--- .../Features/Project/ProjectEntityHandler.coffee | 12 +++++++----- .../Features/References/ReferencesHandler.coffee | 6 +++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index a3d8319424..4e4991a855 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -46,12 +46,12 @@ module.exports = ProjectEditorHandler = signUpDate : user.signUpDate buildFolderModelView: (folder) -> - fileRefs = _.filter folder.fileRefs, (file)-> file? + fileRefs = _.filter (folder.fileRefs or []), (file)-> file? _id : folder._id name : folder.name - folders : @buildFolderModelView childFolder for childFolder in folder.folders + folders : @buildFolderModelView childFolder for childFolder in (folder.folders or []) fileRefs : @buildFileModelView file for file in fileRefs - docs : @buildDocModelView doc for doc in folder.docs + docs : @buildDocModelView doc for doc in (folder.docs or []) buildFileModelView: (file) -> _id : file._id diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index fff91d2ce3..e039f60ca6 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -22,7 +22,9 @@ module.exports = ProjectEntityHandler = folders = {} processFolder = (basePath, folder) -> folders[basePath] = folder - processFolder path.join(basePath, childFolder.name), childFolder for childFolder in folder.folders + for childFolder in (folder.folders or []) + if childFolder.name? + processFolder path.join(basePath, childFolder.name), childFolder ProjectGetter.getProjectWithoutDocLines project_id, (err, project) -> return callback(err) if err? @@ -43,11 +45,11 @@ module.exports = ProjectEntityHandler = for docContent in docContentsArray docContents[docContent._id] = docContent - ProjectEntityHandler.getAllFolders project_id, (error, folders) -> + ProjectEntityHandler.getAllFolders project_id, (error, folders = {}) -> return callback(error) if error? docs = {} for folderPath, folder of folders - for doc in folder.docs + for doc in (folder.docs or []) content = docContents[doc._id.toString()] if content? docs[path.join(folderPath, doc.name)] = { @@ -61,11 +63,11 @@ module.exports = ProjectEntityHandler = getAllFiles: (project_id, callback) -> logger.log project_id:project_id, "getting all files for project" - @getAllFolders project_id, (err, folders) -> + @getAllFolders project_id, (err, folders = {}) -> return callback(err) if err? files = {} for folderPath, folder of folders - for file in folder.fileRefs + for file in (folder.fileRefs or []) if file? files[path.join(folderPath, file.name)] = file callback null, files diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index bd31345f87..c033a4999f 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -20,13 +20,13 @@ module.exports = ReferencesHandler = ids = [] _process = (folder) -> - folder.docs.forEach (doc) -> + (folder.docs or []).forEach (doc) -> if doc?.name?.match(/^.*\.bib$/) ids.push(doc._id) - folder.folders.forEach (folder) -> + (folder.folders or []).forEach (folder) -> _process(folder) - project.rootFolder.forEach (rootFolder) -> + (project.rootFolder or []).forEach (rootFolder) -> _process(rootFolder) return ids From bad9a0282ded53fe0b5469a0a63a17dadca99631 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 23 Mar 2016 14:38:11 +0000 Subject: [PATCH 098/208] Add missing error handler --- .../web/app/coffee/Features/FileStore/FileStoreHandler.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee index 09790cf99f..eaf9c87554 100644 --- a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee +++ b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee @@ -43,6 +43,8 @@ module.exports = FileStoreHandler = uri: "#{@_buildUrl(project_id, file_id)}#{queryString}" timeout:fiveMinsInMs readStream = request(opts) + readStream.on "error", (err) -> + logger.err {err, project_id, file_id, query}, "error in file stream" callback(null, readStream) deleteFile: (project_id, file_id, callback)-> From a95c941cb9055016eaba1c706c1aab16a47c0363 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 23 Mar 2016 15:13:32 +0000 Subject: [PATCH 099/208] Fix FileStore unit tests --- .../coffee/FileStore/FileStoreHandlerTests.coffee | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/web/test/UnitTests/coffee/FileStore/FileStoreHandlerTests.coffee b/services/web/test/UnitTests/coffee/FileStore/FileStoreHandlerTests.coffee index 8f9d8a555e..3b546c74a6 100644 --- a/services/web/test/UnitTests/coffee/FileStore/FileStoreHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/FileStore/FileStoreHandlerTests.coffee @@ -19,7 +19,7 @@ describe "FileStoreHandler", -> on: (type, cb)-> if type == "end" cb() - @readStream = {my:"readStream"} + @readStream = {my:"readStream", on: sinon.stub()} @request = sinon.stub() @settings = apis:{filestore:{url:"http//filestore.sharelatex.test"}} @handler = SandboxedModule.require modulePath, requires: @@ -128,6 +128,12 @@ describe "FileStoreHandler", -> @handler.getFileStream @project_id, @file_id, {}, (err, stream)=> @handler._buildUrl.calledWith(@project_id, @file_id).should.equal true done() + + it "should add an error handler", (done) -> + @handler.getFileStream @project_id, @file_id, {}, (err, stream)=> + stream.on.calledWith("error").should.equal true + done() + describe "copyFile", -> From ae5467894bd21df4bdbc49296ed101ff41962759 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 23 Mar 2016 15:14:49 +0000 Subject: [PATCH 100/208] Standardise on one Errors file --- services/web/app.coffee | 1 - .../Features/Documents/DocumentController.coffee | 1 - .../app/coffee/Features/Errors/ErrorController.coffee | 2 +- .../Features/Project/ProjectEntityHandler.coffee | 2 +- .../app/coffee/Features/Project/ProjectGetter.coffee | 1 - .../app/coffee/Features/Project/ProjectLocator.coffee | 2 +- services/web/app/coffee/errors.coffee | 10 ---------- services/web/app/coffee/models/Project.coffee | 2 +- .../coffee/Documents/DocumentControllerTests.coffee | 2 +- .../coffee/Project/ProjectEntityHandlerTests.coffee | 2 +- .../coffee/Project/ProjectLocatorTests.coffee | 2 +- 11 files changed, 7 insertions(+), 20 deletions(-) delete mode 100644 services/web/app/coffee/errors.coffee diff --git a/services/web/app.coffee b/services/web/app.coffee index 8496af8f17..d8590689b3 100644 --- a/services/web/app.coffee +++ b/services/web/app.coffee @@ -10,7 +10,6 @@ metrics = require("metrics-sharelatex") metrics.initialize("web") metrics.memory.monitor(logger) Server = require("./app/js/infrastructure/Server") -Errors = require "./app/js/errors" argv = require("optimist") .options("user", {alias : "u", description : "Run the server with permissions of the specified user"}) diff --git a/services/web/app/coffee/Features/Documents/DocumentController.coffee b/services/web/app/coffee/Features/Documents/DocumentController.coffee index 8cae627ab6..ba74fc47da 100644 --- a/services/web/app/coffee/Features/Documents/DocumentController.coffee +++ b/services/web/app/coffee/Features/Documents/DocumentController.coffee @@ -1,5 +1,4 @@ ProjectEntityHandler = require "../Project/ProjectEntityHandler" -Errors = require "../../errors" logger = require("logger-sharelatex") module.exports = diff --git a/services/web/app/coffee/Features/Errors/ErrorController.coffee b/services/web/app/coffee/Features/Errors/ErrorController.coffee index 255c747bea..16b160642a 100644 --- a/services/web/app/coffee/Features/Errors/ErrorController.coffee +++ b/services/web/app/coffee/Features/Errors/ErrorController.coffee @@ -14,7 +14,7 @@ module.exports = ErrorController = handleError: (error, req, res, next) -> if error?.code is 'EBADCSRFTOKEN' - logger.log err: error,url:req.url, method:req.method, user:req?.sesson?.user, "invalid csrf" + logger.warn err: error,url:req.url, method:req.method, user:req?.sesson?.user, "invalid csrf" res.sendStatus(403) return if error instanceof Errors.NotFoundError diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index e039f60ca6..946a57d527 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -4,7 +4,7 @@ Doc = require('../../models/Doc').Doc Folder = require('../../models/Folder').Folder File = require('../../models/File').File FileStoreHandler = require("../FileStore/FileStoreHandler") -Errors = require "../../errors" +Errors = require "../Errors/Errors" tpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') projectLocator = require('./ProjectLocator') path = require "path" diff --git a/services/web/app/coffee/Features/Project/ProjectGetter.coffee b/services/web/app/coffee/Features/Project/ProjectGetter.coffee index ff95c02c40..af6178d06b 100644 --- a/services/web/app/coffee/Features/Project/ProjectGetter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectGetter.coffee @@ -3,7 +3,6 @@ db = mongojs.db ObjectId = mongojs.ObjectId async = require "async" Project = require("../../models/Project").Project -Errors = require("../../errors") logger = require("logger-sharelatex") module.exports = ProjectGetter = diff --git a/services/web/app/coffee/Features/Project/ProjectLocator.coffee b/services/web/app/coffee/Features/Project/ProjectLocator.coffee index cd4adad142..36d9203ebe 100644 --- a/services/web/app/coffee/Features/Project/ProjectLocator.coffee +++ b/services/web/app/coffee/Features/Project/ProjectLocator.coffee @@ -1,6 +1,6 @@ Project = require('../../models/Project').Project ProjectGetter = require("./ProjectGetter") -Errors = require "../../errors" +Errors = require "../Errors/Errors" _ = require('underscore') logger = require('logger-sharelatex') async = require('async') diff --git a/services/web/app/coffee/errors.coffee b/services/web/app/coffee/errors.coffee deleted file mode 100644 index 4a29822efc..0000000000 --- a/services/web/app/coffee/errors.coffee +++ /dev/null @@ -1,10 +0,0 @@ -NotFoundError = (message) -> - error = new Error(message) - error.name = "NotFoundError" - error.__proto__ = NotFoundError.prototype - return error -NotFoundError.prototype.__proto__ = Error.prototype - -module.exports = Errors = - NotFoundError: NotFoundError - diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 6397025da5..1d53999bd9 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -5,7 +5,7 @@ FolderSchema = require('./Folder.js').FolderSchema logger = require('logger-sharelatex') sanitize = require('sanitizer') concreteObjectId = require('mongoose').Types.ObjectId -Errors = require "../errors" +Errors = require "../Features/Errors/Errors" Schema = mongoose.Schema diff --git a/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee b/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee index ee1360fa0a..1a7e2fb500 100644 --- a/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee @@ -7,7 +7,7 @@ SandboxedModule = require('sandboxed-module') events = require "events" MockRequest = require "../helpers/MockRequest" MockResponse = require "../helpers/MockResponse" -Errors = require "../../../../app/js/errors" +Errors = require "../../../../app/js/Features/Errors/Errors" describe "DocumentController", -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee index 0e2441cf8d..e946850828 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee @@ -7,7 +7,7 @@ modulePath = "../../../../app/js/Features/Project/ProjectEntityHandler" SandboxedModule = require('sandboxed-module') ObjectId = require("mongoose").Types.ObjectId tk = require 'timekeeper' -Errors = require "../../../../app/js/errors" +Errors = require "../../../../app/js/Features/Errors/Errors" describe 'ProjectEntityHandler', -> project_id = '4eecb1c1bffa66588e0000a1' diff --git a/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee index 1feef45571..15a86d235c 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee @@ -5,7 +5,7 @@ should = chai.should() modulePath = "../../../../app/js/Features/Project/ProjectLocator" SandboxedModule = require('sandboxed-module') sinon = require('sinon') -Errors = require "../../../../app/js/errors" +Errors = require "../../../../app/js/Features/Errors/Errors" expect = require("chai").expect Project = class Project From 4d0fe3bf5d73df5f51eba1f0cd3f55c266e8362a Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 23 Mar 2016 15:18:10 +0000 Subject: [PATCH 101/208] Add error handling in mkdir_p --- .../app/coffee/Features/Project/ProjectEntityHandler.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index 946a57d527..02466d12b5 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -242,6 +242,7 @@ module.exports = ProjectEntityHandler = return folder.length != 0 ProjectGetter.getProjectWithOnlyFolders project_id, (err, project)=> + return callback(err) if err? if path == '/' logger.log project_id: project._id, "mkdir is only trying to make path of / so sending back root folder" return callback(null, [], project.rootFolder[0]) @@ -255,9 +256,11 @@ module.exports = ProjectEntityHandler = parentFolder_id = parentFolder._id builtUpPath = "#{builtUpPath}/#{folderName}" projectLocator.findElementByPath project, builtUpPath, (err, foundFolder)=> + return callback(err) if err? if !foundFolder? logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp" @addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)-> + return callback(err) if err? newFolder.parentFolder_id = parentFolder_id previousFolders.push newFolder callback null, previousFolders From 3fe0674462844b1d6710052915eb717e44cd78b9 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 23 Mar 2016 15:33:01 +0000 Subject: [PATCH 102/208] Revert "Add error handling in mkdir_p" This reverts commit c2458cd19018fe05199ce3b33fd4e9230d1cf319. Apparently mkdirp needs errors to be ignored to work... --- .../app/coffee/Features/Project/ProjectEntityHandler.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index 02466d12b5..946a57d527 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -242,7 +242,6 @@ module.exports = ProjectEntityHandler = return folder.length != 0 ProjectGetter.getProjectWithOnlyFolders project_id, (err, project)=> - return callback(err) if err? if path == '/' logger.log project_id: project._id, "mkdir is only trying to make path of / so sending back root folder" return callback(null, [], project.rootFolder[0]) @@ -256,11 +255,9 @@ module.exports = ProjectEntityHandler = parentFolder_id = parentFolder._id builtUpPath = "#{builtUpPath}/#{folderName}" projectLocator.findElementByPath project, builtUpPath, (err, foundFolder)=> - return callback(err) if err? if !foundFolder? logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp" @addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)-> - return callback(err) if err? newFolder.parentFolder_id = parentFolder_id previousFolders.push newFolder callback null, previousFolders From 90092a0c6184ac0ad914f87e53f484a93415646b Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 23 Mar 2016 16:05:38 +0000 Subject: [PATCH 103/208] improved styling, not perefect still --- services/web/app/views/wiki/page.jade | 24 +++++++++---------- services/web/public/stylesheets/app/wiki.less | 5 ++++ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/services/web/app/views/wiki/page.jade b/services/web/app/views/wiki/page.jade index a18b49b6ec..2270b8237b 100644 --- a/services/web/app/views/wiki/page.jade +++ b/services/web/app/views/wiki/page.jade @@ -11,23 +11,23 @@ block content | !{contents.content} .col-xs-9.page(ng-controller="SearchWikiController") .row - .col-md-12 - form.project-search.form-horizontal(role="form") - .form-group.has-feedback.has-feedback-left.col-md-12 - input.form-control.col-md-12(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....") - i.fa.fa-search.form-control-feedback-left - i.fa.fa-times.form-control-feedback( - ng-click="clearSearchText()", - style="cursor: pointer;", - ng-show="searchQueryText.length > 0" - ) + + form.project-search.form-horizontal.col-md-12(role="form") + .form-group.has-feedback.has-feedback-left.col-md-12 + input.form-control.col-md-12(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....") + i.fa.fa-search.form-control-feedback-left + i.fa.fa-times.form-control-feedback( + ng-click="clearSearchText()", + style="cursor: pointer;", + ng-show="searchQueryText.length > 0" + ) .row .col-md-12(ng-cloak) ul.list-unstyled li(ng-repeat='hit in hits') - .searchResult + .searchResult.thumbnail .row - .col.-md-12 + .col-md-12 a(ng-href='{{hit.url}}') span(ng-bind-html='hit.name') diff --git a/services/web/public/stylesheets/app/wiki.less b/services/web/public/stylesheets/app/wiki.less index cd4022dd89..c8e3a6538c 100644 --- a/services/web/public/stylesheets/app/wiki.less +++ b/services/web/public/stylesheets/app/wiki.less @@ -104,4 +104,9 @@ /*]]>*/ + .thumbnail { + margin-bottom:0px; + margin-top:5px; + } + } \ No newline at end of file From 7634fcf3af4bab3ac080233647d40382243aca34 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 23 Mar 2016 18:23:09 +0000 Subject: [PATCH 104/208] added missing page button (not wired up) to /learn --- services/web/app/views/wiki/page.jade | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/wiki/page.jade b/services/web/app/views/wiki/page.jade index 2270b8237b..77b938c465 100644 --- a/services/web/app/views/wiki/page.jade +++ b/services/web/app/views/wiki/page.jade @@ -11,8 +11,7 @@ block content | !{contents.content} .col-xs-9.page(ng-controller="SearchWikiController") .row - - form.project-search.form-horizontal.col-md-12(role="form") + form.project-search.form-horizontal.col-md-9(role="form") .form-group.has-feedback.has-feedback-left.col-md-12 input.form-control.col-md-12(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....") i.fa.fa-search.form-control-feedback-left @@ -21,6 +20,9 @@ block content style="cursor: pointer;", ng-show="searchQueryText.length > 0" ) + .col-md-2 + a.btn.btn-primary(ng-click="showMissingTemplateModal()") #{translate("missing_page")} + .row .col-md-12(ng-cloak) ul.list-unstyled From 6a4e13d3c82b0e7710fb5eee5d8e0df42e8173a6 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 24 Mar 2016 09:35:11 +0000 Subject: [PATCH 105/208] added templates angular file forgoten in last commit --- .../web/public/coffee/main/templates.coffee | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 services/web/public/coffee/main/templates.coffee diff --git a/services/web/public/coffee/main/templates.coffee b/services/web/public/coffee/main/templates.coffee new file mode 100644 index 0000000000..717f7fb954 --- /dev/null +++ b/services/web/public/coffee/main/templates.coffee @@ -0,0 +1,49 @@ +define [ + "base" +], (App) -> + + App.factory "algoliawiki", -> + client = new AlgoliaSearch("SK53GL4JLY", "e398f35d3074fde57ca6d6c88d8be37c") + index = client.initIndex("lean-wiki-index") + return index + + App.controller "SearchWikiController", ($scope, algoliawiki, _) -> + algolia = algoliawiki + $scope.hits = [] + + $scope.clearSearchText = -> + $scope.searchQueryText = "" + updateHits [] + + $scope.safeApply = (fn)-> + phase = $scope.$root.$$phase + if(phase == '$apply' || phase == '$digest') + $scope.$eval(fn) + else + $scope.$apply(fn) + + buildHitViewModel = (hit)-> + page_underscored = hit.pageName.replace(/\s/g,'_') + section_underscored = hit.sectionName.replace(/\s/g,'_') + result = + name : hit._highlightResult.pageName.value + " - " + hit._highlightResult.sectionName.value + url :"/learn/#{page_underscored}##{section_underscored}" + console.log result + return result + + updateHits = (hits)-> + $scope.safeApply -> + $scope.hits = hits + + $scope.search = -> + query = $scope.searchQueryText + if !query? or query.length == 0 + updateHits [] + return + + algolia.search query, (err, response)-> + if response.hits.length == 0 + updateHits [] + else + hits = _.map response.hits, buildHitViewModel + updateHits hits \ No newline at end of file From ce2cfeaa5b503657cb782f864cb073e20f5a4758 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 24 Mar 2016 09:52:19 +0000 Subject: [PATCH 106/208] don't show search bar in templates if its not configured --- services/web/app/views/wiki/page.jade | 47 ++++++++++--------- .../web/public/coffee/main/templates.coffee | 7 +-- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/services/web/app/views/wiki/page.jade b/services/web/app/views/wiki/page.jade index 77b938c465..53d4fad401 100644 --- a/services/web/app/views/wiki/page.jade +++ b/services/web/app/views/wiki/page.jade @@ -9,29 +9,32 @@ block content .row .col-xs-3.contents(ng-non-bindable) | !{contents.content} - .col-xs-9.page(ng-controller="SearchWikiController") - .row - form.project-search.form-horizontal.col-md-9(role="form") - .form-group.has-feedback.has-feedback-left.col-md-12 - input.form-control.col-md-12(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....") - i.fa.fa-search.form-control-feedback-left - i.fa.fa-times.form-control-feedback( - ng-click="clearSearchText()", - style="cursor: pointer;", - ng-show="searchQueryText.length > 0" - ) - .col-md-2 - a.btn.btn-primary(ng-click="showMissingTemplateModal()") #{translate("missing_page")} - .row - .col-md-12(ng-cloak) - ul.list-unstyled - li(ng-repeat='hit in hits') - .searchResult.thumbnail - .row - .col-md-12 - a(ng-href='{{hit.url}}') - span(ng-bind-html='hit.name') + .col-xs-9.page + - if(typeof(settings.algolia) != "undefined" && typeof(settings.algolia.indexes) != "undefined" && typeof(settings.algolia.indexes.wiki) != "undefined") + span(ng-controller="SearchWikiController") + .row + form.project-search.form-horizontal.col-md-9(role="form") + .form-group.has-feedback.has-feedback-left.col-md-12 + input.form-control.col-md-12(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....") + i.fa.fa-search.form-control-feedback-left + i.fa.fa-times.form-control-feedback( + ng-click="clearSearchText()", + style="cursor: pointer;", + ng-show="searchQueryText.length > 0" + ) + .col-md-2 + a.btn.btn-primary(ng-click="showMissingTemplateModal()") #{translate("missing_page")} + + .row + .col-md-12(ng-cloak) + ul.list-unstyled + li(ng-repeat='hit in hits') + .searchResult.thumbnail + .row + .col-md-12 + a(ng-href='{{hit.url}}') + span(ng-bind-html='hit.name') .card(ng-non-bindable) .page-header diff --git a/services/web/public/coffee/main/templates.coffee b/services/web/public/coffee/main/templates.coffee index 717f7fb954..b9a1d3bc5d 100644 --- a/services/web/public/coffee/main/templates.coffee +++ b/services/web/public/coffee/main/templates.coffee @@ -3,9 +3,10 @@ define [ ], (App) -> App.factory "algoliawiki", -> - client = new AlgoliaSearch("SK53GL4JLY", "e398f35d3074fde57ca6d6c88d8be37c") - index = client.initIndex("lean-wiki-index") - return index + if window.sharelatex?.algolia? and window.sharelatex.algolia?.indexes?.wiki? + client = new AlgoliaSearch(window.sharelatex.algolia?.app_id, window.sharelatex.algolia?.api_key) + index = client.initIndex(window.sharelatex.algolia?.indexes?.wiki) + return index App.controller "SearchWikiController", ($scope, algoliawiki, _) -> algolia = algoliawiki From 9b84e1b1646b9fabd2148239724f231d4823d9b0 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 24 Mar 2016 11:38:56 +0000 Subject: [PATCH 107/208] Improve styling of wiki search --- services/web/app/views/wiki/page.jade | 14 ++++------- .../web/public/coffee/main/templates.coffee | 14 ++++++++++- services/web/public/stylesheets/app/wiki.less | 24 ++++++++++++++++--- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/services/web/app/views/wiki/page.jade b/services/web/app/views/wiki/page.jade index 53d4fad401..c7cc876408 100644 --- a/services/web/app/views/wiki/page.jade +++ b/services/web/app/views/wiki/page.jade @@ -23,20 +23,16 @@ block content style="cursor: pointer;", ng-show="searchQueryText.length > 0" ) - .col-md-2 + .col-md-3.text-right a.btn.btn-primary(ng-click="showMissingTemplateModal()") #{translate("missing_page")} .row .col-md-12(ng-cloak) - ul.list-unstyled - li(ng-repeat='hit in hits') - .searchResult.thumbnail - .row - .col-md-12 - a(ng-href='{{hit.url}}') - span(ng-bind-html='hit.name') + a(ng-href='{{hit.url}}',ng-repeat='hit in hits').search-result.card.card-thin + span(ng-bind-html='hit.name') + div.search-result-content(ng-show="hit.content != ''", ng-bind-html='hit.content') - .card(ng-non-bindable) + .card.row-spaced(ng-non-bindable) .page-header h1 #{title} diff --git a/services/web/public/coffee/main/templates.coffee b/services/web/public/coffee/main/templates.coffee index b9a1d3bc5d..3d05d4ea0f 100644 --- a/services/web/public/coffee/main/templates.coffee +++ b/services/web/public/coffee/main/templates.coffee @@ -26,10 +26,22 @@ define [ buildHitViewModel = (hit)-> page_underscored = hit.pageName.replace(/\s/g,'_') section_underscored = hit.sectionName.replace(/\s/g,'_') + content = hit._highlightResult.content.value + # Replace many new lines + content = content.replace(/\n\n+/g, "\n\n") + lines = content.split("\n") + # Only show the lines that have a highlighted match + matching_lines = [] + for line in lines + if !line.match(/^\[edit\]/) + content += line + "\n" + if line.match(//) + matching_lines.push line + content = matching_lines.join("\n...\n") result = name : hit._highlightResult.pageName.value + " - " + hit._highlightResult.sectionName.value url :"/learn/#{page_underscored}##{section_underscored}" - console.log result + content: content return result updateHits = (hits)-> diff --git a/services/web/public/stylesheets/app/wiki.less b/services/web/public/stylesheets/app/wiki.less index c8e3a6538c..053d0b0619 100644 --- a/services/web/public/stylesheets/app/wiki.less +++ b/services/web/public/stylesheets/app/wiki.less @@ -104,9 +104,27 @@ /*]]>*/ - .thumbnail { - margin-bottom:0px; - margin-top:5px; + a.search-result { + display: block; + margin-top: @line-height-computed / 2; + .search-result-content { + margin-top: @line-height-computed / 4; + white-space: pre-wrap; + font-size: 0.8em; + color: @gray-dark; + em { + font-weight: bold; + } + } + + &:hover { + text-decoration: none; + .search-result-content { + color: @gray-darker; + + } + box-shadow: 0 2px 4px rgba(0,0,0,0.35); + } } } \ No newline at end of file From 63c183402c12b15f9d0aaf95cd9c257601b3bb10 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 24 Mar 2016 11:42:48 +0000 Subject: [PATCH 108/208] Style active and focus states of search results too --- services/web/public/stylesheets/app/wiki.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/app/wiki.less b/services/web/public/stylesheets/app/wiki.less index 053d0b0619..210230989c 100644 --- a/services/web/public/stylesheets/app/wiki.less +++ b/services/web/public/stylesheets/app/wiki.less @@ -117,7 +117,7 @@ } } - &:hover { + &:hover, &:active, &:focus { text-decoration: none; .search-result-content { color: @gray-darker; From 6662d4c2cdf94446982fdd02f8f4e73ade63c5da Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 24 Mar 2016 12:05:54 +0000 Subject: [PATCH 109/208] wired up modal for learn and added translations --- services/web/app/views/contact-us-modal.jade | 14 +-- services/web/app/views/wiki/page.jade | 33 ++++++- services/web/public/coffee/main.coffee | 2 +- services/web/public/coffee/main/learn.coffee | 92 ++++++++++++++++++++ 4 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 services/web/public/coffee/main/learn.coffee diff --git a/services/web/app/views/contact-us-modal.jade b/services/web/app/views/contact-us-modal.jade index dade3a7730..32e096b7d5 100644 --- a/services/web/app/views/contact-us-modal.jade +++ b/services/web/app/views/contact-us-modal.jade @@ -5,26 +5,26 @@ script(type='text/ng-template', id='supportModalTemplate') data-dismiss="modal" ng-click="close()" ) × - h3 Contact Us + h3 #{translate("contact_us")} .modal-body.contact-us-modal span(ng-show="sent == false") label - | Subject + | #{translate("subject")} .form-group input.field.text.medium.span8.form-control(ng-model="form.subject", maxlength='255', tabindex='1', onkeyup='') label.desc - | Email + | #{translate("email")} .form-group input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '#{getUserEmail()}'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2') label#title12.desc - | Project URL (optional) + | #{translate("project_url")} (#{translate("optional")}) .form-group input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='') label.desc - | Message + | #{translate("suggestion")} .form-group textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', maxlength='255', tabindex='4', onkeyup='') .form-group.text-center - input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value='Get in Touch') + input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value='#{translate("contact_us")}') span(ng-show="sent") - p Request Sent, Thank you. \ No newline at end of file + p #{translate("request_sent_thank_you")} \ No newline at end of file diff --git a/services/web/app/views/wiki/page.jade b/services/web/app/views/wiki/page.jade index c7cc876408..6a6bea2202 100644 --- a/services/web/app/views/wiki/page.jade +++ b/services/web/app/views/wiki/page.jade @@ -24,7 +24,7 @@ block content ng-show="searchQueryText.length > 0" ) .col-md-3.text-right - a.btn.btn-primary(ng-click="showMissingTemplateModal()") #{translate("missing_page")} + a.btn.btn-primary(ng-click="showMissingTemplateModal()") #{translate("suggest_new_doc")} .row .col-md-12(ng-cloak) @@ -36,4 +36,33 @@ block content .page-header h1 #{title} - | !{page.content} \ No newline at end of file + | !{page.content} + + + + + script(type="text/ng-template", id="missingWikiPageModal") + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="close()" + ) × + h3 #{translate("suggest_new_doc")} + .modal-body.contact-us-modal + span(ng-show="sent == false") + label.desc + | #{translate("email")} (#{translate("optional")}) + .form-group + input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '#{getUserEmail()}'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2') + label.desc + | #{translate("suggestion")} + .form-group + textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', maxlength='255', tabindex='4', onkeyup='') + span(ng-show="sent") + p #{translate("request_sent_thank_you")} + .modal-footer + button.btn.btn-default(ng-click="close()") + span #{translate("dismiss")} + button.btn-success.btn(type='submit', ng-disabled="sending", ng-click="contactUs()") #{translate("contact_us")} + diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index de36e33c34..d85d89cfe8 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -16,7 +16,7 @@ define [ "main/register-users" "main/subscription/group-subscription-invite-controller" "main/contact-us" - "main/templates" + "main/learn" "analytics/AbTestingManager" "directives/asyncForm" "directives/stopPropagation" diff --git a/services/web/public/coffee/main/learn.coffee b/services/web/public/coffee/main/learn.coffee new file mode 100644 index 0000000000..57053bd4fa --- /dev/null +++ b/services/web/public/coffee/main/learn.coffee @@ -0,0 +1,92 @@ +define [ + "base" +], (App) -> + + App.factory "algoliawiki", -> + if window.sharelatex?.algolia? and window.sharelatex.algolia?.indexes?.wiki? + client = new AlgoliaSearch(window.sharelatex.algolia?.app_id, window.sharelatex.algolia?.api_key) + index = client.initIndex(window.sharelatex.algolia?.indexes?.wiki) + return index + + App.controller "SearchWikiController", ($scope, algoliawiki, _, $modal) -> + algolia = algoliawiki + $scope.hits = [] + + $scope.clearSearchText = -> + $scope.searchQueryText = "" + updateHits [] + + $scope.safeApply = (fn)-> + phase = $scope.$root.$$phase + if(phase == '$apply' || phase == '$digest') + $scope.$eval(fn) + else + $scope.$apply(fn) + + buildHitViewModel = (hit)-> + page_underscored = hit.pageName.replace(/\s/g,'_') + section_underscored = hit.sectionName.replace(/\s/g,'_') + content = hit._highlightResult.content.value + # Replace many new lines + content = content.replace(/\n\n+/g, "\n\n") + lines = content.split("\n") + # Only show the lines that have a highlighted match + matching_lines = [] + for line in lines + if !line.match(/^\[edit\]/) + content += line + "\n" + if line.match(//) + matching_lines.push line + content = matching_lines.join("\n...\n") + result = + name : hit._highlightResult.pageName.value + " - " + hit._highlightResult.sectionName.value + url :"/learn/#{page_underscored}##{section_underscored}" + content: content + return result + + updateHits = (hits)-> + $scope.safeApply -> + $scope.hits = hits + + $scope.search = -> + query = $scope.searchQueryText + if !query? or query.length == 0 + updateHits [] + return + + algolia.search query, (err, response)-> + if response.hits.length == 0 + updateHits [] + else + hits = _.map response.hits, buildHitViewModel + updateHits hits + + $scope.showMissingTemplateModal = () -> + modalInstance = $modal.open( + templateUrl: "missingWikiPageModal" + controller: "MissingWikiPageController" + ) + + + App.controller 'MissingWikiPageController', ($scope, $modalInstance) -> + $scope.form = {} + $scope.sent = false + $scope.sending = false + $scope.contactUs = -> + if !$scope.form.message? + console.log "message not set" + return + $scope.sending = true + ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32) + params = + email: $scope.form.email or "support@sharelatex.com" + message: $scope.form.message or "" + subject: "new wiki page sujection - [#{ticketNumber}]" + labels: "support wiki" + + Groove.createTicket params, (err, json)-> + $scope.sent = true + $scope.$apply() + + $scope.close = () -> + $modalInstance.close() From e2dffdf7222b119af7b467c1a67ee19f301e7975 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 24 Mar 2016 14:23:18 +0000 Subject: [PATCH 110/208] when user contact us put project url at bottom of message --- services/web/public/coffee/main/contact-us.coffee | 6 ++++-- services/web/public/coffee/main/learn.coffee | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee index 9596468ec5..852fb97c30 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -20,11 +20,13 @@ define [ return $scope.sending = true ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32) + message = $scope.form.message + if $scope.form.project_url + message = "#{message}\n\n project_url = #{$scope.form.project_url}" params = email: $scope.form.email - message: $scope.form.message or "" + message: message or "" subject: $scope.form.subject + " - [#{ticketNumber}]" - about : $scope.form.project_url or "" labels: "support" Groove.createTicket params, (err, json)-> diff --git a/services/web/public/coffee/main/learn.coffee b/services/web/public/coffee/main/learn.coffee index 57053bd4fa..28026be508 100644 --- a/services/web/public/coffee/main/learn.coffee +++ b/services/web/public/coffee/main/learn.coffee @@ -53,7 +53,7 @@ define [ if !query? or query.length == 0 updateHits [] return - + algolia.search query, (err, response)-> if response.hits.length == 0 updateHits [] From 1a651b38f43c2d529f3ce8bc2bdee1a79f008d99 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 29 Mar 2016 14:15:33 +0100 Subject: [PATCH 111/208] added sendgrid as email option --- services/web/app/coffee/Features/Email/EmailSender.coffee | 5 +++++ services/web/package.json | 1 + 2 files changed, 6 insertions(+) diff --git a/services/web/app/coffee/Features/Email/EmailSender.coffee b/services/web/app/coffee/Features/Email/EmailSender.coffee index c5ff09b1b0..cb74502827 100644 --- a/services/web/app/coffee/Features/Email/EmailSender.coffee +++ b/services/web/app/coffee/Features/Email/EmailSender.coffee @@ -3,6 +3,8 @@ metrics = require('../../infrastructure/Metrics') Settings = require('settings-sharelatex') nodemailer = require("nodemailer") sesTransport = require('nodemailer-ses-transport') +sgTransport = require('nodemailer-sendgrid-transport') + _ = require("underscore") if Settings.email? and Settings.email.fromAddress? @@ -19,6 +21,9 @@ client = if Settings?.email?.parameters?.AWSAccessKeyID? logger.log "using aws ses for email" nm_client = nodemailer.createTransport(sesTransport(Settings.email.parameters)) +else if Settings?.email?.parameters?.sendgridApiKey? + logger.log "using sendgrid for email" + nm_client = nodemailer.createTransport(sgTransport({auth:{api_key:Settings?.email?.parameters?.sendgridApiKey}})) else if Settings?.email?.parameters? smtp = _.pick(Settings?.email?.parameters, "host", "port", "secure", "auth") diff --git a/services/web/package.json b/services/web/package.json index 021d86f6d7..dc2eeb77f4 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -39,6 +39,7 @@ "multer": "^0.1.8", "node-uuid": "1.4.1", "nodemailer": "2.1.0", + "nodemailer-sendgrid-transport": "^0.2.0", "nodemailer-ses-transport": "^1.3.0", "optimist": "0.6.1", "redback": "0.4.0", From c7da9f1eefc079f8fbda2b9278137da8970afd1e Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 29 Mar 2016 14:25:59 +0100 Subject: [PATCH 112/208] added different check to contact-us modal --- services/web/public/coffee/main/contact-us.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee index 852fb97c30..79d4caa4ab 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -21,7 +21,7 @@ define [ $scope.sending = true ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32) message = $scope.form.message - if $scope.form.project_url + if $scope.form.project_url? message = "#{message}\n\n project_url = #{$scope.form.project_url}" params = email: $scope.form.email From 059d3432c2db894e866281856f516d505638d3ae Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 30 Mar 2016 11:28:56 +0100 Subject: [PATCH 113/208] Allow citations like `-parencite[page 6]{` to work. --- .../aceEditor/auto-complete/AutoCompleteManager.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index 32a60370c1..d256960165 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -7,7 +7,7 @@ define [ Range = ace.require("ace/range").Range getLastCommandFragment = (lineUpToCursor) -> - if m = lineUpToCursor.match(/(\\[^\\ ]+)$/) + if m = lineUpToCursor.match(/(\\[^\\]+)$/) return m[1] else return null @@ -47,7 +47,7 @@ define [ lineUpToCursor = editor.getSession().getTextRange(upToCursorRange) commandFragment = getLastCommandFragment(lineUpToCursor) if commandFragment - citeMatch = commandFragment.match(/^~?\\([a-z]*cite[a-z]?){([^}]*,)?(\w*)/) + citeMatch = commandFragment.match(/^~?\\([a-z]*cite[a-z]?(?:\[.*])?){([^}]*,)?(\w*)/) if citeMatch beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999) lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange) From db8fb63bb56ce2975762b5b3b29a314fcb64a973 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 31 Mar 2016 11:53:59 +0100 Subject: [PATCH 114/208] fixed change payment details page also increased size of year field for firefox users and removed unnded options in new plan page --- .../Features/Project/ProjectController.coffee | 2 +- .../SubscriptionController.coffee | 20 ++++--------------- services/web/app/views/subscriptions/new.jade | 2 +- .../SubscriptionControllerTests.coffee | 12 ++--------- 4 files changed, 8 insertions(+), 28 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 34a44994be..b99bd8258f 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -240,7 +240,7 @@ module.exports = ProjectController = bodyClasses: ["editor"] project_id : project._id user : { - id : user.id + id : user_id email : user.email first_name : user.first_name last_name : user.last_name diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index f0026bd7f5..2a1e576231 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -48,7 +48,7 @@ module.exports = SubscriptionController = subscription: plan_code : req.query.planCode currency: currency - account_code: user.id + account_code: user._id }, (error, signature) -> return next(error) if error? res.render "subscriptions/new", @@ -64,19 +64,7 @@ module.exports = SubscriptionController = showCouponField: req.query.scf showVatField: req.query.svf couponCode: req.query.cc or "" - subscriptionFormOptions: JSON.stringify - acceptedCards: ['discover', 'mastercard', 'visa'] - target : "#subscribeForm" - signature : signature - planCode : req.query.planCode - successURL : "#{Settings.siteUrl}/user/subscription/create?_csrf=#{req.session._csrf}" - accountCode : user.id - enableCoupons: true - acceptPaypal: true - account : - firstName : user.first_name - lastName : user.last_name - email : user.email + userSubscriptionPage: (req, res, next) -> @@ -128,7 +116,7 @@ module.exports = SubscriptionController = res.redirect "/user/subscription" else RecurlyWrapper.sign { - account_code: user.id + account_code: user._id }, (error, signature) -> return next(error) if error? res.render "subscriptions/edit-billing-details", @@ -139,7 +127,7 @@ module.exports = SubscriptionController = signature : signature successURL : "#{Settings.siteUrl}/user/subscription/update" user : - id : user.id + id : user._id createSubscription: (req, res, next)-> AuthenticationController.getLoggedInUser req, (error, user) -> diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade index b70aec29fc..e84dc56eec 100644 --- a/services/web/app/views/subscriptions/new.jade +++ b/services/web/app/views/subscriptions/new.jade @@ -108,7 +108,7 @@ block content option(value="10") 10 option(value="11") 11 option(value="12") 12 - .col-md-3 + .col-md-4 .form-group(ng-class="validation.correctExpiry == false || validation.errorFields.year ? 'has-error' : ''") select.form-control(data-recurly='year', ng-change="validateExpiry()", ng-model='data.year') option(value="", disabled, selected) Year diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee index f2abbe0219..27e9e571d1 100644 --- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee @@ -99,7 +99,7 @@ describe "SubscriptionController sanboxed", -> describe "with a user with a subscription", -> beforeEach (done) -> @LimitationsManager.userHasSubscription.callsArgWith(1, null, true) - @user.id = @activeRecurlySubscription.account.account_code + @user._id = @activeRecurlySubscription.account.account_code @res.callback = done @SubscriptionController.editBillingDetailsPage(@req, @res) @@ -110,7 +110,7 @@ describe "SubscriptionController sanboxed", -> it "should set the correct variables for the template", -> should.exist @res.renderedVariables.signature @res.renderedVariables.successURL.should.equal "#{@settings.siteUrl}/user/subscription/update" - @res.renderedVariables.user.id.should.equal @user.id + @res.renderedVariables.user.id.should.equal @user._id describe "with a user without subscription", -> beforeEach (done) -> @@ -140,14 +140,6 @@ describe "SubscriptionController sanboxed", -> done() @SubscriptionController.paymentPage @req, @res - it "should set the successURL", (done)-> - @req.session._csrf = @csrfToken = "mock-csrf-token" - @res.render = (page, opts)=> - url = JSON.parse(opts.subscriptionFormOptions).successURL - url.should.equal("#{@settings.siteUrl}/user/subscription/create?_csrf=#{@csrfToken}") - done() - @SubscriptionController.paymentPage @req, @res - describe "with a user with subscription", -> it "should redirect to the subscription dashboard", (done)-> @LimitationsManager.userHasSubscription.callsArgWith(1, null, true) From 5a201153c7226fb6f7b652ea62d83b1cb94c7279 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 31 Mar 2016 16:58:16 +0100 Subject: [PATCH 115/208] refactored subscription updater again adding _setUsersMinimumFeatures single place you can call and the users minimum features are set --- .../Subscription/SubscriptionUpdater.coffee | 51 ++++---- .../SubscriptionUpdaterTests.coffee | 109 ++++++++++++------ 2 files changed, 98 insertions(+), 62 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index 69d31d7277..6dd21f1018 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -11,33 +11,18 @@ ReferalAllocator = require("../Referal/ReferalAllocator") oneMonthInSeconds = 60 * 60 * 24 * 30 -module.exports = +module.exports = SubscriptionUpdater = syncSubscription: (recurlySubscription, adminUser_id, callback) -> - self = @ logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "syncSubscription, creating new if subscription does not exist" - jobs = - subscription: (cb)-> - SubscriptionLocator.getUsersSubscription adminUser_id, cb - groupSubscription: (cb)-> - SubscriptionLocator.getGroupSubscriptionMemberOf adminUser_id, cb - async.series jobs, (err, results)-> - {subscription, groupSubscription} = results + SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)-> if subscription? logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "subscription does exist" - self._updateSubscription recurlySubscription, subscription, (err)-> - if err? - logger.err err:err, adminUser_id:adminUser_id, "error syncing subscription" - return callback(err) - if groupSubscription? and recurlySubscription.state == "expired" - logger.log adminUser_id:adminUser_id, "subscription does exist" - UserFeaturesUpdater.updateFeatures adminUser_id, groupSubscription.planCode, callback - else - callback() + SubscriptionUpdater._updateSubscriptionFromRecurly recurlySubscription, subscription, callback else logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "subscription does not exist, creating a new one" - self._createNewSubscription adminUser_id, (err, subscription)-> - self._updateSubscription recurlySubscription, subscription, callback + SubscriptionUpdater._createNewSubscription adminUser_id, (err, subscription)-> + SubscriptionUpdater._updateSubscriptionFromRecurly recurlySubscription, subscription, callback addUserToGroup: (adminUser_id, user_id, callback)-> logger.log adminUser_id:adminUser_id, user_id:user_id, "adding user into mongo subscription" @@ -60,7 +45,8 @@ module.exports = if err? logger.err err:err, searchOps:searchOps, removeOperation:removeOperation, "error removing user from group" return callback(err) - UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, callback + SubscriptionUpdater._setUsersMinimumFeatures user_id, callback + _createNewSubscription: (adminUser_id, callback)-> logger.log adminUser_id:adminUser_id, "creating new subscription" @@ -69,7 +55,7 @@ module.exports = subscription.save (err)-> callback err, subscription - _updateSubscription: (recurlySubscription, subscription, callback)-> + _updateSubscriptionFromRecurly: (recurlySubscription, subscription, callback)-> logger.log recurlySubscription:recurlySubscription, subscription:subscription, "updaing subscription" plan = PlansLocator.findLocalPlanInSettings(recurlySubscription.plan.plan_code) if recurlySubscription.state == "expired" @@ -88,8 +74,25 @@ module.exports = allIds = _.union subscription.members_id, [subscription.admin_id] jobs = allIds.map (user_id)-> return (cb)-> - UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, cb - jobs.push (cb)-> ReferalAllocator.assignBonus subscription.admin_id, cb + SubscriptionUpdater._setUsersMinimumFeatures user_id, cb async.series jobs, callback + _setUsersMinimumFeatures: (user_id, callback)-> + jobs = + subscription: (cb)-> + SubscriptionLocator.getUsersSubscription user_id, cb + groupSubscription: (cb)-> + SubscriptionLocator.getGroupSubscriptionMemberOf user_id, cb + async.series jobs, (err, results)-> + {subscription, groupSubscription} = results + if subscription? and subscription.planCode? + UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback + else if groupSubscription? and groupSubscription.planCode? + UserFeaturesUpdater.updateFeatures user_id, groupSubscription.planCode, callback + else + UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, (err)-> + if err? + logger.err err:err, user_id:user_id, "Error setting minimum user feature" + return callback(err) + ReferalAllocator.assignBonus user_id, callback diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee index 924dc42ef2..c91ca34023 100644 --- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee @@ -5,7 +5,7 @@ modulePath = "../../../../app/js/Features/Subscription/SubscriptionUpdater" assert = require("chai").assert ObjectId = require('mongoose').Types.ObjectId -describe "Subscription Updater", -> +describe "SubscriptionUpdater", -> beforeEach -> @recurlySubscription = @@ -13,7 +13,7 @@ describe "Subscription Updater", -> plan: plan_code: "kjhsakjds" @adminUser = - _id:"5208dd34438843e2db000007" + _id: @adminuser_id = "5208dd34438843e2db000007" @otherUserId = "5208dd34438842e2db000005" @allUserIds = ["13213", "dsadas", "djsaiud89"] @subscription = subscription = @@ -21,14 +21,14 @@ describe "Subscription Updater", -> members_id: @allUserIds save: sinon.stub().callsArgWith(0) freeTrial:{} - plan_code:"student_or_something" + planCode:"student_or_something" @groupSubscription = admin_id: @adminUser._id members_id: @allUserIds save: sinon.stub().callsArgWith(0) freeTrial:{} - plan_code:"group_subscription" + planCode:"group_subscription" @updateStub = sinon.stub().callsArgWith(2, null) @@ -71,42 +71,33 @@ describe "Subscription Updater", -> beforeEach -> @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) - @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, @groupSubscription) - @SubscriptionUpdater._updateSubscription = sinon.stub().callsArgWith(2) + @SubscriptionUpdater._updateSubscriptionFromRecurly = sinon.stub().callsArgWith(2) it "should update the subscription if the user already is admin of one", (done)-> @SubscriptionUpdater._createNewSubscription = sinon.stub() @SubscriptionUpdater.syncSubscription @recurlySubscription, @adminUser._id, (err)=> @SubscriptionLocator.getUsersSubscription.calledWith(@adminUser._id).should.equal true - @SubscriptionUpdater._updateSubscription.called.should.equal true - @SubscriptionUpdater._updateSubscription.calledWith(@recurlySubscription, @subscription).should.equal true - done() - - it "should sync with the group subscription if the recurly subscription is expired", (done)-> - @recurlySubscription.state = "expired" - - @SubscriptionUpdater.syncSubscription @recurlySubscription, @adminUser._id, (err)=> - @SubscriptionLocator.getUsersSubscription.calledWith(@adminUser._id).should.equal true - @SubscriptionUpdater._updateSubscription.called.should.equal true - @SubscriptionUpdater._updateSubscription.calledWith(@recurlySubscription, @subscription).should.equal true - @UserFeaturesUpdater.updateFeatures.calledWith(@adminUser._id, @groupSubscription.planCode).should.equal true + @SubscriptionUpdater._updateSubscriptionFromRecurly.called.should.equal true + @SubscriptionUpdater._updateSubscriptionFromRecurly.calledWith(@recurlySubscription, @subscription).should.equal true done() it "should not call updateFeatures with group subscription if recurly subscription is not expired", (done)-> @SubscriptionUpdater.syncSubscription @recurlySubscription, @adminUser._id, (err)=> @SubscriptionLocator.getUsersSubscription.calledWith(@adminUser._id).should.equal true - @SubscriptionUpdater._updateSubscription.called.should.equal true - @SubscriptionUpdater._updateSubscription.calledWith(@recurlySubscription, @subscription).should.equal true + @SubscriptionUpdater._updateSubscriptionFromRecurly.called.should.equal true + @SubscriptionUpdater._updateSubscriptionFromRecurly.calledWith(@recurlySubscription, @subscription).should.equal true @UserFeaturesUpdater.updateFeatures.called.should.equal false done() - describe "_updateSubscription", -> - + describe "_updateSubscriptionFromRecurly", -> + beforeEach -> + @SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub().callsArgWith(1) + it "should update the subscription with token etc when not expired", (done)-> - @SubscriptionUpdater._updateSubscription @recurlySubscription, @subscription, (err)=> + @SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=> @subscription.recurlySubscription_id.should.equal @recurlySubscription.uuid @subscription.planCode.should.equal @recurlySubscription.plan.plan_code @@ -114,43 +105,40 @@ describe "Subscription Updater", -> assert.equal(@subscription.freeTrial.expiresAt, undefined) assert.equal(@subscription.freeTrial.planCode, undefined) @subscription.save.called.should.equal true - @UserFeaturesUpdater.updateFeatures.calledWith(@adminUser._id, @recurlySubscription.plan.plan_code).should.equal true + @SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@adminUser._id).should.equal true done() it "should remove the recurlySubscription_id when expired", (done)-> @recurlySubscription.state = "expired" - @SubscriptionUpdater._updateSubscription @recurlySubscription, @subscription, (err)=> + @SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=> assert.equal(@subscription.recurlySubscription_id, undefined) @subscription.save.called.should.equal true - @UserFeaturesUpdater.updateFeatures.calledWith(@adminUser._id, @Settings.defaultPlanCode).should.equal true + @SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@adminUser._id).should.equal true done() it "should update all the users features", (done)-> - @SubscriptionUpdater._updateSubscription @recurlySubscription, @subscription, (err)=> - @UserFeaturesUpdater.updateFeatures.calledWith(@adminUser._id, @recurlySubscription.plan.plan_code).should.equal true - @UserFeaturesUpdater.updateFeatures.calledWith(@allUserIds[0], @recurlySubscription.plan.plan_code).should.equal true - @UserFeaturesUpdater.updateFeatures.calledWith(@allUserIds[1], @recurlySubscription.plan.plan_code).should.equal true - @UserFeaturesUpdater.updateFeatures.calledWith(@allUserIds[2], @recurlySubscription.plan.plan_code).should.equal true + @SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=> + @SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@adminUser._id).should.equal true + @SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@allUserIds[0]).should.equal true + @SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@allUserIds[1]).should.equal true + @SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@allUserIds[2]).should.equal true done() it "should set group to true and save how many members can be added to group", (done)-> @PlansLocator.findLocalPlanInSettings.withArgs(@recurlySubscription.plan.plan_code).returns({groupPlan:true, membersLimit:5}) - @SubscriptionUpdater._updateSubscription @recurlySubscription, @subscription, (err)=> + @SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=> @subscription.membersLimit.should.equal 5 @subscription.groupPlan.should.equal true done() it "should not set group to true or set groupPlan", (done)-> - @SubscriptionUpdater._updateSubscription @recurlySubscription, @subscription, (err)=> + @SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=> assert.notEqual @subscription.membersLimit, 5 assert.notEqual @subscription.groupPlan, true done() - it "should call assignBonus", (done)-> - @SubscriptionUpdater._updateSubscription @recurlySubscription, @subscription, (err)=> - @ReferalAllocator.assignBonus.calledWith(@subscription.admin_id).should.equal true - done() + describe "_createNewSubscription", -> it "should create a new subscription then update the subscription", (done)-> @@ -176,6 +164,9 @@ describe "Subscription Updater", -> done() describe "removeUserFromGroup", -> + beforeEach -> + @SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub().callsArgWith(1) + it "should pull the users id from the group", (done)-> @SubscriptionUpdater.removeUserFromGroup @adminUser._id, @otherUserId, => searchOps = @@ -187,5 +178,47 @@ describe "Subscription Updater", -> it "should update the users features", (done)-> @SubscriptionUpdater.removeUserFromGroup @adminUser._id, @otherUserId, => - @UserFeaturesUpdater.updateFeatures.calledWith(@otherUserId, @Settings.defaultPlanCode).should.equal true + @SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@otherUserId).should.equal true done() + + describe "_setUsersMinimumFeatures", -> + + + + it "should call updateFeatures with the subscription if set", (done)-> + @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) + @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null) + + @SubscriptionUpdater._setUsersMinimumFeatures @adminUser._id, (err)=> + args = @UserFeaturesUpdater.updateFeatures.args[0] + assert.equal args[0], @adminUser._id + assert.equal args[1], @subscription.planCode + done() + + it "should call updateFeatures with the group subscription if set", (done)-> + @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null) + @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, @groupSubscription) + + @SubscriptionUpdater._setUsersMinimumFeatures @adminUser._id, (err)=> + args = @UserFeaturesUpdater.updateFeatures.args[0] + assert.equal args[0], @adminUser._id + assert.equal args[1], @groupSubscription.planCode + done() + + + it "should call updateFeatures with default if there are no subscriptions for user", (done)-> + @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null) + @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null) + @SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=> + args = @UserFeaturesUpdater.updateFeatures.args[0] + assert.equal args[0], @adminUser._id + assert.equal args[1], @Settings.defaultPlanCode + done() + + it "should call assignBonus", (done)-> + @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null) + @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null) + @SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=> + @ReferalAllocator.assignBonus.calledWith(@adminuser_id).should.equal true + done() + From 0ce514a5a6172abc453e1a8eb8f36acb4726a538 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 31 Mar 2016 17:10:49 +0100 Subject: [PATCH 116/208] Add features to the user object in ide. --- .../web/app/coffee/Features/Project/ProjectController.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index b99bd8258f..29fef997f3 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -248,6 +248,7 @@ module.exports = ProjectController = subscription : freeTrial: {allowed: allowedFreeTrial} featureSwitches: user.featureSwitches + features: user.features } userSettings: { mode : user.ace.mode From d80191c03bcb4af43c8579282a8e22cf78b096f3 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 31 Mar 2016 17:28:03 +0100 Subject: [PATCH 117/208] added more logging and missing err handler in subscription updater --- .../web/app/coffee/Features/Referal/ReferalAllocator.coffee | 2 +- .../coffee/Features/Subscription/SubscriptionUpdater.coffee | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee b/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee index 0127e5681e..b28979aaf0 100644 --- a/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee +++ b/services/web/app/coffee/Features/Referal/ReferalAllocator.coffee @@ -35,7 +35,7 @@ module.exports = ReferalAllocator = query = _id: user_id User.findOne query, (error, user) -> return callback(error) if error - return callback(new Error("user not found")) if !user? + return callback(new Error("user not found #{user_id} for assignBonus")) if !user? logger.log user_id: user_id, refered_user_count: user.refered_user_count, "assigning bonus" if user.refered_user_count? and user.refered_user_count > 0 newFeatures = ReferalAllocator._calculateFeatures(user) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index 6dd21f1018..d24f8e53c8 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -84,12 +84,18 @@ module.exports = SubscriptionUpdater = groupSubscription: (cb)-> SubscriptionLocator.getGroupSubscriptionMemberOf user_id, cb async.series jobs, (err, results)-> + if err? + logger.err err:err, user_id:user, "error getting subscription or group for _setUsersMinimumFeatures" + return callback(err) {subscription, groupSubscription} = results if subscription? and subscription.planCode? + logger.log user_id:user_id, "using users subscription plan code for features" UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback else if groupSubscription? and groupSubscription.planCode? + logger.log user_id:user_id, "using group which user is memor of for features" UserFeaturesUpdater.updateFeatures user_id, groupSubscription.planCode, callback else + logger.log user_id:user_id, "using default features for user with no subscription or group" UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, (err)-> if err? logger.err err:err, user_id:user_id, "Error setting minimum user feature" From 81cd2adfd340876c4570e52275e83476ccbbdf6b Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 31 Mar 2016 17:36:06 +0100 Subject: [PATCH 118/208] member_ids != members_id --- .../coffee/Features/Subscription/SubscriptionUpdater.coffee | 2 +- .../coffee/Subscription/SubscriptionUpdaterTests.coffee | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index d24f8e53c8..b317be605a 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -71,7 +71,7 @@ module.exports = SubscriptionUpdater = subscription.groupPlan = true subscription.membersLimit = plan.membersLimit subscription.save -> - allIds = _.union subscription.members_id, [subscription.admin_id] + allIds = _.union subscription.member_ids, [subscription.admin_id] jobs = allIds.map (user_id)-> return (cb)-> SubscriptionUpdater._setUsersMinimumFeatures user_id, cb diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee index c91ca34023..3ae7eee71b 100644 --- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee @@ -18,14 +18,14 @@ describe "SubscriptionUpdater", -> @allUserIds = ["13213", "dsadas", "djsaiud89"] @subscription = subscription = admin_id: @adminUser._id - members_id: @allUserIds + member_ids: @allUserIds save: sinon.stub().callsArgWith(0) freeTrial:{} planCode:"student_or_something" @groupSubscription = admin_id: @adminUser._id - members_id: @allUserIds + member_ids: @allUserIds save: sinon.stub().callsArgWith(0) freeTrial:{} planCode:"group_subscription" From 3d719af71e693a837c2ee1efb08ad541b3c47977 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 31 Mar 2016 18:04:20 +0100 Subject: [PATCH 119/208] if subscription is downgraded don't use plan code on subscription, use default instead --- .../Features/Subscription/SubscriptionUpdater.coffee | 2 +- .../Subscription/SubscriptionUpdaterTests.coffee | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index b317be605a..a4a0864d4f 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -88,7 +88,7 @@ module.exports = SubscriptionUpdater = logger.err err:err, user_id:user, "error getting subscription or group for _setUsersMinimumFeatures" return callback(err) {subscription, groupSubscription} = results - if subscription? and subscription.planCode? + if subscription? and subscription.planCode? and subscription.planCode != Settings.defaultPlanCode logger.log user_id:user_id, "using users subscription plan code for features" UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback else if groupSubscription? and groupSubscription.planCode? diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee index 3ae7eee71b..ffef94157b 100644 --- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee @@ -183,8 +183,6 @@ describe "SubscriptionUpdater", -> describe "_setUsersMinimumFeatures", -> - - it "should call updateFeatures with the subscription if set", (done)-> @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null) @@ -205,6 +203,16 @@ describe "SubscriptionUpdater", -> assert.equal args[1], @groupSubscription.planCode done() + it "should call not call updateFeatures with users subscription if the subscription plan code is the default one (downgraded)", (done)-> + @subscription.planCode = @Settings.defaultPlanCode + @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) + @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, @groupSubscription) + @SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=> + args = @UserFeaturesUpdater.updateFeatures.args[0] + assert.equal args[0], @adminUser._id + assert.equal args[1], @groupSubscription.planCode + done() + it "should call updateFeatures with default if there are no subscriptions for user", (done)-> @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null) From a686d187ad99ae6b9995fceb9d2b8e33e18d0db6 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 1 Apr 2016 11:36:19 +0100 Subject: [PATCH 120/208] Pull refProviders into user model --- .../app/coffee/Features/Project/ProjectController.coffee | 1 + services/web/app/coffee/models/User.coffee | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 29fef997f3..f810067991 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -249,6 +249,7 @@ module.exports = ProjectController = freeTrial: {allowed: allowedFreeTrial} featureSwitches: user.featureSwitches features: user.features + refProviders: user.refProviders } userSettings: { mode : user.ace.mode diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index 0cd7196b64..4ceb87871f 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -36,6 +36,8 @@ UserSchema = new Schema compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup } templates: { type:Boolean, default: Settings.defaultFeatures.templates } references: { type:Boolean, default: Settings.defaultFeatures.references } + mendeley: { type:Boolean, default: Settings.defaultFeatures.mendeley } + zotero: { type:Boolean, default: Settings.defaultFeatures.zotero } } featureSwitches : { pdfng: { type: Boolean } @@ -54,6 +56,10 @@ UserSchema = new Schema # For example, a user signing up directly for a paid plan # has this set to true, despite never having had a free trial hadFreeTrial: {type: Boolean, default: false} + refProviders: { + mendeley: Boolean # coerce the refProviders values to Booleans + zotero: Boolean + } conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: 10) From 633839c98b828efbf656d16d06d272c1c227b980 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 4 Apr 2016 16:05:42 +0100 Subject: [PATCH 121/208] for support tickets include browser/os and hide email address if set already. This way we know who they are logged in with which has caused condfusion. --- services/web/app/views/contact-us-modal.jade | 4 +- .../web/public/coffee/main/contact-us.coffee | 7 +- services/web/public/js/libs/platform.js | 1130 +++++++++++++++++ 3 files changed, 1138 insertions(+), 3 deletions(-) create mode 100644 services/web/public/js/libs/platform.js diff --git a/services/web/app/views/contact-us-modal.jade b/services/web/app/views/contact-us-modal.jade index 32e096b7d5..e78670d911 100644 --- a/services/web/app/views/contact-us-modal.jade +++ b/services/web/app/views/contact-us-modal.jade @@ -12,9 +12,9 @@ script(type='text/ng-template', id='supportModalTemplate') | #{translate("subject")} .form-group input.field.text.medium.span8.form-control(ng-model="form.subject", maxlength='255', tabindex='1', onkeyup='') - label.desc + label.desc(ng-show="'#{getUserEmail()}'.length < 1") | #{translate("email")} - .form-group + .form-group(ng-show="'#{getUserEmail()}'.length < 1") input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '#{getUserEmail()}'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2') label#title12.desc | #{translate("project_url")} (#{translate("optional")}) diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee index 79d4caa4ab..08bf5bfa45 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -1,6 +1,7 @@ define [ "base" -], (App) -> + "libs/platform" +], (App, platform) -> App.controller 'ContactModal', ($scope, $modal) -> @@ -28,6 +29,8 @@ define [ message: message or "" subject: $scope.form.subject + " - [#{ticketNumber}]" labels: "support" + about: "
browser: #{platform?.name} #{platform?.version}
+
os: #{platform?.os?.family} #{platform?.os?.version}
" Groove.createTicket params, (err, json)-> $scope.sent = true @@ -59,3 +62,5 @@ define [ Groove.createTicket params, (err, json)-> $scope.sent = true $scope.$apply() + + diff --git a/services/web/public/js/libs/platform.js b/services/web/public/js/libs/platform.js new file mode 100644 index 0000000000..d50f7355cf --- /dev/null +++ b/services/web/public/js/libs/platform.js @@ -0,0 +1,1130 @@ +/*! + * Platform.js v1.3.1 + * Copyright 2014-2016 Benjamin Tan + * Copyright 2011-2013 John-David Dalton + * Available under MIT license + */ +;(function() { + 'use strict'; + + /** Used to determine if values are of the language type `Object`. */ + var objectTypes = { + 'function': true, + 'object': true + }; + + /** Used as a reference to the global object. */ + var root = (objectTypes[typeof window] && window) || this; + + /** Backup possible global object. */ + var oldRoot = root; + + /** Detect free variable `exports`. */ + var freeExports = objectTypes[typeof exports] && exports; + + /** Detect free variable `module`. */ + var freeModule = objectTypes[typeof module] && module && !module.nodeType && module; + + /** Detect free variable `global` from Node.js or Browserified code and use it as `root`. */ + var freeGlobal = freeExports && freeModule && typeof global == 'object' && global; + if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal || freeGlobal.self === freeGlobal)) { + root = freeGlobal; + } + + /** + * Used as the maximum length of an array-like object. + * See the [ES6 spec](http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength) + * for more details. + */ + var maxSafeInteger = Math.pow(2, 53) - 1; + + /** Regular expression to detect Opera. */ + var reOpera = /\bOpera/; + + /** Possible global object. */ + var thisBinding = this; + + /** Used for native method references. */ + var objectProto = Object.prototype; + + /** Used to check for own properties of an object. */ + var hasOwnProperty = objectProto.hasOwnProperty; + + /** Used to resolve the internal `[[Class]]` of values. */ + var toString = objectProto.toString; + + /*--------------------------------------------------------------------------*/ + + /** + * Capitalizes a string value. + * + * @private + * @param {string} string The string to capitalize. + * @returns {string} The capitalized string. + */ + function capitalize(string) { + string = String(string); + return string.charAt(0).toUpperCase() + string.slice(1); + } + + /** + * A utility function to clean up the OS name. + * + * @private + * @param {string} os The OS name to clean up. + * @param {string} [pattern] A `RegExp` pattern matching the OS name. + * @param {string} [label] A label for the OS. + */ + function cleanupOS(os, pattern, label) { + // Platform tokens are defined at: + // http://msdn.microsoft.com/en-us/library/ms537503(VS.85).aspx + // http://web.archive.org/web/20081122053950/http://msdn.microsoft.com/en-us/library/ms537503(VS.85).aspx + var data = { + '10.0': '10', + '6.4': '10 Technical Preview', + '6.3': '8.1', + '6.2': '8', + '6.1': '7 / Server 2008 R2', + '6.0': 'Vista / Server 2008', + '5.2': 'XP 64-bit / Server 2003', + '5.1': 'XP', + '5.01': '2000 SP1', + '5.0': '2000', + '4.0': 'NT', + '4.90': 'ME' + }; + // Detect Windows version from platform tokens. + if (pattern && label && /^Win/i.test(os) && !/^Windows Phone /i.test(os) && + (data = data[/[\d.]+$/.exec(os)])) { + os = 'Windows ' + data; + } + // Correct character case and cleanup string. + os = String(os); + + if (pattern && label) { + os = os.replace(RegExp(pattern, 'i'), label); + } + + os = format( + os.replace(/ ce$/i, ' CE') + .replace(/\bhpw/i, 'web') + .replace(/\bMacintosh\b/, 'Mac OS') + .replace(/_PowerPC\b/i, ' OS') + .replace(/\b(OS X) [^ \d]+/i, '$1') + .replace(/\bMac (OS X)\b/, '$1') + .replace(/\/(\d)/, ' $1') + .replace(/_/g, '.') + .replace(/(?: BePC|[ .]*fc[ \d.]+)$/i, '') + .replace(/\bx86\.64\b/gi, 'x86_64') + .replace(/\b(Windows Phone) OS\b/, '$1') + .replace(/\b(Chrome OS \w+) [\d.]+\b/, '$1') + .split(' on ')[0] + ); + + return os; + } + + /** + * An iteration utility for arrays and objects. + * + * @private + * @param {Array|Object} object The object to iterate over. + * @param {Function} callback The function called per iteration. + */ + function each(object, callback) { + var index = -1, + length = object ? object.length : 0; + + if (typeof length == 'number' && length > -1 && length <= maxSafeInteger) { + while (++index < length) { + callback(object[index], index, object); + } + } else { + forOwn(object, callback); + } + } + + /** + * Trim and conditionally capitalize string values. + * + * @private + * @param {string} string The string to format. + * @returns {string} The formatted string. + */ + function format(string) { + string = trim(string); + return /^(?:webOS|i(?:OS|P))/.test(string) + ? string + : capitalize(string); + } + + /** + * Iterates over an object's own properties, executing the `callback` for each. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} callback The function executed per own property. + */ + function forOwn(object, callback) { + for (var key in object) { + if (hasOwnProperty.call(object, key)) { + callback(object[key], key, object); + } + } + } + + /** + * Gets the internal `[[Class]]` of a value. + * + * @private + * @param {*} value The value. + * @returns {string} The `[[Class]]`. + */ + function getClassOf(value) { + return value == null + ? capitalize(value) + : toString.call(value).slice(8, -1); + } + + /** + * Host objects can return type values that are different from their actual + * data type. The objects we are concerned with usually return non-primitive + * types of "object", "function", or "unknown". + * + * @private + * @param {*} object The owner of the property. + * @param {string} property The property to check. + * @returns {boolean} Returns `true` if the property value is a non-primitive, else `false`. + */ + function isHostType(object, property) { + var type = object != null ? typeof object[property] : 'number'; + return !/^(?:boolean|number|string|undefined)$/.test(type) && + (type == 'object' ? !!object[property] : true); + } + + /** + * Prepares a string for use in a `RegExp` by making hyphens and spaces optional. + * + * @private + * @param {string} string The string to qualify. + * @returns {string} The qualified string. + */ + function qualify(string) { + return String(string).replace(/([ -])(?!$)/g, '$1?'); + } + + /** + * A bare-bones `Array#reduce` like utility function. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} callback The function called per iteration. + * @returns {*} The accumulated result. + */ + function reduce(array, callback) { + var accumulator = null; + each(array, function(value, index) { + accumulator = callback(accumulator, value, index, array); + }); + return accumulator; + } + + /** + * Removes leading and trailing whitespace from a string. + * + * @private + * @param {string} string The string to trim. + * @returns {string} The trimmed string. + */ + function trim(string) { + return String(string).replace(/^ +| +$/g, ''); + } + + /*--------------------------------------------------------------------------*/ + + /** + * Creates a new platform object. + * + * @memberOf platform + * @param {Object|string} [ua=navigator.userAgent] The user agent string or + * context object. + * @returns {Object} A platform object. + */ + function parse(ua) { + + /** The environment context object. */ + var context = root; + + /** Used to flag when a custom context is provided. */ + var isCustomContext = ua && typeof ua == 'object' && getClassOf(ua) != 'String'; + + // Juggle arguments. + if (isCustomContext) { + context = ua; + ua = null; + } + + /** Browser navigator object. */ + var nav = context.navigator || {}; + + /** Browser user agent string. */ + var userAgent = nav.userAgent || ''; + + ua || (ua = userAgent); + + /** Used to flag when `thisBinding` is the [ModuleScope]. */ + var isModuleScope = isCustomContext || thisBinding == oldRoot; + + /** Used to detect if browser is like Chrome. */ + var likeChrome = isCustomContext + ? !!nav.likeChrome + : /\bChrome\b/.test(ua) && !/internal|\n/i.test(toString.toString()); + + /** Internal `[[Class]]` value shortcuts. */ + var objectClass = 'Object', + airRuntimeClass = isCustomContext ? objectClass : 'ScriptBridgingProxyObject', + enviroClass = isCustomContext ? objectClass : 'Environment', + javaClass = (isCustomContext && context.java) ? 'JavaPackage' : getClassOf(context.java), + phantomClass = isCustomContext ? objectClass : 'RuntimeObject'; + + /** Detect Java environments. */ + var java = /\bJava/.test(javaClass) && context.java; + + /** Detect Rhino. */ + var rhino = java && getClassOf(context.environment) == enviroClass; + + /** A character to represent alpha. */ + var alpha = java ? 'a' : '\u03b1'; + + /** A character to represent beta. */ + var beta = java ? 'b' : '\u03b2'; + + /** Browser document object. */ + var doc = context.document || {}; + + /** + * Detect Opera browser (Presto-based). + * http://www.howtocreate.co.uk/operaStuff/operaObject.html + * http://dev.opera.com/articles/view/opera-mini-web-content-authoring-guidelines/#operamini + */ + var opera = context.operamini || context.opera; + + /** Opera `[[Class]]`. */ + var operaClass = reOpera.test(operaClass = (isCustomContext && opera) ? opera['[[Class]]'] : getClassOf(opera)) + ? operaClass + : (opera = null); + + /*------------------------------------------------------------------------*/ + + /** Temporary variable used over the script's lifetime. */ + var data; + + /** The CPU architecture. */ + var arch = ua; + + /** Platform description array. */ + var description = []; + + /** Platform alpha/beta indicator. */ + var prerelease = null; + + /** A flag to indicate that environment features should be used to resolve the platform. */ + var useFeatures = ua == userAgent; + + /** The browser/environment version. */ + var version = useFeatures && opera && typeof opera.version == 'function' && opera.version(); + + /** A flag to indicate if the OS begins with "Name Version /". */ + var isSpecialCasedOS; + + /* Detectable layout engines (order is important). */ + var layout = getLayout([ + { 'label': 'EdgeHTML', 'pattern': 'Edge' }, + 'Trident', + { 'label': 'WebKit', 'pattern': 'AppleWebKit' }, + 'iCab', + 'Presto', + 'NetFront', + 'Tasman', + 'KHTML', + 'Gecko' + ]); + + /* Detectable browser names (order is important). */ + var name = getName([ + 'Adobe AIR', + 'Arora', + 'Avant Browser', + 'Breach', + 'Camino', + 'Epiphany', + 'Fennec', + 'Flock', + 'Galeon', + 'GreenBrowser', + 'iCab', + 'Iceweasel', + 'K-Meleon', + 'Konqueror', + 'Lunascape', + 'Maxthon', + { 'label': 'Microsoft Edge', 'pattern': 'Edge' }, + 'Midori', + 'Nook Browser', + 'PaleMoon', + 'PhantomJS', + 'Raven', + 'Rekonq', + 'RockMelt', + 'SeaMonkey', + { 'label': 'Silk', 'pattern': '(?:Cloud9|Silk-Accelerated)' }, + 'Sleipnir', + 'SlimBrowser', + { 'label': 'SRWare Iron', 'pattern': 'Iron' }, + 'Sunrise', + 'Swiftfox', + 'WebPositive', + 'Opera Mini', + { 'label': 'Opera Mini', 'pattern': 'OPiOS' }, + 'Opera', + { 'label': 'Opera', 'pattern': 'OPR' }, + 'Chrome', + { 'label': 'Chrome Mobile', 'pattern': '(?:CriOS|CrMo)' }, + { 'label': 'Firefox', 'pattern': '(?:Firefox|Minefield)' }, + { 'label': 'Firefox Mobile', 'pattern': 'FxiOS' }, + { 'label': 'IE', 'pattern': 'IEMobile' }, + { 'label': 'IE', 'pattern': 'MSIE' }, + 'Safari' + ]); + + /* Detectable products (order is important). */ + var product = getProduct([ + { 'label': 'BlackBerry', 'pattern': 'BB10' }, + 'BlackBerry', + { 'label': 'Galaxy S', 'pattern': 'GT-I9000' }, + { 'label': 'Galaxy S2', 'pattern': 'GT-I9100' }, + { 'label': 'Galaxy S3', 'pattern': 'GT-I9300' }, + { 'label': 'Galaxy S4', 'pattern': 'GT-I9500' }, + 'Google TV', + 'Lumia', + 'iPad', + 'iPod', + 'iPhone', + 'Kindle', + { 'label': 'Kindle Fire', 'pattern': '(?:Cloud9|Silk-Accelerated)' }, + 'Nexus', + 'Nook', + 'PlayBook', + 'PlayStation 3', + 'PlayStation 4', + 'PlayStation Vita', + 'TouchPad', + 'Transformer', + { 'label': 'Wii U', 'pattern': 'WiiU' }, + 'Wii', + 'Xbox One', + { 'label': 'Xbox 360', 'pattern': 'Xbox' }, + 'Xoom' + ]); + + /* Detectable manufacturers. */ + var manufacturer = getManufacturer({ + 'Apple': { 'iPad': 1, 'iPhone': 1, 'iPod': 1 }, + 'Amazon': { 'Kindle': 1, 'Kindle Fire': 1 }, + 'Asus': { 'Transformer': 1 }, + 'Barnes & Noble': { 'Nook': 1 }, + 'BlackBerry': { 'PlayBook': 1 }, + 'Google': { 'Google TV': 1, 'Nexus': 1 }, + 'HP': { 'TouchPad': 1 }, + 'HTC': {}, + 'LG': {}, + 'Microsoft': { 'Xbox': 1, 'Xbox One': 1 }, + 'Motorola': { 'Xoom': 1 }, + 'Nintendo': { 'Wii U': 1, 'Wii': 1 }, + 'Nokia': { 'Lumia': 1 }, + 'Samsung': { 'Galaxy S': 1, 'Galaxy S2': 1, 'Galaxy S3': 1, 'Galaxy S4': 1 }, + 'Sony': { 'PlayStation 4': 1, 'PlayStation 3': 1, 'PlayStation Vita': 1 } + }); + + /* Detectable operating systems (order is important). */ + var os = getOS([ + 'Windows Phone ', + 'Android', + 'CentOS', + { 'label': 'Chrome OS', 'pattern': 'CrOS' }, + 'Debian', + 'Fedora', + 'FreeBSD', + 'Gentoo', + 'Haiku', + 'Kubuntu', + 'Linux Mint', + 'OpenBSD', + 'Red Hat', + 'SuSE', + 'Ubuntu', + 'Xubuntu', + 'Cygwin', + 'Symbian OS', + 'hpwOS', + 'webOS ', + 'webOS', + 'Tablet OS', + 'Linux', + 'Mac OS X', + 'Macintosh', + 'Mac', + 'Windows 98;', + 'Windows ' + ]); + + /*------------------------------------------------------------------------*/ + + /** + * Picks the layout engine from an array of guesses. + * + * @private + * @param {Array} guesses An array of guesses. + * @returns {null|string} The detected layout engine. + */ + function getLayout(guesses) { + return reduce(guesses, function(result, guess) { + return result || RegExp('\\b' + ( + guess.pattern || qualify(guess) + ) + '\\b', 'i').exec(ua) && (guess.label || guess); + }); + } + + /** + * Picks the manufacturer from an array of guesses. + * + * @private + * @param {Array} guesses An object of guesses. + * @returns {null|string} The detected manufacturer. + */ + function getManufacturer(guesses) { + return reduce(guesses, function(result, value, key) { + // Lookup the manufacturer by product or scan the UA for the manufacturer. + return result || ( + value[product] || + value[/^[a-z]+(?: +[a-z]+\b)*/i.exec(product)] || + RegExp('\\b' + qualify(key) + '(?:\\b|\\w*\\d)', 'i').exec(ua) + ) && key; + }); + } + + /** + * Picks the browser name from an array of guesses. + * + * @private + * @param {Array} guesses An array of guesses. + * @returns {null|string} The detected browser name. + */ + function getName(guesses) { + return reduce(guesses, function(result, guess) { + return result || RegExp('\\b' + ( + guess.pattern || qualify(guess) + ) + '\\b', 'i').exec(ua) && (guess.label || guess); + }); + } + + /** + * Picks the OS name from an array of guesses. + * + * @private + * @param {Array} guesses An array of guesses. + * @returns {null|string} The detected OS name. + */ + function getOS(guesses) { + return reduce(guesses, function(result, guess) { + var pattern = guess.pattern || qualify(guess); + if (!result && (result = + RegExp('\\b' + pattern + '(?:/[\\d.]+|[ \\w.]*)', 'i').exec(ua) + )) { + result = cleanupOS(result, pattern, guess.label || guess); + } + return result; + }); + } + + /** + * Picks the product name from an array of guesses. + * + * @private + * @param {Array} guesses An array of guesses. + * @returns {null|string} The detected product name. + */ + function getProduct(guesses) { + return reduce(guesses, function(result, guess) { + var pattern = guess.pattern || qualify(guess); + if (!result && (result = + RegExp('\\b' + pattern + ' *\\d+[.\\w_]*', 'i').exec(ua) || + RegExp('\\b' + pattern + '(?:; *(?:[a-z]+[_-])?[a-z]+\\d+|[^ ();-]*)', 'i').exec(ua) + )) { + // Split by forward slash and append product version if needed. + if ((result = String((guess.label && !RegExp(pattern, 'i').test(guess.label)) ? guess.label : result).split('/'))[1] && !/[\d.]+/.test(result[0])) { + result[0] += ' ' + result[1]; + } + // Correct character case and cleanup string. + guess = guess.label || guess; + result = format(result[0] + .replace(RegExp(pattern, 'i'), guess) + .replace(RegExp('; *(?:' + guess + '[_-])?', 'i'), ' ') + .replace(RegExp('(' + guess + ')[-_.]?(\\w)', 'i'), '$1 $2')); + } + return result; + }); + } + + /** + * Resolves the version using an array of UA patterns. + * + * @private + * @param {Array} patterns An array of UA patterns. + * @returns {null|string} The detected version. + */ + function getVersion(patterns) { + return reduce(patterns, function(result, pattern) { + return result || (RegExp(pattern + + '(?:-[\\d.]+/|(?: for [\\w-]+)?[ /-])([\\d.]+[^ ();/_-]*)', 'i').exec(ua) || 0)[1] || null; + }); + } + + /** + * Returns `platform.description` when the platform object is coerced to a string. + * + * @name toString + * @memberOf platform + * @returns {string} Returns `platform.description` if available, else an empty string. + */ + function toStringPlatform() { + return this.description || ''; + } + + /*------------------------------------------------------------------------*/ + + // Convert layout to an array so we can add extra details. + layout && (layout = [layout]); + + // Detect product names that contain their manufacturer's name. + if (manufacturer && !product) { + product = getProduct([manufacturer]); + } + // Clean up Google TV. + if ((data = /\bGoogle TV\b/.exec(product))) { + product = data[0]; + } + // Detect simulators. + if (/\bSimulator\b/i.test(ua)) { + product = (product ? product + ' ' : '') + 'Simulator'; + } + // Detect Opera Mini 8+ running in Turbo/Uncompressed mode on iOS. + if (name == 'Opera Mini' && /\bOPiOS\b/.test(ua)) { + description.push('running in Turbo/Uncompressed mode'); + } + // Detect iOS. + if (/^iP/.test(product)) { + name || (name = 'Safari'); + os = 'iOS' + ((data = / OS ([\d_]+)/i.exec(ua)) + ? ' ' + data[1].replace(/_/g, '.') + : ''); + } + // Detect Kubuntu. + else if (name == 'Konqueror' && !/buntu/i.test(os)) { + os = 'Kubuntu'; + } + // Detect Android browsers. + else if (manufacturer && manufacturer != 'Google' && + ((/Chrome/.test(name) && !/\bMobile Safari\b/i.test(ua)) || /\bVita\b/.test(product))) { + name = 'Android Browser'; + os = /\bAndroid\b/.test(os) ? os : 'Android'; + } + // Detect Silk desktop/accelerated modes. + else if (name == 'Silk') { + if (!/\bMobi/i.test(ua)) { + os = 'Android'; + description.unshift('desktop mode'); + } + if (/Accelerated *= *true/i.test(ua)) { + description.unshift('accelerated'); + } + } + // Detect PaleMoon identifying as Firefox. + else if (name == 'PaleMoon' && (data = /\bFirefox\/([\d.]+)\b/.exec(ua))) { + description.push('identifying as Firefox ' + data[1]); + } + // Detect Firefox OS and products running Firefox. + else if (name == 'Firefox' && (data = /\b(Mobile|Tablet|TV)\b/i.exec(ua))) { + os || (os = 'Firefox OS'); + product || (product = data[1]); + } + // Detect false positives for Firefox/Safari. + else if (!name || (data = !/\bMinefield\b/i.test(ua) && /\b(?:Firefox|Safari)\b/.exec(name))) { + // Escape the `/` for Firefox 1. + if (name && !product && /[\/,]|^[^(]+?\)/.test(ua.slice(ua.indexOf(data + '/') + 8))) { + // Clear name of false positives. + name = null; + } + // Reassign a generic name. + if ((data = product || manufacturer || os) && + (product || manufacturer || /\b(?:Android|Symbian OS|Tablet OS|webOS)\b/.test(os))) { + name = /[a-z]+(?: Hat)?/i.exec(/\bAndroid\b/.test(os) ? os : data) + ' Browser'; + } + } + // Detect non-Opera (Presto-based) versions (order is important). + if (!version) { + version = getVersion([ + '(?:Cloud9|CriOS|CrMo|Edge|FxiOS|IEMobile|Iron|Opera ?Mini|OPiOS|OPR|Raven|Silk(?!/[\\d.]+$))', + 'Version', + qualify(name), + '(?:Firefox|Minefield|NetFront)' + ]); + } + // Detect stubborn layout engines. + if ((data = + layout == 'iCab' && parseFloat(version) > 3 && 'WebKit' || + /\bOpera\b/.test(name) && (/\bOPR\b/.test(ua) ? 'Blink' : 'Presto') || + /\b(?:Midori|Nook|Safari)\b/i.test(ua) && !/^(?:Trident|EdgeHTML)$/.test(layout) && 'WebKit' || + !layout && /\bMSIE\b/i.test(ua) && (os == 'Mac OS' ? 'Tasman' : 'Trident') || + layout == 'WebKit' && /\bPlayStation\b(?! Vita\b)/i.test(name) && 'NetFront' + )) { + layout = [data]; + } + // Detect Windows Phone 7 desktop mode. + if (name == 'IE' && (data = (/; *(?:XBLWP|ZuneWP)(\d+)/i.exec(ua) || 0)[1])) { + name += ' Mobile'; + os = 'Windows Phone ' + (/\+$/.test(data) ? data : data + '.x'); + description.unshift('desktop mode'); + } + // Detect Windows Phone 8.x desktop mode. + else if (/\bWPDesktop\b/i.test(ua)) { + name = 'IE Mobile'; + os = 'Windows Phone 8.x'; + description.unshift('desktop mode'); + version || (version = (/\brv:([\d.]+)/.exec(ua) || 0)[1]); + } + // Detect IE 11. + else if (name != 'IE' && layout == 'Trident' && (data = /\brv:([\d.]+)/.exec(ua))) { + if (name) { + description.push('identifying as ' + name + (version ? ' ' + version : '')); + } + name = 'IE'; + version = data[1]; + } + // Leverage environment features. + if (useFeatures) { + // Detect server-side environments. + // Rhino has a global function while others have a global object. + if (isHostType(context, 'global')) { + if (java) { + data = java.lang.System; + arch = data.getProperty('os.arch'); + os = os || data.getProperty('os.name') + ' ' + data.getProperty('os.version'); + } + if (isModuleScope && isHostType(context, 'system') && (data = [context.system])[0]) { + os || (os = data[0].os || null); + try { + data[1] = context.require('ringo/engine').version; + version = data[1].join('.'); + name = 'RingoJS'; + } catch(e) { + if (data[0].global.system == context.system) { + name = 'Narwhal'; + } + } + } + else if (typeof context.process == 'object' && (data = context.process)) { + name = 'Node.js'; + arch = data.arch; + os = data.platform; + version = /[\d.]+/.exec(data.version)[0]; + } + else if (rhino) { + name = 'Rhino'; + } + } + // Detect Adobe AIR. + else if (getClassOf((data = context.runtime)) == airRuntimeClass) { + name = 'Adobe AIR'; + os = data.flash.system.Capabilities.os; + } + // Detect PhantomJS. + else if (getClassOf((data = context.phantom)) == phantomClass) { + name = 'PhantomJS'; + version = (data = data.version || null) && (data.major + '.' + data.minor + '.' + data.patch); + } + // Detect IE compatibility modes. + else if (typeof doc.documentMode == 'number' && (data = /\bTrident\/(\d+)/i.exec(ua))) { + // We're in compatibility mode when the Trident version + 4 doesn't + // equal the document mode. + version = [version, doc.documentMode]; + if ((data = +data[1] + 4) != version[1]) { + description.push('IE ' + version[1] + ' mode'); + layout && (layout[1] = ''); + version[1] = data; + } + version = name == 'IE' ? String(version[1].toFixed(1)) : version[0]; + } + os = os && format(os); + } + // Detect prerelease phases. + if (version && (data = + /(?:[ab]|dp|pre|[ab]\d+pre)(?:\d+\+?)?$/i.exec(version) || + /(?:alpha|beta)(?: ?\d)?/i.exec(ua + ';' + (useFeatures && nav.appMinorVersion)) || + /\bMinefield\b/i.test(ua) && 'a' + )) { + prerelease = /b/i.test(data) ? 'beta' : 'alpha'; + version = version.replace(RegExp(data + '\\+?$'), '') + + (prerelease == 'beta' ? beta : alpha) + (/\d+\+?/.exec(data) || ''); + } + // Detect Firefox Mobile. + if (name == 'Fennec' || name == 'Firefox' && /\b(?:Android|Firefox OS)\b/.test(os)) { + name = 'Firefox Mobile'; + } + // Obscure Maxthon's unreliable version. + else if (name == 'Maxthon' && version) { + version = version.replace(/\.[\d.]+/, '.x'); + } + // Detect Xbox 360 and Xbox One. + else if (/\bXbox\b/i.test(product)) { + os = null; + if (product == 'Xbox 360' && /\bIEMobile\b/.test(ua)) { + description.unshift('mobile mode'); + } + } + // Add mobile postfix. + else if ((/^(?:Chrome|IE|Opera)$/.test(name) || name && !product && !/Browser|Mobi/.test(name)) && + (os == 'Windows CE' || /Mobi/i.test(ua))) { + name += ' Mobile'; + } + // Detect IE platform preview. + else if (name == 'IE' && useFeatures && context.external === null) { + description.unshift('platform preview'); + } + // Detect BlackBerry OS version. + // http://docs.blackberry.com/en/developers/deliverables/18169/HTTP_headers_sent_by_BB_Browser_1234911_11.jsp + else if ((/\bBlackBerry\b/.test(product) || /\bBB10\b/.test(ua)) && (data = + (RegExp(product.replace(/ +/g, ' *') + '/([.\\d]+)', 'i').exec(ua) || 0)[1] || + version + )) { + data = [data, /BB10/.test(ua)]; + os = (data[1] ? (product = null, manufacturer = 'BlackBerry') : 'Device Software') + ' ' + data[0]; + version = null; + } + // Detect Opera identifying/masking itself as another browser. + // http://www.opera.com/support/kb/view/843/ + else if (this != forOwn && product != 'Wii' && ( + (useFeatures && opera) || + (/Opera/.test(name) && /\b(?:MSIE|Firefox)\b/i.test(ua)) || + (name == 'Firefox' && /\bOS X (?:\d+\.){2,}/.test(os)) || + (name == 'IE' && ( + (os && !/^Win/.test(os) && version > 5.5) || + /\bWindows XP\b/.test(os) && version > 8 || + version == 8 && !/\bTrident\b/.test(ua) + )) + ) && !reOpera.test((data = parse.call(forOwn, ua.replace(reOpera, '') + ';'))) && data.name) { + // When "identifying", the UA contains both Opera and the other browser's name. + data = 'ing as ' + data.name + ((data = data.version) ? ' ' + data : ''); + if (reOpera.test(name)) { + if (/\bIE\b/.test(data) && os == 'Mac OS') { + os = null; + } + data = 'identify' + data; + } + // When "masking", the UA contains only the other browser's name. + else { + data = 'mask' + data; + if (operaClass) { + name = format(operaClass.replace(/([a-z])([A-Z])/g, '$1 $2')); + } else { + name = 'Opera'; + } + if (/\bIE\b/.test(data)) { + os = null; + } + if (!useFeatures) { + version = null; + } + } + layout = ['Presto']; + description.push(data); + } + // Detect WebKit Nightly and approximate Chrome/Safari versions. + if ((data = (/\bAppleWebKit\/([\d.]+\+?)/i.exec(ua) || 0)[1])) { + // Correct build number for numeric comparison. + // (e.g. "532.5" becomes "532.05") + data = [parseFloat(data.replace(/\.(\d)$/, '.0$1')), data]; + // Nightly builds are postfixed with a "+". + if (name == 'Safari' && data[1].slice(-1) == '+') { + name = 'WebKit Nightly'; + prerelease = 'alpha'; + version = data[1].slice(0, -1); + } + // Clear incorrect browser versions. + else if (version == data[1] || + version == (data[2] = (/\bSafari\/([\d.]+\+?)/i.exec(ua) || 0)[1])) { + version = null; + } + // Use the full Chrome version when available. + data[1] = (/\bChrome\/([\d.]+)/i.exec(ua) || 0)[1]; + // Detect Blink layout engine. + if (data[0] == 537.36 && data[2] == 537.36 && parseFloat(data[1]) >= 28 && layout == 'WebKit') { + layout = ['Blink']; + } + // Detect JavaScriptCore. + // http://stackoverflow.com/questions/6768474/how-can-i-detect-which-javascript-engine-v8-or-jsc-is-used-at-runtime-in-androi + if (!useFeatures || (!likeChrome && !data[1])) { + layout && (layout[1] = 'like Safari'); + data = (data = data[0], data < 400 ? 1 : data < 500 ? 2 : data < 526 ? 3 : data < 533 ? 4 : data < 534 ? '4+' : data < 535 ? 5 : data < 537 ? 6 : data < 538 ? 7 : data < 601 ? 8 : '8'); + } else { + layout && (layout[1] = 'like Chrome'); + data = data[1] || (data = data[0], data < 530 ? 1 : data < 532 ? 2 : data < 532.05 ? 3 : data < 533 ? 4 : data < 534.03 ? 5 : data < 534.07 ? 6 : data < 534.10 ? 7 : data < 534.13 ? 8 : data < 534.16 ? 9 : data < 534.24 ? 10 : data < 534.30 ? 11 : data < 535.01 ? 12 : data < 535.02 ? '13+' : data < 535.07 ? 15 : data < 535.11 ? 16 : data < 535.19 ? 17 : data < 536.05 ? 18 : data < 536.10 ? 19 : data < 537.01 ? 20 : data < 537.11 ? '21+' : data < 537.13 ? 23 : data < 537.18 ? 24 : data < 537.24 ? 25 : data < 537.36 ? 26 : layout != 'Blink' ? '27' : '28'); + } + // Add the postfix of ".x" or "+" for approximate versions. + layout && (layout[1] += ' ' + (data += typeof data == 'number' ? '.x' : /[.+]/.test(data) ? '' : '+')); + // Obscure version for some Safari 1-2 releases. + if (name == 'Safari' && (!version || parseInt(version) > 45)) { + version = data; + } + } + // Detect Opera desktop modes. + if (name == 'Opera' && (data = /\bzbov|zvav$/.exec(os))) { + name += ' '; + description.unshift('desktop mode'); + if (data == 'zvav') { + name += 'Mini'; + version = null; + } else { + name += 'Mobile'; + } + os = os.replace(RegExp(' *' + data + '$'), ''); + } + // Detect Chrome desktop mode. + else if (name == 'Safari' && /\bChrome\b/.exec(layout && layout[1])) { + description.unshift('desktop mode'); + name = 'Chrome Mobile'; + version = null; + + if (/\bOS X\b/.test(os)) { + manufacturer = 'Apple'; + os = 'iOS 4.3+'; + } else { + os = null; + } + } + // Strip incorrect OS versions. + if (version && version.indexOf((data = /[\d.]+$/.exec(os))) == 0 && + ua.indexOf('/' + data + '-') > -1) { + os = trim(os.replace(data, '')); + } + // Add layout engine. + if (layout && !/\b(?:Avant|Nook)\b/.test(name) && ( + /Browser|Lunascape|Maxthon/.test(name) || + name != 'Safari' && /^iOS/.test(os) && /\bSafari\b/.test(layout[1]) || + /^(?:Adobe|Arora|Breach|Midori|Opera|Phantom|Rekonq|Rock|Sleipnir|Web)/.test(name) && layout[1])) { + // Don't add layout details to description if they are falsey. + (data = layout[layout.length - 1]) && description.push(data); + } + // Combine contextual information. + if (description.length) { + description = ['(' + description.join('; ') + ')']; + } + // Append manufacturer to description. + if (manufacturer && product && product.indexOf(manufacturer) < 0) { + description.push('on ' + manufacturer); + } + // Append product to description. + if (product) { + description.push((/^on /.test(description[description.length - 1]) ? '' : 'on ') + product); + } + // Parse the OS into an object. + if (os) { + data = + / ([\d.+]+)$/.exec(os) || + (isSpecialCasedOS = /^[a-z]+ ([\d.+]+) \//i.exec(os)); + os = { + 'architecture': 32, + 'family': (data && !isSpecialCasedOS) ? os.replace(data[0], '') : os, + 'version': data ? data[1] : null, + 'toString': function() { + var version = this.version; + return this.family + ((version && !isSpecialCasedOS) ? ' ' + version : '') + (this.architecture == 64 ? ' 64-bit' : ''); + } + }; + } + // Add browser/OS architecture. + if ((data = /\b(?:AMD|IA|Win|WOW|x86_|x)64\b/i.exec(arch)) && !/\bi686\b/i.test(arch)) { + if (os) { + os.architecture = 64; + os.family = os.family.replace(RegExp(' *' + data), ''); + } + if ( + name && (/\bWOW64\b/i.test(ua) || + (useFeatures && /\w(?:86|32)$/.test(nav.cpuClass || nav.platform) && !/\bWin64; x64\b/i.test(ua))) + ) { + description.unshift('32-bit'); + } + } + + ua || (ua = null); + + /*------------------------------------------------------------------------*/ + + /** + * The platform object. + * + * @name platform + * @type Object + */ + var platform = {}; + + /** + * The platform description. + * + * @memberOf platform + * @type string|null + */ + platform.description = ua; + + /** + * The name of the browser's layout engine. + * + * @memberOf platform + * @type string|null + */ + platform.layout = layout && layout[0]; + + /** + * The name of the product's manufacturer. + * + * @memberOf platform + * @type string|null + */ + platform.manufacturer = manufacturer; + + /** + * The name of the browser/environment. + * + * @memberOf platform + * @type string|null + */ + platform.name = name; + + /** + * The alpha/beta release indicator. + * + * @memberOf platform + * @type string|null + */ + platform.prerelease = prerelease; + + /** + * The name of the product hosting the browser. + * + * @memberOf platform + * @type string|null + */ + platform.product = product; + + /** + * The browser's user agent string. + * + * @memberOf platform + * @type string|null + */ + platform.ua = ua; + + /** + * The browser/environment version. + * + * @memberOf platform + * @type string|null + */ + platform.version = name && version; + + /** + * The name of the operating system. + * + * @memberOf platform + * @type Object + */ + platform.os = os || { + + /** + * The CPU architecture the OS is built for. + * + * @memberOf platform.os + * @type number|null + */ + 'architecture': null, + + /** + * The family of the OS. + * + * Common values include: + * "Windows", "Windows 7 / Server 2008 R2", "Windows Vista / Server 2008", + * "Windows XP", "OS X", "Ubuntu", "Debian", "Fedora", "Red Hat", "SuSE", + * "Android", "iOS" and "Windows Phone" + * + * @memberOf platform.os + * @type string|null + */ + 'family': null, + + /** + * The version of the OS. + * + * @memberOf platform.os + * @type string|null + */ + 'version': null, + + /** + * Returns the OS string. + * + * @memberOf platform.os + * @returns {string} The OS string. + */ + 'toString': function() { return 'null'; } + }; + + platform.parse = parse; + platform.toString = toStringPlatform; + + if (platform.version) { + description.unshift(version); + } + if (platform.name) { + description.unshift(name); + } + if (os && name && !(os == String(os).split(' ')[0] && (os == name.split(' ')[0] || product))) { + description.push(product ? '(' + os + ')' : 'on ' + os); + } + if (description.length) { + platform.description = description.join(' '); + } + return platform; + } + + /*--------------------------------------------------------------------------*/ + + // Export platform. + // Some AMD build optimizers, like r.js, check for condition patterns like the following: + if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) { + // Define as an anonymous module so platform can be aliased through path mapping. + define(function() { + return parse(); + }); + } + // Check for `exports` after `define` in case a build optimizer adds an `exports` object. + else if (freeExports && freeModule) { + // Export for CommonJS support. + forOwn(parse(), function(value, key) { + freeExports[key] = value; + }); + } + else { + // Export to the global object. + root.platform = parse(); + } +}.call(this)); \ No newline at end of file From 77eba3ea227835b43b418cff94f23e085175f3ec Mon Sep 17 00:00:00 2001 From: Damien Date: Mon, 4 Apr 2016 23:47:40 +0200 Subject: [PATCH 122/208] Correct alignment for word count dialog on less than `md` sized screen --- .../web/app/views/project/editor/left-menu.jade | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/services/web/app/views/project/editor/left-menu.jade b/services/web/app/views/project/editor/left-menu.jade index 0c6cf8250e..52ea62d505 100644 --- a/services/web/app/views/project/editor/left-menu.jade +++ b/services/web/app/views/project/editor/left-menu.jade @@ -198,21 +198,21 @@ script(type='text/ng-template', id='wordCountModalTemplate') div(ng-if="!status.loading") .container-fluid .row - .col-md-4 + .col-xs-4 .pull-right #{translate("total_words")} : - .col-md-6 {{data.textWords}} + .col-xs-6 {{data.textWords}} .row - .col-md-4 + .col-xs-4 .pull-right #{translate("headers")} : - .col-md-6 {{data.headers}} + .col-xs-6 {{data.headers}} .row - .col-md-4 + .col-xs-4 .pull-right #{translate("math_inline")} : - .col-md-6 {{data.mathInline}} + .col-xs-6 {{data.mathInline}} .row - .col-md-4 + .col-xs-4 .pull-right #{translate("math_display")} : - .col-md-6 {{data.mathDisplay}} + .col-xs-6 {{data.mathDisplay}} .modal-footer button.btn.btn-default( ng-disabled="state.inflight" From bd54218cc91687a758f5599e3a4b62f29cf3c682 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 5 Apr 2016 14:17:36 +0100 Subject: [PATCH 123/208] added grunt as depenency --- services/web/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/package.json b/services/web/package.json index dc2eeb77f4..1104db5b0d 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -23,6 +23,7 @@ "dateformat": "1.0.4-1.2.3", "express": "4.13.0", "express-session": "1.11.3", + "grunt": "^0.4.5", "heapdump": "^0.3.7", "http-proxy": "^1.8.1", "jade": "~1.3.1", From 424e09ec051930c591ee4fd1c4c2309eea78e707 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 5 Apr 2016 15:50:15 +0100 Subject: [PATCH 124/208] getGroupSubscriptionMemberOf gets plan code --- .../app/coffee/Features/Subscription/SubscriptionLocator.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee index c728ea65cd..d73a35a69d 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee @@ -25,4 +25,4 @@ module.exports = Subscription.findOne {member_ids: user_id, _id:subscription_id}, {_id:1}, callback getGroupSubscriptionMemberOf: (user_id, callback)-> - Subscription.findOne {member_ids: user_id}, {_id:1}, callback \ No newline at end of file + Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback \ No newline at end of file From 38c8eb868ae12f7333ad36b5acb06e579079480c Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Wed, 6 Apr 2016 11:30:26 -0300 Subject: [PATCH 125/208] Update package.json --- services/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/package.json b/services/web/package.json index 1104db5b0d..a2940f911f 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -27,7 +27,7 @@ "heapdump": "^0.3.7", "http-proxy": "^1.8.1", "jade": "~1.3.1", - "ldapjs": "^0.7.1", + "ldapjs": "^1.0.0", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.3.1", "lynx": "0.1.1", "marked": "^0.3.3", From ae79ce75b748f1a53928cd33226d84a60eb07c1a Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Wed, 6 Apr 2016 11:32:08 -0300 Subject: [PATCH 126/208] Update settings.defaults.coffee --- services/web/config/settings.defaults.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 0b3ee90e96..2c16d41a52 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -393,6 +393,10 @@ module.exports = # anonymous: false # adminDN: 'cn=read-only-admin,dc=example,dc=com' # adminPW: 'password' + # starttls: true + # tlsOptions: + # rejectUnauthorized: false + # ca: '/etc/ldap/ca_certs.pem' #templateLinks: [{ # name : "CV projects", From 010bb61bafc1da7180dd6c365a4e7720a7239ba8 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 11 Apr 2016 08:26:06 +0100 Subject: [PATCH 127/208] add michael pic --- services/web/public/img/about/michael.jpg | Bin 0 -> 8901 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/web/public/img/about/michael.jpg diff --git a/services/web/public/img/about/michael.jpg b/services/web/public/img/about/michael.jpg new file mode 100644 index 0000000000000000000000000000000000000000..afb6022a648ef2345468e03a9b26e63d2c45e693 GIT binary patch literal 8901 zcmeHrcT`i&yY41bDbkyOM2ZSX3q6#GNEc9P3JQi65Ry=(gMd^Kqzgz>iquH&g3<(( zD!m3!nslT?Zm|8%y6d}l-FyB$-|Xy}cV?b>=Y5_%d#{<`hH=vXjfRT43P3;r0BZOP zz+pjF%5FAR0HCD>@B;uq29Oa@10cLbg1=D&G(T--yzNXt2N2>*0{jIKoCAn|+5iwv z!1mXUCAj<>gZDrH`8k#Wk9!$x0}%bjzr~k;5*&p_{mQ}0I=WzS5s~ZQ>*}iNdWttr z`%hy#o#$5x5fPPuiikrYB4AM&sDunu90CC6{FDC7OR(yn^4H!8;rbl|KH88=zwM6z z@Eib0@g9jpAP~{#gnwcqIC)O=#|F_w5&XgsfCzzT0)YHi6rhXIpg;aWLeWHj+a;rk z|F$bclm4CyuT1or-?lp5CjX03N&gdrsE9~9!EX$nXFBMwKGx}ke{J_yXaA1XentJ) zHh4wzdn{h(%s+nc!DfPfx1ZXC|M8zm@}G8m*J(Oc7q7}szMy|n_@CMUfELdV1O)&_ zb^stAI(39kcwcm?`1%FBP4}}7+TX{QR{)ZP$M_PC!%~o1J3HG$g@sX0LU0STnWd09 z8Y%1sw-*)_5)lUEU~cwsb6ZPiu$d*o1|`q6R@=Y@wy}`sGL+B~(Xv;vw6;<6aJ1C* z(AG2eur-&l;DRZD<=midNPDEEGaT%Ov_m;T-Q>A`I)~!z(`I2V@K1=dtvuHqt(#ya zw4)_hLP%UlgbP2~(ZULP`?|{SvG|%i*Y6-*U0sD-#e~p~2w_nf85v;_h%f{qh(`!I zxucxnZh|N$?mr%`TRNFL+SohWpi$sck8m@zi?cize&(Mc!Cmk{{S$Xf{CoG4JRWfx z=Pv?C`+r9NQR)xS-Wu(UcCtqQAK-txf6l-K#?Jx0VTp25zb?-uF7aRLC@Zu)m#DO` zC|E)QpNR;IoZi0x>PR@kQriV-X6Y!;1rh(*pH`R#p+z1&c}xN=P7)U@KuU zAqhbVAxW^Yy88WJE>!H`PS#3jq&P_`(vE9z7jd#IA5CEVHaI{wB- zjQ?y1iiitBMD+fAHhzJ$&=xjU?*A9iU-h7;pkF@mOYxr^3gg$2@b86mYW`i33gCa! zzY6@Tz`qLotH8es{HwtKJO%!ua0o-o>*}8$nghT-8YwB7=r-vf| zfUy%80fcP4s_gh}JqQ4?gJ1xOsEmY+ID{Rf0Fa&TKSabKBJ3ck|5}mz1N!BQ_opwK zgQm6UPMzm>mC|39W~tTK;2Hs1GQb_MCM383fM^K_X$f%604IL)PE7DK{TznR@Mj+) zViHoaGvpL_Kso*dNI*ykA|fOvJ`Iz=`}Ax?L`zI}K~#~1{uZ2+%Ygyn7nMQAeXaOC zuA!@^Z(w-mu93Ngr4_>3 z=Ao0b%Oh7ecaMN4fkD9`p<&T6v2pPUFA|epz0Sh6=GaTl#3PR7-8iN8 zliB|pG5`M-v%kdt!)pvs0ti8%(;p%JM~Ht=WT%gX_zWp22^j@BB?SdJK0ixKO-V&d zMMXi)NKH#e&%nSyNzKU2NY6}5&p?mEJHbCtLSp{6?{sp|*K4k>KGiyu>@5qa{j#P7vZQe}r%Vk=m zSuWW&$>|aQs?SbiJtcxhDv?eiK`N%7L-!Poz zh$q+8hOsr-(#3({X`Qsd<9GEBjXn1;+-3`K7wOB#^(S$8(&H*+>Ug! zcv&0*ve9m!Akq(}EDEB=Ub-9l6}hfCT5xj&y|VQy_&hv>=Pf&eJN?q~J*xV5$*vdT zk1y`8(}#<3+WA7i<;!@-3^d&e9z*CLk?B=xTD4_I@X(3yN5FROsvSw`Bi%z4cYViv zI?0~Qv&p_1o$>1TmUIIiRruTZa5bzqmfJ|MEf>n`VRl?oH)4sVV;|V7amYg{4Xcl& z_L31rixr5QUwFr;>3R09>|M$rloyS@XHCFRv^8VhJLUf> zc^(mW&!+UDp=j3fhkN-Vp^EK z{C5C z?zA>N)^Q}`-Yg0n-<;15%#WSk(Dy1n?l-U)!S#(iIm>^f1S*k<^lz|f2r)d^ZCNoJ z?X#IJ?i-a=iXeqpN%Vkh)C%1dmf*0xYbsVq`+fSl%; zbp}o9$Su>CqsM`}fi6@^Y7Nfto7g!_AB@#9jsgXOEA!Otq||JM$m99%KX=d%Dwt{difd^m!7v zE7Yew4c|sa<`s%YQ1N6*#as4B~FZC-d)p!sv{#^4eDCM9E={!2};<=m<+DlTy5;vF3DOo72mxw1H>se#D2=@IlihI3>URphGdWD)o%x}dI#yOPLbtE~qI zl%~Bel$|zg5)}AgBpw7uWg90)`%yKobhdv#vmi@JzL_hl(l)3U<~V=YVA}3$>X?=}iZW>8 z5Zs|WCagMDEI5EGp4upH?px&@tHV3k1}k%vf{@MA+{?+I)o&5pZ&ZKWc%svgQu!-=`(d?i;$*ElWw+n3m(!n#!rH4YaU2-N?&dMvF~(1zO}LR zC2^ZoNxRMqv(M>(Ss|E4Q%ogmDVdZp(|XV3I0m%|M47~__wPJjPiVeudPsl!u6g9Q z$qqfKo^xA^Em^tSy7{ac?Moh6<>QbKC3Gf|Y?To&Fz5Gb57qZrlv+IFtV&`Qs`A+4EJ=JM>hS8;W3=JE0kDLUvCOV8*k%igt zII2_4wk-PtQSTSa+Jw+b?tU3xY*V(+bQ=JQ%b%ItAu zw#?{+K?Uo}!lTE~HKgnuPa}`hwlS3%PsSPgG+8aOqz+6!2XdBBOFY4-6>6t`RnvAWHx2OTmx?!_&tY!`9 z2LIJ@S=CE?5l?;`9So=&eY)k<8x76xK5M;uO~sStXz;M*xeF$^0eyU4P!>DTJ6zl> zgth+20<4+H=PfGo7u(iI7w;aFD_`nOlk@33Asg_5ZSby^Pe5n7p{Tj7ep_Uep9*2s zgNBZGr7s*zuVrQyMRqOs+b2JrV6DV2Q+<{++|Wd2K4i>0dr~cOgO4#ZY9qL1ioiCN zY%k7yhQ&_EsE=(yh`3p_lph(T&Jz)UTD4uK4o+o0&$fs4#n>>lqp=wJCUz{tH1DR+ z#o3Cl!#)}H?pwu0Q?NbKFLQDwIf5=vb=I^vE%&TODMIv$#KMASlmk*Ed!6qz_qRbm zE-7r8OE<0s1cc4px|h?#j~V6dnRE6%=hk+kFT0(O#1~MumnB6L(!uD-v zgPRuK3GQK<4g|NZi+2iSJ zdY6^)NGCJ98oN9uqej7F;Rt~mcEKp#!E8;~Hi`n)r<#2&>-&)|kobedNA&!$$dJZx zNhqqz!(p=8!8vyNC{k0P1tHBskH7x`Lom4{_+ND2o~Fcy}(1{m~} zzxbw#0}RY2+RSUEF-*d`t2Q^|+#Oqn*S`CJ1@0sSG#S;^8iMmONSB;i>{Gcm2D5S} z>`K<(Wie!~pFAX*|MIP7skL`_Qs%~kmnq5_L!XJiVuA`&4?7|rNjnF2zSwD3=MtmnzbdT zDdMt1mF_04SrbV8*LFzfl^j9t;!e@si?yNqm~@R%PS99PEo4>MFq<3KJ5`C|Kaz+F zwM8USnXLI>9=#c86^fdQl8ajjImv= z*N>MlV9Zt=&vVcRcB+Zm)Cp^~TfL zIzkO6iNM&@>sRW6!2aLmOMoLP7P2k92J^12hi~BfzKJJBY1)fS+1;w;fmQqg;xX$2 zV~CHeuB5DaW4V5~zR|#S0Togf#;8z~jI5K1?M4&UCvY~BTGJ(OB%$^(O8vEC0gP5X zd;|9QgmuF-NKWF7TQW8?deUTMN(b$mQHw=iP}w(vxZTxv_&z~xrZ9Y*3bSrFd!+jI z30ryjml@)V#Wf1|dTZ7{cDNIrTVKxNkGP+9B)Y@GCw<$0VPI(a*f(lBtKpfmrhw(v ze27EW9ZGM34h2Ck&vmr_b~Re!zDesOarb1b)7~w1sb6kEry)FufNq7dE0>gSfAn3=9(~=e_X#o*y_=^$Qr-g zF$$Xoe9Ci~zdxN`Bh!%J(4O2fe)n|#?hoVOs!bd)VZcM(XaMo7%4btqx!gX!{Zt+@ zbz?meTipvo53`}IBdD;=A>9w1)l8{ZzO2%>A!9f z{G@^d^lLtr^$+YMitZ*R@7=VptZ!uH0rk}qg~F_+*p^^4XknLQt5zz>yUAvX6s3In z3nLGxVHyXjdcADA6`CPDT-QyjR7o4AZRa=6{y?DS$EOp7&Qo-5NyrK3d&f(J&4V%Q z1Hz2s?}{%6QGUNfQo3N%S@YVYz<| z2VgoNUYXrmBg7%!89c(q2z65HOp)4GIxwY8o!vSOCj?tQPl6`juNB2#E+|S#kC<_` z;jVGpmu$J25T1Ic^D!uw{aYFGVntbf*`eT%iWCplxS77~P@XS_tW9;dQN!ew9l&9@ z;#{~l<;dnOm55-cd>c;7%05`dHvEcc>~%!_voF!t-_Xp^==tWhs=j3{N?4h^gaa~y z@8W>>*QaLhRP)$;U&8^E-MGQ7AdK)hOykqXz1_B{P06LSABW6a1v)I=Q#_H5tsiW95THj#^=P?_v_hj0}UOCnZ*5)hR&S@CuTiZ6jpEP8;lzcqOeTdmE zQZ8P2`6+FaK4#hU1r8`vJ&MNx=Y5Cq|D#wd-`_`Um}bl&Oy8R{yuPp}j03nQOL2f% zKMs( Date: Mon, 11 Apr 2016 09:55:53 +0100 Subject: [PATCH 128/208] In Safari, limit iterations of the command-parsing loop. --- .../auto-complete/SuggestionManager.coffee | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee index e0c3710358..914f43ee6b 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee @@ -1,11 +1,30 @@ define [], () -> + + browserIsSafari = () -> + userAgent = navigator.userAgent + ( + userAgent.match(/.*Safari\/.*/) && + !userAgent.match(/.*Chrome\/.*/) && + !userAgent.match(/.*Chromium\/.*/) + ) + + class Parser constructor: (@doc) -> parse: () -> + limit = null + if browserIsSafari() + limit = 100 + commands = [] seen = {} + iterations = 0 while command = @nextCommand() + iterations += 1 + if limit && iterations > limit + return commands + docState = @doc optionalArgs = 0 From 8d6003dcc5b5b5c29a93ed773937ac7b4a39f7e1 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 11 Apr 2016 09:58:37 +0100 Subject: [PATCH 129/208] Add commentary --- .../directives/aceEditor/auto-complete/SuggestionManager.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee index 914f43ee6b..3e5066d5d8 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee @@ -13,6 +13,8 @@ define [], () -> constructor: (@doc) -> parse: () -> + # Safari regex is super slow, freezes browser for minutes on end, + # hacky solution: limit iterations limit = null if browserIsSafari() limit = 100 From e3b2ec997749f8e40c04e79ccde446f0e5d868d0 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 11 Apr 2016 14:35:59 +0100 Subject: [PATCH 130/208] Add a `ide.browserIsSafari` boolean flag. --- services/web/public/coffee/ide.coffee | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 161289d055..af6111cd44 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -112,4 +112,17 @@ define [ ide.localStorage = localStorage + ide.browserIsSafari = false + try + userAgent = navigator.userAgent + ide.browserIsSafari = ( + userAgent && + userAgent.match(/.*Safari\/.*/) && + !userAgent.match(/.*Chrome\/.*/) && + !userAgent.match(/.*Chromium\/.*/) + ) + catch err + console.error err + + angular.bootstrap(document.body, ["SharelatexApp"]) From 76697599aeb70ccb3233bf5be619f959e8cbc4d1 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 11 Apr 2016 14:45:26 +0100 Subject: [PATCH 131/208] Use the _ide version of browserIsSafari --- .../aceEditor/auto-complete/SuggestionManager.coffee | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee index 3e5066d5d8..97241a90ce 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee @@ -1,14 +1,5 @@ define [], () -> - browserIsSafari = () -> - userAgent = navigator.userAgent - ( - userAgent.match(/.*Safari\/.*/) && - !userAgent.match(/.*Chrome\/.*/) && - !userAgent.match(/.*Chromium\/.*/) - ) - - class Parser constructor: (@doc) -> @@ -16,7 +7,7 @@ define [], () -> # Safari regex is super slow, freezes browser for minutes on end, # hacky solution: limit iterations limit = null - if browserIsSafari() + if window?._ide?.browserIsSafari limit = 100 commands = [] From ffa04c7b554fb4bd2a472e975914542346ea9dad Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 13 Apr 2016 15:38:04 +0100 Subject: [PATCH 132/208] add project url onto query string for compile hashing --- services/web/app/coffee/Features/Compile/ClsiManager.coffee | 2 ++ .../web/app/coffee/Features/Compile/CompileController.coffee | 1 + 2 files changed, 3 insertions(+) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 174ebe830b..0b8cf1b0b3 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -37,6 +37,8 @@ module.exports = ClsiManager = url: "#{compilerUrl}/project/#{project_id}/compile" json: req jar: false + query: + project_id:project_id }, (error, response, body) -> return callback(error) if error? if 200 <= response.statusCode < 300 diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index 89fbc87e2e..54b3b1b598 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -127,6 +127,7 @@ module.exports = CompileController = for h, v of req.headers newHeaders[h] = req.headers[h] if h.match /^(If-|Range)/i options.headers = newHeaders + req.query.project_id = project_id proxy = request(options) proxy.pipe(res) proxy.on "error", (error) -> From 9d027b88ba964be77c458a93617f9fb3037597c9 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 15 Apr 2016 09:45:48 +0100 Subject: [PATCH 133/208] Bug fix: re-introduce the project-too-large error message --- services/web/app/views/project/editor/pdf.jade | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index f633060aaf..c596fd915f 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -210,8 +210,10 @@ div.full-size.pdf(ng-controller="PdfController") sixpack-convert="track_changes_feature_info" ng-click="startFreeTrial('compile-timeout')" ) #{translate("start_free_trial")} - // // // // + .alert.alert-danger(ng-show="pdf.projectTooLarge") + strong #{translate("project_too_large")} + span #{translate("project_too_large_please_reduce")} script(type='text/ng-template', id='clearCacheModalTemplate') From 145b201087ccdcd0c863ab79269625fecdc976ef Mon Sep 17 00:00:00 2001 From: Caleb Cooper Date: Fri, 15 Apr 2016 13:46:11 -0400 Subject: [PATCH 134/208] Put brackets around the example ca path for ldap starttls support in the default config. This resolves an issue where the system was breaking the path into an array and trying to act on the first item in that array, "/", rather than the full path. --- 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 2c16d41a52..ee77b43cb6 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -396,7 +396,7 @@ module.exports = # starttls: true # tlsOptions: # rejectUnauthorized: false - # ca: '/etc/ldap/ca_certs.pem' + # ca: ['/etc/ldap/ca_certs.pem'] #templateLinks: [{ # name : "CV projects", From d54515a9a7ad1565bf86a2d06eee6b6f40eb9cf1 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 19 Apr 2016 11:53:27 +0100 Subject: [PATCH 135/208] add a `featureToggle` property to ide. --- services/web/public/coffee/ide.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index af6111cd44..ceebadac46 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -124,5 +124,8 @@ define [ catch err console.error err + # User can append ?ft=somefeature to url to activate a feature toggle + ide.featureToggle = location?.search?.match(/^\?ft=(\w+)$/)?[1] + angular.bootstrap(document.body, ["SharelatexApp"]) From b37595acf9d98f2de2ceb258bf1cf8077708a90f Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 19 Apr 2016 16:48:51 +0100 Subject: [PATCH 136/208] persist cookie in redis for compiles. --- .../Features/Compile/ClsiManager.coffee | 29 +++-- .../Compile/ClsiRequestManager.coffee | 60 ++++++++++ .../Features/Compile/CompileController.coffee | 55 +++++---- .../app/coffee/infrastructure/Server.coffee | 2 +- services/web/package.json | 1 + .../coffee/Compile/ClsiManagerTests.coffee | 44 +++---- .../Compile/ClsiRequestManagerTests.coffee | 111 ++++++++++++++++++ .../Compile/CompileControllerTests.coffee | 16 ++- 8 files changed, 262 insertions(+), 56 deletions(-) create mode 100644 services/web/app/coffee/Features/Compile/ClsiRequestManager.coffee create mode 100644 services/web/test/UnitTests/coffee/Compile/ClsiRequestManagerTests.coffee diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 0b8cf1b0b3..6f4efabcf9 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -6,8 +6,19 @@ Project = require("../../models/Project").Project ProjectEntityHandler = require("../Project/ProjectEntityHandler") logger = require "logger-sharelatex" url = require("url") +ClsiRequestManager = require("./ClsiRequestManager") + module.exports = ClsiManager = + + _makeRequest: (project_id, opts, callback)-> + ClsiRequestManager.getCookieJar project_id, (err, jar)-> + if err? + logger.err err:err, "error getting cookie jar for clsi request" + return callback(err) + opts.jar = jar + request opts, callback + sendRequest: (project_id, options = {}, callback = (error, success) ->) -> ClsiManager._buildRequest project_id, options, (error, req) -> return callback(error) if error? @@ -23,7 +34,10 @@ module.exports = ClsiManager = deleteAuxFiles: (project_id, options, callback = (error) ->) -> compilerUrl = @_getCompilerUrl(options?.compileGroup) - request.del "#{compilerUrl}/project/#{project_id}", callback + opts = + url:"#{compilerUrl}/project/#{project_id}" + method:"DELETE" + ClsiManager._makeRequest project_id, opts, callback _getCompilerUrl: (compileGroup) -> if compileGroup == "priority" @@ -33,13 +47,11 @@ module.exports = ClsiManager = _postToClsi: (project_id, req, compileGroup, callback = (error, response) ->) -> compilerUrl = @_getCompilerUrl(compileGroup) - request.post { + opts = url: "#{compilerUrl}/project/#{project_id}/compile" json: req - jar: false - query: - project_id:project_id - }, (error, response, body) -> + method: "POST" + ClsiManager._makeRequest project_id, opts, (error, response, body) -> return callback(error) if error? if 200 <= response.statusCode < 300 callback null, body @@ -117,9 +129,10 @@ module.exports = ClsiManager = wordcount_url = "#{compilerUrl}/project/#{project_id}/wordcount?file=#{encodeURIComponent(filename)}" if req.compile.options.imageName? wordcount_url += "&image=#{encodeURIComponent(req.compile.options.imageName)}" - request.get { + opts = url: wordcount_url - }, (error, response, body) -> + method: "GET" + ClsiManager._makeRequest project_id, opts, (error, response, body) -> return callback(error) if error? if 200 <= response.statusCode < 300 callback null, body diff --git a/services/web/app/coffee/Features/Compile/ClsiRequestManager.coffee b/services/web/app/coffee/Features/Compile/ClsiRequestManager.coffee new file mode 100644 index 0000000000..9de06c1538 --- /dev/null +++ b/services/web/app/coffee/Features/Compile/ClsiRequestManager.coffee @@ -0,0 +1,60 @@ +Settings = require "settings-sharelatex" +request = require('request') +redis = require("redis-sharelatex") +rclient = redis.createClient(Settings.redis.web) +cookie = require('cookie') + +buildKey = (project_id)-> + return "clsiserver:#{project_id}" + + +ONE_WEEK_IN_SECONDS = 60 * 60 * 24 * 7 + +module.exports = ClsiRequestManager = + + _getServerId : (project_id, callback = (err, serverId)->)-> + multi = rclient.multi() + multi.get buildKey(project_id) + multi.expire buildKey(project_id), ONE_WEEK_IN_SECONDS + multi.exec (err, results)-> + if err? + return callback(err) + serverId = results[0] + if serverId? + return callback(null, serverId) + else + return ClsiRequestManager._getServerIdViaRequest project_id, callback + + + _getServerIdViaRequest :(project_id, callback = (err, serverId)->)-> + url = "#{Settings.apis.clsi.url}/project/#{project_id}/status" + request.get url, (err, res, body)-> + if err? + logger.err err:err, project_id:project_id, "error getting initial server id for project" + return callback(err) + ClsiRequestManager.setServerId project_id, res, callback + + _parseServerIdFromResponse : (response)-> + console.log response.headers + cookies = cookie.parse(response.headers["set-cookie"]?[0] or "") + return cookies?.clsiserver + + setServerId: (project_id, response, callback = ->)-> + serverId = ClsiRequestManager._parseServerIdFromResponse(response) + multi = rclient.multi() + multi.set buildKey(project_id), serverId + multi.expire buildKey(project_id), ONE_WEEK_IN_SECONDS + multi.exec callback + + + getCookieJar: (project_id, opts, callback = (err, jar)->)-> + ClsiRequestManager._getServerId project_id, (err, serverId)=> + if err? + logger.err err:err, project_id:project_id, "error getting server id" + return callback(err) + cookie = request.cookie("clsiserver=#{serverId}") + jar = request.jar() + jar.setCookie cookie, Settings.apis.clsi.url + callback(null, jar) + + diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index 54b3b1b598..0e600dc3b0 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -8,6 +8,8 @@ Settings = require "settings-sharelatex" AuthenticationController = require "../Authentication/AuthenticationController" UserGetter = require "../User/UserGetter" RateLimiter = require("../../infrastructure/RateLimiter") +ClsiRequestManager = require("./ClsiRequestManager") + module.exports = CompileController = compile: (req, res, next = (error) ->) -> @@ -107,31 +109,34 @@ module.exports = CompileController = CompileController.proxyToClsiWithLimits(project_id, url, limits, req, res, next) proxyToClsiWithLimits: (project_id, url, limits, req, res, next = (error) ->) -> - if limits.compileGroup == "priority" - compilerUrl = Settings.apis.clsi_priority.url - else - compilerUrl = Settings.apis.clsi.url - url = "#{compilerUrl}#{url}" - logger.log url: url, "proxying to CLSI" - oneMinute = 60 * 1000 - # the base request - options = { url: url, method: req.method, timeout: oneMinute } - # if we have a build parameter, pass it through to the clsi - if req.query?.pdfng && req.query?.build? # only for new pdf viewer - options.qs = {} - options.qs.build = req.query.build - # if we are byte serving pdfs, pass through If-* and Range headers - # do not send any others, there's a proxying loop if Host: is passed! - if req.query?.pdfng - newHeaders = {} - for h, v of req.headers - newHeaders[h] = req.headers[h] if h.match /^(If-|Range)/i - options.headers = newHeaders - req.query.project_id = project_id - proxy = request(options) - proxy.pipe(res) - proxy.on "error", (error) -> - logger.warn err: error, url: url, "CLSI proxy error" + ClsiRequestManager.getCookieJar project_id, (err, jar)-> + if err? + logger.err err:err, "error getting cookie jar for clsi request" + return callback(err) + if limits.compileGroup == "priority" + compilerUrl = Settings.apis.clsi_priority.url + else + compilerUrl = Settings.apis.clsi.url + url = "#{compilerUrl}#{url}" + logger.log url: url, "proxying to CLSI" + oneMinute = 60 * 1000 + # the base request + options = { url: url, method: req.method, timeout: oneMinute, jar : jar } + # if we have a build parameter, pass it through to the clsi + if req.query?.pdfng && req.query?.build? # only for new pdf viewer + options.qs = {} + options.qs.build = req.query.build + # if we are byte serving pdfs, pass through If-* and Range headers + # do not send any others, there's a proxying loop if Host: is passed! + if req.query?.pdfng + newHeaders = {} + for h, v of req.headers + newHeaders[h] = req.headers[h] if h.match /^(If-|Range)/i + options.headers = newHeaders + proxy = request(options) + proxy.pipe(res) + proxy.on "error", (error) -> + logger.warn err: error, url: url, "CLSI proxy error" wordCount: (req, res, next) -> project_id = req.params.Project_id diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index fea8752bb2..11a3b5237e 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -125,7 +125,7 @@ apiRouter.get "/profile", (req, res) -> , time app.get "/heapdump", (req, res)-> - require('heapdump').writeSnapshot '/tmp/' + Date.now() + '.clsi.heapsnapshot', (err, filename)-> + require('heapdump').writeSnapshot '/tmp/' + Date.now() + '.web.heapsnapshot', (err, filename)-> res.send filename logger.info ("creating HTTP server").yellow diff --git a/services/web/package.json b/services/web/package.json index a2940f911f..33b6298fcc 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -18,6 +18,7 @@ "body-parser": "^1.13.1", "bufferedstream": "1.6.0", "connect-redis": "2.3.0", + "cookie": "^0.2.3", "cookie-parser": "1.3.5", "csurf": "^1.8.3", "dateformat": "1.0.4-1.2.3", diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee index 3aa5b80528..00400542d8 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee @@ -7,6 +7,9 @@ SandboxedModule = require('sandboxed-module') describe "ClsiManager", -> beforeEach -> + @jar = {cookie:"stuff"} + @ClsiRequestManager = + getCookieJar: sinon.stub().callsArgWith(1, null, @jar) @ClsiManager = SandboxedModule.require modulePath, requires: "settings-sharelatex": @settings = apis: @@ -19,8 +22,9 @@ describe "ClsiManager", -> url: "https://clsipremium.example.com" "../../models/Project": Project: @Project = {} "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} + "./ClsiRequestManager": @ClsiRequestManager "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } - "request": @request = {} + "request": @request = sinon.stub() @project_id = "project-id" @callback = sinon.stub() @@ -80,15 +84,15 @@ describe "ClsiManager", -> describe "deleteAuxFiles", -> beforeEach -> - @request.del = sinon.stub().callsArg(1) + @ClsiManager._makeRequest = sinon.stub().callsArg(2) describe "with the standard compileGroup", -> beforeEach -> @ClsiManager.deleteAuxFiles @project_id, {compileGroup: "standard"}, @callback it "should call the delete method in the standard CLSI", -> - @request.del - .calledWith("#{@settings.apis.clsi.url}/project/#{@project_id}") + @ClsiManager._makeRequest + .calledWith(@project_id, { method:"DELETE", url:"#{@settings.apis.clsi.url}/project/#{@project_id}"}) .should.equal true it "should call the callback", -> @@ -99,8 +103,8 @@ describe "ClsiManager", -> @ClsiManager.deleteAuxFiles @project_id, {compileGroup: "priority"}, @callback it "should call the delete method in the CLSI", -> - @request.del - .calledWith("#{@settings.apis.clsi_priority.url}/project/#{@project_id}") + @ClsiManager._makeRequest + .calledWith(@project_id, { method:"DELETE", url:"#{@settings.apis.clsi_priority.url}/project/#{@project_id}"}) .should.equal true describe "_buildRequest", -> @@ -235,15 +239,15 @@ describe "ClsiManager", -> describe "successfully", -> beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}, @body = { mock: "foo" }) + @ClsiManager._makeRequest = sinon.stub().callsArgWith(2, null, {statusCode: 204}, @body = { mock: "foo" }) @ClsiManager._postToClsi @project_id, @req, "standard", @callback it 'should send the request to the CLSI', -> url = "#{@settings.apis.clsi.url}/project/#{@project_id}/compile" - @request.post.calledWith({ + @ClsiManager._makeRequest.calledWith(@project_id, { + method: "POST", url: url json: @req - jar: false }).should.equal true it "should call the callback with the body and no error", -> @@ -251,7 +255,7 @@ describe "ClsiManager", -> describe "when the CLSI returns an error", -> beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 500}, @body = { mock: "foo" }) + @ClsiManager._makeRequest = sinon.stub().callsArgWith(2, null, {statusCode: 500}, @body = { mock: "foo" }) @ClsiManager._postToClsi @project_id, @req, "standard", @callback it "should call the callback with the body and the error", -> @@ -259,20 +263,20 @@ describe "ClsiManager", -> describe "when the compiler is priority", -> beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 500}, @body = { mock: "foo" }) + @ClsiManager._makeRequest = sinon.stub() @ClsiManager._postToClsi @project_id, @req, "priority", @callback it "should use the clsi_priority url", -> url = "#{@settings.apis.clsi_priority.url}/project/#{@project_id}/compile" - @request.post.calledWith({ + @ClsiManager._makeRequest.calledWith(@project_id, { + method: "POST", url: url json: @req - jar: false }).should.equal true describe "wordCount", -> beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body = { mock: "foo" }) + @ClsiManager._makeRequest = sinon.stub().callsArgWith(2, null, {statusCode: 200}, @body = { mock: "foo" }) @ClsiManager._buildRequest = sinon.stub().callsArgWith(2, null, @req = { compile: { rootResourcePath: "rootfile.text", options: {} } }) @ClsiManager._getCompilerUrl = sinon.stub().returns "compiler.url" @@ -281,8 +285,8 @@ describe "ClsiManager", -> @ClsiManager.wordCount @project_id, false, {}, @callback it "should call wordCount with root file", -> - @request.get - .calledWith({ url: "compiler.url/project/#{@project_id}/wordcount?file=rootfile.text" }) + @ClsiManager._makeRequest + .calledWith(@project_id, { method: "GET", url: "compiler.url/project/#{@project_id}/wordcount?file=rootfile.text" }) .should.equal true it "should call the callback", -> @@ -293,8 +297,8 @@ describe "ClsiManager", -> @ClsiManager.wordCount @project_id, "main.tex", {}, @callback it "should call wordCount with param file", -> - @request.get - .calledWith({ url: "compiler.url/project/#{@project_id}/wordcount?file=main.tex" }) + @ClsiManager._makeRequest + .calledWith(@project_id, { method: "GET", url: "compiler.url/project/#{@project_id}/wordcount?file=main.tex" }) .should.equal true describe "with image", -> @@ -303,6 +307,6 @@ describe "ClsiManager", -> @ClsiManager.wordCount @project_id, "main.tex", {}, @callback it "should call wordCount with file and image", -> - @request.get - .calledWith({ url: "compiler.url/project/#{@project_id}/wordcount?file=main.tex&image=#{encodeURIComponent(@image)}" }) + @ClsiManager._makeRequest + .calledWith(@project_id, { method: "GET", url: "compiler.url/project/#{@project_id}/wordcount?file=main.tex&image=#{encodeURIComponent(@image)}" }) .should.equal true diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiRequestManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiRequestManagerTests.coffee new file mode 100644 index 0000000000..a88cef61c6 --- /dev/null +++ b/services/web/test/UnitTests/coffee/Compile/ClsiRequestManagerTests.coffee @@ -0,0 +1,111 @@ +sinon = require('sinon') +chai = require('chai') +should = chai.should() +expect = chai.expect +modulePath = "../../../../app/js/Features/Compile/ClsiRequestManager.js" +SandboxedModule = require('sandboxed-module') +realRequst = require("request") +describe "ClsiRequestManager", -> + beforeEach -> + @redisMulti = + set:sinon.stub() + get:sinon.stub() + expire:sinon.stub() + exec:sinon.stub() + self = @ + @project_id = "123423431321" + @request = + get: sinon.stub() + cookie:realRequst.cookie + jar: realRequst.jar + @ClsiRequestManager = SandboxedModule.require modulePath, requires: + "redis-sharelatex" : + createClient: => + auth:-> + multi: -> return self.redisMulti + "settings-sharelatex": @settings = + redis: + web:"redis.something" + apis: + clsi: + url: "http://clsi.example.com" + "request": @request + + "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } + + + + describe "getServerId", -> + + it "should call get for the key", (done)-> + @redisMulti.exec.callsArgWith(0, null, ["clsi-7"]) + @ClsiRequestManager._getServerId @project_id, (err, serverId)=> + @redisMulti.get.calledWith("clsiserver:#{@project_id}").should.equal true + serverId.should.equal "clsi-7" + done() + + it "should expire the key", (done)-> + @redisMulti.exec.callsArgWith(0, null, ["clsi-7"]) + @ClsiRequestManager._getServerId @project_id, (err, serverId)=> + @redisMulti.expire.calledWith("clsiserver:#{@project_id}", 60 * 60 * 24 * 7).should.equal true + done() + + it "should _getServerIdViaRequest if no key is found", (done)-> + @ClsiRequestManager._getServerIdViaRequest = sinon.stub().callsArgWith(1) + @redisMulti.exec.callsArgWith(0, null, []) + @ClsiRequestManager._getServerId @project_id, (err, serverId)=> + @ClsiRequestManager._getServerIdViaRequest.calledWith(@project_id).should.equal true + done() + + + describe "_getServerIdViaRequest", -> + + it "should make a request to the clsi", (done)-> + response = "some data" + @request.get.callsArgWith(1, null, response) + @ClsiRequestManager.setServerId = sinon.stub().callsArgWith(2) + @ClsiRequestManager._getServerIdViaRequest @project_id, (err, serverId)=> + args = @ClsiRequestManager.setServerId.args[0] + args[0].should.equal @project_id + args[1].should.deep.equal response + done() + + describe "setServerId", -> + + it "should set the server id with a ttl", (done)-> + @ClsiRequestManager._parseServerIdFromResponse = sinon.stub().returns("clsi-8") + response = "dsadsakj" + @redisMulti.exec.callsArgWith(0) + @ClsiRequestManager.setServerId @project_id, response, (err)=> + @redisMulti.set.calledWith("clsiserver:#{@project_id}", "clsi-8").should.equal true + @redisMulti.expire.calledWith("clsiserver:#{@project_id}", 60 * 60 * 24 * 7).should.equal true + done() + + + describe "getCookieJar", -> + + it "should return a jar with the cookie set populated from redis", (done)-> + @ClsiRequestManager._getServerId = sinon.stub().callsArgWith(1, null, "clsi-11") + opts = {} + @ClsiRequestManager.getCookieJar @project_id, opts, (err, jar)-> + jar._jar.store.idx["clsi.example.com"]["/"].clsiserver.key.should.equal "clsiserver" + jar._jar.store.idx["clsi.example.com"]["/"].clsiserver.value.should.equal "clsi-11" + done() + + + # describe "_parseServerIdFromResponse", -> + # it "take the cookie from the response", (done)-> + + # a.should.equal + + + + + + + + + + + + diff --git a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee b/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee index 26cf0a2e2d..0827a6f206 100644 --- a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee @@ -22,6 +22,9 @@ describe "CompileController", -> url: "clsi.example.com" clsi_priority: url: "clsi-priority.example.com" + @jar = {cookie:"stuff"} + @ClsiRequestManager = + getCookieJar:sinon.stub().callsArgWith(1, null, @jar) @CompileController = SandboxedModule.require modulePath, requires: "settings-sharelatex": @settings "request": @request = sinon.stub() @@ -33,6 +36,7 @@ describe "CompileController", -> "./ClsiManager": @ClsiManager "../Authentication/AuthenticationController": @AuthenticationController = {} "../../infrastructure/RateLimiter":@RateLimiter + "./ClsiRequestManager":@ClsiRequestManager @project_id = "project-id" @user = features: @@ -182,6 +186,7 @@ describe "CompileController", -> it "should open a request to the CLSI", -> @request .calledWith( + jar:@jar method: @req.method url: "#{@settings.apis.clsi.url}#{@url}", timeout: 60 * 1000 @@ -204,6 +209,7 @@ describe "CompileController", -> it "should proxy to the priority url if the user has the feature", ()-> @request .calledWith( + jar:@jar method: @req.method url: "#{@settings.apis.clsi_priority.url}#{@url}", timeout: 60 * 1000 @@ -218,6 +224,7 @@ describe "CompileController", -> it "should open a request to the CLSI", -> @request .calledWith( + jar:@jar method: @req.method url: "#{@settings.apis.clsi.url}#{@url}", timeout: 60 * 1000 @@ -240,6 +247,7 @@ describe "CompileController", -> it "should proxy to the priority url if the user has the feature", ()-> @request .calledWith( + jar:@jar method: @req.method url: "#{@settings.apis.clsi_priority.url}#{@url}", timeout: 60 * 1000 @@ -254,6 +262,7 @@ describe "CompileController", -> it "should proxy to the standard url", ()-> @request .calledWith( + jar:@jar method: @req.method url: "#{@settings.apis.clsi.url}#{@url}", timeout: 60 * 1000 @@ -269,6 +278,7 @@ describe "CompileController", -> it "should proxy to the standard url without the build parameter", ()-> @request .calledWith( + jar:@jar method: @req.method url: "#{@settings.apis.clsi.url}#{@url}", timeout: 60 * 1000 @@ -286,6 +296,7 @@ describe "CompileController", -> it "should open a request to the CLSI", -> @request .calledWith( + jar:@jar method: @req.method url: "#{@settings.apis.clsi.url}#{@url}", timeout: 60 * 1000 @@ -313,6 +324,7 @@ describe "CompileController", -> it "should proxy to the priority url if the user has the feature", ()-> @request .calledWith( + jar:@jar method: @req.method url: "#{@settings.apis.clsi_priority.url}#{@url}", timeout: 60 * 1000 @@ -331,8 +343,8 @@ describe "CompileController", -> @CompileController.proxyToClsi(@project_id, @url = "/test", @req, @res, @next) it "should proxy to the standard url with the build parameter", ()-> - @request - .calledWith( + @request.calledWith( + jar:@jar method: @req.method qs: {build: 1234} url: "#{@settings.apis.clsi.url}#{@url}", From 4d54de8b9a55b7c14763b06b41662139a59fa908 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 20 Apr 2016 15:06:39 +0100 Subject: [PATCH 137/208] renamed ClsiRequestManager to ClsiCookieManager and improved logging --- ...anager.coffee => ClsiCookieManager.coffee} | 29 +++++++------ .../Features/Compile/ClsiManager.coffee | 24 ++++++----- .../Features/Compile/CompileController.coffee | 4 +- services/web/config/settings.defaults.coffee | 3 ++ ...s.coffee => ClsiCookieManagerTests.coffee} | 43 ++++++++----------- .../coffee/Compile/ClsiManagerTests.coffee | 4 +- .../Compile/CompileControllerTests.coffee | 4 +- 7 files changed, 56 insertions(+), 55 deletions(-) rename services/web/app/coffee/Features/Compile/{ClsiRequestManager.coffee => ClsiCookieManager.coffee} (59%) rename services/web/test/UnitTests/coffee/Compile/{ClsiRequestManagerTests.coffee => ClsiCookieManagerTests.coffee} (63%) diff --git a/services/web/app/coffee/Features/Compile/ClsiRequestManager.coffee b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee similarity index 59% rename from services/web/app/coffee/Features/Compile/ClsiRequestManager.coffee rename to services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee index 9de06c1538..e2b82e5d5d 100644 --- a/services/web/app/coffee/Features/Compile/ClsiRequestManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee @@ -2,7 +2,8 @@ Settings = require "settings-sharelatex" request = require('request') redis = require("redis-sharelatex") rclient = redis.createClient(Settings.redis.web) -cookie = require('cookie') +Cookie = require('cookie') +logger = require "logger-sharelatex" buildKey = (project_id)-> return "clsiserver:#{project_id}" @@ -10,7 +11,7 @@ buildKey = (project_id)-> ONE_WEEK_IN_SECONDS = 60 * 60 * 24 * 7 -module.exports = ClsiRequestManager = +module.exports = ClsiCookieManager = _getServerId : (project_id, callback = (err, serverId)->)-> multi = rclient.multi() @@ -23,38 +24,40 @@ module.exports = ClsiRequestManager = if serverId? return callback(null, serverId) else - return ClsiRequestManager._getServerIdViaRequest project_id, callback + return ClsiCookieManager._populateServerIdViaRequest project_id, callback - _getServerIdViaRequest :(project_id, callback = (err, serverId)->)-> + _populateServerIdViaRequest :(project_id, callback = (err, serverId)->)-> url = "#{Settings.apis.clsi.url}/project/#{project_id}/status" request.get url, (err, res, body)-> if err? logger.err err:err, project_id:project_id, "error getting initial server id for project" return callback(err) - ClsiRequestManager.setServerId project_id, res, callback + ClsiCookieManager.setServerId project_id, res, (err)-> + if err? + logger.err err:err, project_id:project_id, "error setting server id via populate request" + callback(err) _parseServerIdFromResponse : (response)-> - console.log response.headers - cookies = cookie.parse(response.headers["set-cookie"]?[0] or "") - return cookies?.clsiserver + cookies = Cookie.parse(response.headers["set-cookie"]?[0] or "") + return cookies?[Settings.clsiCookieKey] setServerId: (project_id, response, callback = ->)-> - serverId = ClsiRequestManager._parseServerIdFromResponse(response) + serverId = ClsiCookieManager._parseServerIdFromResponse(response) multi = rclient.multi() multi.set buildKey(project_id), serverId multi.expire buildKey(project_id), ONE_WEEK_IN_SECONDS multi.exec callback - getCookieJar: (project_id, opts, callback = (err, jar)->)-> - ClsiRequestManager._getServerId project_id, (err, serverId)=> + getCookieJar: (project_id, callback = (err, jar)->)-> + ClsiCookieManager._getServerId project_id, (err, serverId)=> if err? logger.err err:err, project_id:project_id, "error getting server id" return callback(err) - cookie = request.cookie("clsiserver=#{serverId}") + serverCookie = request.cookie("clsiserver=#{serverId}") jar = request.jar() - jar.setCookie cookie, Settings.apis.clsi.url + jar.setCookie serverCookie, Settings.apis.clsi.url callback(null, jar) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 6f4efabcf9..0513fd4817 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -6,25 +6,19 @@ Project = require("../../models/Project").Project ProjectEntityHandler = require("../Project/ProjectEntityHandler") logger = require "logger-sharelatex" url = require("url") -ClsiRequestManager = require("./ClsiRequestManager") +ClsiCookieManager = require("./ClsiCookieManager") module.exports = ClsiManager = - _makeRequest: (project_id, opts, callback)-> - ClsiRequestManager.getCookieJar project_id, (err, jar)-> - if err? - logger.err err:err, "error getting cookie jar for clsi request" - return callback(err) - opts.jar = jar - request opts, callback - sendRequest: (project_id, options = {}, callback = (error, success) ->) -> ClsiManager._buildRequest project_id, options, (error, req) -> return callback(error) if error? logger.log project_id: project_id, "sending compile to CLSI" ClsiManager._postToClsi project_id, req, options.compileGroup, (error, response) -> - return callback(error) if error? + if error? + logger.err err:error, project_id:project_id, "error sending request to clsi" + return callback(error) logger.log project_id: project_id, response: response, "received compile response from CLSI" callback( null @@ -39,6 +33,16 @@ module.exports = ClsiManager = method:"DELETE" ClsiManager._makeRequest project_id, opts, callback + + _makeRequest: (project_id, opts, callback)-> + ClsiCookieManager.getCookieJar project_id, (err, jar)-> + if err? + logger.err err:err, "error getting cookie jar for clsi request" + return callback(err) + opts.jar = jar + request opts, callback + + _getCompilerUrl: (compileGroup) -> if compileGroup == "priority" return Settings.apis.clsi_priority.url diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index 0e600dc3b0..934f9e97a4 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -8,7 +8,7 @@ Settings = require "settings-sharelatex" AuthenticationController = require "../Authentication/AuthenticationController" UserGetter = require "../User/UserGetter" RateLimiter = require("../../infrastructure/RateLimiter") -ClsiRequestManager = require("./ClsiRequestManager") +ClsiCookieManager = require("./ClsiCookieManager") module.exports = CompileController = @@ -109,7 +109,7 @@ module.exports = CompileController = CompileController.proxyToClsiWithLimits(project_id, url, limits, req, res, next) proxyToClsiWithLimits: (project_id, url, limits, req, res, next = (error) ->) -> - ClsiRequestManager.getCookieJar project_id, (err, jar)-> + ClsiCookieManager.getCookieJar project_id, (err, jar)-> if err? logger.err err:err, "error getting cookie jar for clsi request" return callback(err) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 2c16d41a52..5a1d207236 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -126,6 +126,9 @@ module.exports = # cookieDomain: ".sharelatex.dev" cookieName:"sharelatex.sid" + # this is only used if cookies are used for clsi backend + #clsiCookieKey: "clsiserver" + # Same, but with http auth credentials. httpAuthSiteUrl: 'http://#{httpAuthUser}:#{httpAuthPass}@localhost:3000' diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiRequestManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee similarity index 63% rename from services/web/test/UnitTests/coffee/Compile/ClsiRequestManagerTests.coffee rename to services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee index a88cef61c6..855a60b767 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiRequestManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee @@ -2,10 +2,10 @@ sinon = require('sinon') chai = require('chai') should = chai.should() expect = chai.expect -modulePath = "../../../../app/js/Features/Compile/ClsiRequestManager.js" +modulePath = "../../../../app/js/Features/Compile/ClsiCookieManager.js" SandboxedModule = require('sandboxed-module') realRequst = require("request") -describe "ClsiRequestManager", -> +describe "ClsiCookieManager", -> beforeEach -> @redisMulti = set:sinon.stub() @@ -18,7 +18,7 @@ describe "ClsiRequestManager", -> get: sinon.stub() cookie:realRequst.cookie jar: realRequst.jar - @ClsiRequestManager = SandboxedModule.require modulePath, requires: + @ClsiCookieManager = SandboxedModule.require modulePath, requires: "redis-sharelatex" : createClient: => auth:-> @@ -39,33 +39,33 @@ describe "ClsiRequestManager", -> it "should call get for the key", (done)-> @redisMulti.exec.callsArgWith(0, null, ["clsi-7"]) - @ClsiRequestManager._getServerId @project_id, (err, serverId)=> + @ClsiCookieManager._getServerId @project_id, (err, serverId)=> @redisMulti.get.calledWith("clsiserver:#{@project_id}").should.equal true serverId.should.equal "clsi-7" done() it "should expire the key", (done)-> @redisMulti.exec.callsArgWith(0, null, ["clsi-7"]) - @ClsiRequestManager._getServerId @project_id, (err, serverId)=> + @ClsiCookieManager._getServerId @project_id, (err, serverId)=> @redisMulti.expire.calledWith("clsiserver:#{@project_id}", 60 * 60 * 24 * 7).should.equal true done() - it "should _getServerIdViaRequest if no key is found", (done)-> - @ClsiRequestManager._getServerIdViaRequest = sinon.stub().callsArgWith(1) + it "should _populateServerIdViaRequest if no key is found", (done)-> + @ClsiCookieManager._populateServerIdViaRequest = sinon.stub().callsArgWith(1) @redisMulti.exec.callsArgWith(0, null, []) - @ClsiRequestManager._getServerId @project_id, (err, serverId)=> - @ClsiRequestManager._getServerIdViaRequest.calledWith(@project_id).should.equal true + @ClsiCookieManager._getServerId @project_id, (err, serverId)=> + @ClsiCookieManager._populateServerIdViaRequest.calledWith(@project_id).should.equal true done() - describe "_getServerIdViaRequest", -> + describe "_populateServerIdViaRequest", -> it "should make a request to the clsi", (done)-> response = "some data" @request.get.callsArgWith(1, null, response) - @ClsiRequestManager.setServerId = sinon.stub().callsArgWith(2) - @ClsiRequestManager._getServerIdViaRequest @project_id, (err, serverId)=> - args = @ClsiRequestManager.setServerId.args[0] + @ClsiCookieManager.setServerId = sinon.stub().callsArgWith(2) + @ClsiCookieManager._populateServerIdViaRequest @project_id, (err, serverId)=> + args = @ClsiCookieManager.setServerId.args[0] args[0].should.equal @project_id args[1].should.deep.equal response done() @@ -73,10 +73,10 @@ describe "ClsiRequestManager", -> describe "setServerId", -> it "should set the server id with a ttl", (done)-> - @ClsiRequestManager._parseServerIdFromResponse = sinon.stub().returns("clsi-8") + @ClsiCookieManager._parseServerIdFromResponse = sinon.stub().returns("clsi-8") response = "dsadsakj" @redisMulti.exec.callsArgWith(0) - @ClsiRequestManager.setServerId @project_id, response, (err)=> + @ClsiCookieManager.setServerId @project_id, response, (err)=> @redisMulti.set.calledWith("clsiserver:#{@project_id}", "clsi-8").should.equal true @redisMulti.expire.calledWith("clsiserver:#{@project_id}", 60 * 60 * 24 * 7).should.equal true done() @@ -85,23 +85,14 @@ describe "ClsiRequestManager", -> describe "getCookieJar", -> it "should return a jar with the cookie set populated from redis", (done)-> - @ClsiRequestManager._getServerId = sinon.stub().callsArgWith(1, null, "clsi-11") + @ClsiCookieManager._getServerId = sinon.stub().callsArgWith(1, null, "clsi-11") opts = {} - @ClsiRequestManager.getCookieJar @project_id, opts, (err, jar)-> + @ClsiCookieManager.getCookieJar @project_id, (err, jar)-> jar._jar.store.idx["clsi.example.com"]["/"].clsiserver.key.should.equal "clsiserver" jar._jar.store.idx["clsi.example.com"]["/"].clsiserver.value.should.equal "clsi-11" done() - # describe "_parseServerIdFromResponse", -> - # it "take the cookie from the response", (done)-> - - # a.should.equal - - - - - diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee index 00400542d8..8f30634e9d 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee @@ -8,7 +8,7 @@ SandboxedModule = require('sandboxed-module') describe "ClsiManager", -> beforeEach -> @jar = {cookie:"stuff"} - @ClsiRequestManager = + @ClsiCookieManager = getCookieJar: sinon.stub().callsArgWith(1, null, @jar) @ClsiManager = SandboxedModule.require modulePath, requires: "settings-sharelatex": @settings = @@ -22,7 +22,7 @@ describe "ClsiManager", -> url: "https://clsipremium.example.com" "../../models/Project": Project: @Project = {} "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} - "./ClsiRequestManager": @ClsiRequestManager + "./ClsiCookieManager": @ClsiCookieManager "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } "request": @request = sinon.stub() @project_id = "project-id" diff --git a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee b/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee index 0827a6f206..6bae5803c4 100644 --- a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee @@ -23,7 +23,7 @@ describe "CompileController", -> clsi_priority: url: "clsi-priority.example.com" @jar = {cookie:"stuff"} - @ClsiRequestManager = + @ClsiCookieManager = getCookieJar:sinon.stub().callsArgWith(1, null, @jar) @CompileController = SandboxedModule.require modulePath, requires: "settings-sharelatex": @settings @@ -36,7 +36,7 @@ describe "CompileController", -> "./ClsiManager": @ClsiManager "../Authentication/AuthenticationController": @AuthenticationController = {} "../../infrastructure/RateLimiter":@RateLimiter - "./ClsiRequestManager":@ClsiRequestManager + "./ClsiCookieManager":@ClsiCookieManager @project_id = "project-id" @user = features: From 18560d862162290479ea34f78c63341286697517 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 20 Apr 2016 16:17:06 +0100 Subject: [PATCH 138/208] set server cookie on every compile response and don't expire on get --- .../Features/Compile/ClsiCookieManager.coffee | 1 - .../Features/Compile/ClsiManager.coffee | 6 ++- .../Compile/ClsiCookieManagerTests.coffee | 6 --- .../coffee/Compile/ClsiManagerTests.coffee | 45 +++++++++++++++++++ 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee index e2b82e5d5d..04332e685b 100644 --- a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee @@ -16,7 +16,6 @@ module.exports = ClsiCookieManager = _getServerId : (project_id, callback = (err, serverId)->)-> multi = rclient.multi() multi.get buildKey(project_id) - multi.expire buildKey(project_id), ONE_WEEK_IN_SECONDS multi.exec (err, results)-> if err? return callback(err) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 0513fd4817..a0e03c2226 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -40,7 +40,11 @@ module.exports = ClsiManager = logger.err err:err, "error getting cookie jar for clsi request" return callback(err) opts.jar = jar - request opts, callback + request opts, (err, response, body)-> + if err? + logger.err err:err, project_id:project_id, url:opts?.url, "error making request to clsi" + return callback(err) + ClsiCookieManager.setServerId project_id, response, callback _getCompilerUrl: (compileGroup) -> diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee index 855a60b767..587e906db9 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee @@ -44,12 +44,6 @@ describe "ClsiCookieManager", -> serverId.should.equal "clsi-7" done() - it "should expire the key", (done)-> - @redisMulti.exec.callsArgWith(0, null, ["clsi-7"]) - @ClsiCookieManager._getServerId @project_id, (err, serverId)=> - @redisMulti.expire.calledWith("clsiserver:#{@project_id}", 60 * 60 * 24 * 7).should.equal true - done() - it "should _populateServerIdViaRequest if no key is found", (done)-> @ClsiCookieManager._populateServerIdViaRequest = sinon.stub().callsArgWith(1) @redisMulti.exec.callsArgWith(0, null, []) diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee index 8f30634e9d..ba4f717e5f 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee @@ -10,6 +10,7 @@ describe "ClsiManager", -> @jar = {cookie:"stuff"} @ClsiCookieManager = getCookieJar: sinon.stub().callsArgWith(1, null, @jar) + setServerId: sinon.stub().callsArgWith(2) @ClsiManager = SandboxedModule.require modulePath, requires: "settings-sharelatex": @settings = apis: @@ -310,3 +311,47 @@ describe "ClsiManager", -> @ClsiManager._makeRequest .calledWith(@project_id, { method: "GET", url: "compiler.url/project/#{@project_id}/wordcount?file=main.tex&image=#{encodeURIComponent(@image)}" }) .should.equal true + + + + describe "_makeRequest", -> + + beforeEach -> + @response = {there:"something"} + @request.callsArgWith(1, null, @response) + @opts = + method: "SOMETHIGN" + url: "http://a place on the web" + + it "should process a request with a cookie jar", (done)-> + @ClsiManager._makeRequest @project_id, @opts, => + args = @request.args[0] + args[0].method.should.equal @opts.method + args[0].url.should.equal @opts.url + args[0].jar.should.equal @jar + done() + + it "should set the cookie again on response as it might have changed", (done)-> + @ClsiManager._makeRequest @project_id, @opts, => + @ClsiCookieManager.setServerId.calledWith(@project_id, @response).should.equal true + done() + + + + + + + + + + + + + + + + + + + + From 616630200a35604f2104408126d2f935e14ce2a8 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 20 Apr 2016 17:00:17 +0100 Subject: [PATCH 139/208] improve send calls and return correct stuff from _makeRequest --- services/web/app/coffee/Features/Compile/ClsiManager.coffee | 6 +++++- .../app/coffee/Features/Compile/CompileController.coffee | 2 +- .../web/test/UnitTests/coffee/helpers/MockResponse.coffee | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index a0e03c2226..473df0f5cd 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -44,7 +44,11 @@ module.exports = ClsiManager = if err? logger.err err:err, project_id:project_id, url:opts?.url, "error making request to clsi" return callback(err) - ClsiCookieManager.setServerId project_id, response, callback + ClsiCookieManager.setServerId project_id, response, (err)-> + if err? + logger.warn err:err, project_id:project_id, "error setting server id" + + return callback err, response, body _getCompilerUrl: (compileGroup) -> diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index 934f9e97a4..6ffca6affe 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -33,7 +33,7 @@ module.exports = CompileController = CompileManager.compile project_id, user_id, options, (error, status, outputFiles, output, limits) -> return next(error) if error? res.contentType("application/json") - res.send 200, JSON.stringify { + res.status(200).send JSON.stringify { status: status outputFiles: outputFiles compileGroup: limits?.compileGroup diff --git a/services/web/test/UnitTests/coffee/helpers/MockResponse.coffee b/services/web/test/UnitTests/coffee/helpers/MockResponse.coffee index a7182c9df0..5ed7c58522 100644 --- a/services/web/test/UnitTests/coffee/helpers/MockResponse.coffee +++ b/services/web/test/UnitTests/coffee/helpers/MockResponse.coffee @@ -63,6 +63,10 @@ class MockResponse @body = body if body @callback() if @callback? + status: (@statusCode)-> + return @ + + setHeader: (header, value) -> @headers[header] = value From 07ad6eb70479337401899b376fb7a656d150f8d8 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 25 Apr 2016 12:42:03 +0100 Subject: [PATCH 140/208] actually use the rendering-error state. --- services/web/app/views/project/editor/pdf.jade | 6 +++++- .../public/coffee/ide/pdf/controllers/PdfController.coffee | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index c596fd915f..f5c64c2d6b 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -156,9 +156,13 @@ div.full-size.pdf(ng-controller="PdfController") .pdf-errors(ng-switch-when="errors") - .alert.alert-danger(ng-show="pdf.error || pdf.renderingError") + .alert.alert-danger(ng-show="pdf.error") strong #{translate("server_error")} span #{translate("somthing_went_wrong_compiling")} + + .alert.alert-danger(ng-show="pdf.renderingError") + strong #{translate("pdf_rendering_error")} + span #{translate("something_went_wrong_rendering_pdf")} .alert.alert-danger(ng-show="pdf.clsiMaintenance") strong #{translate("server_error")} diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 5847b58e3e..64553085f5 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -46,6 +46,7 @@ define [ $scope.pdf.url = null $scope.pdf.clsiMaintenance = false $scope.pdf.tooRecentlyCompiled = false + $scope.pdf.renderingError = false if response.status == "timedout" $scope.pdf.view = 'errors' @@ -179,6 +180,7 @@ define [ parseCompileResponse(data) .error () -> $scope.pdf.compiling = false + $scope.pdf.renderingError = false $scope.pdf.error = true $scope.pdf.view = 'errors' From 1ee94f9bf5095738638e0128653703c7ae0f649c Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 27 Apr 2016 16:20:10 +0100 Subject: [PATCH 141/208] return server id for set server id and _populateServerIdViaRequest --- .../Features/Compile/ClsiCookieManager.coffee | 9 +++--- .../Compile/ClsiCookieManagerTests.coffee | 28 ++++++++++++++----- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee index 04332e685b..ff42eb8820 100644 --- a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee @@ -32,21 +32,22 @@ module.exports = ClsiCookieManager = if err? logger.err err:err, project_id:project_id, "error getting initial server id for project" return callback(err) - ClsiCookieManager.setServerId project_id, res, (err)-> + ClsiCookieManager.setServerId project_id, res, (err, serverId)-> if err? logger.err err:err, project_id:project_id, "error setting server id via populate request" - callback(err) + callback(err, serverId) _parseServerIdFromResponse : (response)-> cookies = Cookie.parse(response.headers["set-cookie"]?[0] or "") return cookies?[Settings.clsiCookieKey] - setServerId: (project_id, response, callback = ->)-> + setServerId: (project_id, response, callback = (err, serverId)->)-> serverId = ClsiCookieManager._parseServerIdFromResponse(response) multi = rclient.multi() multi.set buildKey(project_id), serverId multi.expire buildKey(project_id), ONE_WEEK_IN_SECONDS - multi.exec callback + multi.exec (err)-> + callback(err, serverId) getCookieJar: (project_id, callback = (err, jar)->)-> diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee index 587e906db9..e4013ff06e 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee @@ -5,6 +5,7 @@ expect = chai.expect modulePath = "../../../../app/js/Features/Compile/ClsiCookieManager.js" SandboxedModule = require('sandboxed-module') realRequst = require("request") + describe "ClsiCookieManager", -> beforeEach -> @redisMulti = @@ -54,27 +55,40 @@ describe "ClsiCookieManager", -> describe "_populateServerIdViaRequest", -> + beforeEach -> + @response = "some data" + @request.get.callsArgWith(1, null, @response) + @ClsiCookieManager.setServerId = sinon.stub().callsArgWith(2, null, "clsi-9") + it "should make a request to the clsi", (done)-> - response = "some data" - @request.get.callsArgWith(1, null, response) - @ClsiCookieManager.setServerId = sinon.stub().callsArgWith(2) @ClsiCookieManager._populateServerIdViaRequest @project_id, (err, serverId)=> args = @ClsiCookieManager.setServerId.args[0] args[0].should.equal @project_id - args[1].should.deep.equal response + args[1].should.deep.equal @response + done() + + it "should return the server id", (done)-> + @ClsiCookieManager._populateServerIdViaRequest @project_id, (err, serverId)=> + serverId.should.equal "clsi-9" done() describe "setServerId", -> - it "should set the server id with a ttl", (done)-> + beforeEach -> + @response = "dsadsakj" @ClsiCookieManager._parseServerIdFromResponse = sinon.stub().returns("clsi-8") - response = "dsadsakj" @redisMulti.exec.callsArgWith(0) - @ClsiCookieManager.setServerId @project_id, response, (err)=> + + it "should set the server id with a ttl", (done)-> + @ClsiCookieManager.setServerId @project_id, @response, (err)=> @redisMulti.set.calledWith("clsiserver:#{@project_id}", "clsi-8").should.equal true @redisMulti.expire.calledWith("clsiserver:#{@project_id}", 60 * 60 * 24 * 7).should.equal true done() + it "should return the server id", (done)-> + @ClsiCookieManager.setServerId @project_id, @response, (err, serverId)=> + serverId.should.equal "clsi-8" + done() describe "getCookieJar", -> From 78b08060abf457c359eaf96c35005bf1bf091ecd Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 27 Apr 2016 16:56:21 +0100 Subject: [PATCH 142/208] redis get not multi used for _getServerId --- .../Features/Compile/ClsiCookieManager.coffee | 9 +++++---- .../coffee/Compile/ClsiCookieManagerTests.coffee | 16 ++++++++++------ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee index ff42eb8820..3c2fe8a879 100644 --- a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee @@ -9,17 +9,16 @@ buildKey = (project_id)-> return "clsiserver:#{project_id}" +clsiCookiesEnabled = Settings.clsiCookieKey? and Settings.clsiCookieKey.length != 0 + ONE_WEEK_IN_SECONDS = 60 * 60 * 24 * 7 module.exports = ClsiCookieManager = _getServerId : (project_id, callback = (err, serverId)->)-> - multi = rclient.multi() - multi.get buildKey(project_id) - multi.exec (err, results)-> + rclient.get buildKey(project_id), (err, serverId)-> if err? return callback(err) - serverId = results[0] if serverId? return callback(null, serverId) else @@ -51,6 +50,8 @@ module.exports = ClsiCookieManager = getCookieJar: (project_id, callback = (err, jar)->)-> + # if !clsiCookiesEnabled + # return callback(null, request.jar()) ClsiCookieManager._getServerId project_id, (err, serverId)=> if err? logger.err err:err, project_id:project_id, "error getting server id" diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee index e4013ff06e..576024e10e 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee @@ -8,12 +8,16 @@ realRequst = require("request") describe "ClsiCookieManager", -> beforeEach -> + self = @ @redisMulti = set:sinon.stub() get:sinon.stub() expire:sinon.stub() exec:sinon.stub() - self = @ + @redis = + auth:-> + get:sinon.stub() + multi: -> return self.redisMulti @project_id = "123423431321" @request = get: sinon.stub() @@ -22,14 +26,14 @@ describe "ClsiCookieManager", -> @ClsiCookieManager = SandboxedModule.require modulePath, requires: "redis-sharelatex" : createClient: => - auth:-> - multi: -> return self.redisMulti + @redis "settings-sharelatex": @settings = redis: web:"redis.something" apis: clsi: url: "http://clsi.example.com" + clsiCookieKey: "coooookie" "request": @request "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } @@ -39,15 +43,15 @@ describe "ClsiCookieManager", -> describe "getServerId", -> it "should call get for the key", (done)-> - @redisMulti.exec.callsArgWith(0, null, ["clsi-7"]) + @redis.get.callsArgWith(1, null, "clsi-7") @ClsiCookieManager._getServerId @project_id, (err, serverId)=> - @redisMulti.get.calledWith("clsiserver:#{@project_id}").should.equal true + @redis.get.calledWith("clsiserver:#{@project_id}").should.equal true serverId.should.equal "clsi-7" done() it "should _populateServerIdViaRequest if no key is found", (done)-> @ClsiCookieManager._populateServerIdViaRequest = sinon.stub().callsArgWith(1) - @redisMulti.exec.callsArgWith(0, null, []) + @redis.get.callsArgWith(1, null) @ClsiCookieManager._getServerId @project_id, (err, serverId)=> @ClsiCookieManager._populateServerIdViaRequest.calledWith(@project_id).should.equal true done() From b00bd5cd946b90c157d4e02b916798390ff879b1 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 27 Apr 2016 17:05:12 +0100 Subject: [PATCH 143/208] if clsi cookies are not enabled don't call redis, return empty --- .../Features/Compile/ClsiCookieManager.coffee | 7 ++-- .../Compile/ClsiCookieManagerTests.coffee | 42 +++++++++++++------ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee index 3c2fe8a879..ad8c908cee 100644 --- a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee @@ -8,7 +8,6 @@ logger = require "logger-sharelatex" buildKey = (project_id)-> return "clsiserver:#{project_id}" - clsiCookiesEnabled = Settings.clsiCookieKey? and Settings.clsiCookieKey.length != 0 ONE_WEEK_IN_SECONDS = 60 * 60 * 24 * 7 @@ -41,6 +40,8 @@ module.exports = ClsiCookieManager = return cookies?[Settings.clsiCookieKey] setServerId: (project_id, response, callback = (err, serverId)->)-> + if !clsiCookiesEnabled + return callback() serverId = ClsiCookieManager._parseServerIdFromResponse(response) multi = rclient.multi() multi.set buildKey(project_id), serverId @@ -50,8 +51,8 @@ module.exports = ClsiCookieManager = getCookieJar: (project_id, callback = (err, jar)->)-> - # if !clsiCookiesEnabled - # return callback(null, request.jar()) + if !clsiCookiesEnabled + return callback(null, request.jar()) ClsiCookieManager._getServerId project_id, (err, serverId)=> if err? logger.err err:err, project_id:project_id, "error getting server id" diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee index 576024e10e..28dad51d8b 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee @@ -1,5 +1,6 @@ sinon = require('sinon') chai = require('chai') +assert = chai.assert should = chai.should() expect = chai.expect modulePath = "../../../../app/js/Features/Compile/ClsiCookieManager.js" @@ -23,20 +24,22 @@ describe "ClsiCookieManager", -> get: sinon.stub() cookie:realRequst.cookie jar: realRequst.jar - @ClsiCookieManager = SandboxedModule.require modulePath, requires: + @settings = + redis: + web:"redis.something" + apis: + clsi: + url: "http://clsi.example.com" + clsiCookieKey: "coooookie" + @requires = "redis-sharelatex" : createClient: => @redis - "settings-sharelatex": @settings = - redis: - web:"redis.something" - apis: - clsi: - url: "http://clsi.example.com" - clsiCookieKey: "coooookie" + "settings-sharelatex": @settings "request": @request "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } + @ClsiCookieManager = SandboxedModule.require modulePath, requires:@requires @@ -94,21 +97,36 @@ describe "ClsiCookieManager", -> serverId.should.equal "clsi-8" done() + + it "should not set the server id if clsiCookies are not enabled", (done)-> + delete @settings.clsiCookieKey + @ClsiCookieManager = SandboxedModule.require modulePath, requires:@requires + @ClsiCookieManager.setServerId @project_id, @response, (err, serverId)=> + @redisMulti.exec.called.should.equal false + done() + describe "getCookieJar", -> - it "should return a jar with the cookie set populated from redis", (done)-> + beforeEach -> @ClsiCookieManager._getServerId = sinon.stub().callsArgWith(1, null, "clsi-11") - opts = {} + + it "should return a jar with the cookie set populated from redis", (done)-> @ClsiCookieManager.getCookieJar @project_id, (err, jar)-> jar._jar.store.idx["clsi.example.com"]["/"].clsiserver.key.should.equal "clsiserver" jar._jar.store.idx["clsi.example.com"]["/"].clsiserver.value.should.equal "clsi-11" done() + it "should return empty cookie jar if clsiCookies are not enabled", (done)-> + delete @settings.clsiCookieKey + @ClsiCookieManager = SandboxedModule.require modulePath, requires:@requires + @ClsiCookieManager.getCookieJar @project_id, (err, jar)-> + assert.deepEqual jar, realRequst.jar() + done() + + - - From 74205ce11b30ac224ef73759fccddfbf1087eee6 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 28 Apr 2016 16:01:47 +0100 Subject: [PATCH 144/208] Remove `features.mendeley` and `features.zotero` from User model --- services/web/app/coffee/models/User.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index 4ceb87871f..b0a7628f17 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -36,8 +36,6 @@ UserSchema = new Schema compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup } templates: { type:Boolean, default: Settings.defaultFeatures.templates } references: { type:Boolean, default: Settings.defaultFeatures.references } - mendeley: { type:Boolean, default: Settings.defaultFeatures.mendeley } - zotero: { type:Boolean, default: Settings.defaultFeatures.zotero } } featureSwitches : { pdfng: { type: Boolean } From 9db647491a8159fed6ccbb132637f6b62755ecd9 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 28 Apr 2016 17:21:07 +0100 Subject: [PATCH 145/208] added different ldap config example --- 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 ee77b43cb6..17482ab7f7 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -388,7 +388,7 @@ module.exports = # filter: "(uid=:userKey)" # failMessage: 'LDAP User Fail' # fieldName: 'LDAP User' - # placeholder: 'LDAP User ID' + # placeholder: 'email@example.com' # emailAtt: 'mail' # anonymous: false # adminDN: 'cn=read-only-admin,dc=example,dc=com' From b237a022c3c61a15eed1e8d23afbb67d925ca48a Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 28 Apr 2016 17:24:00 +0100 Subject: [PATCH 146/208] added rate limiting on wiki endpoint --- services/web/app/coffee/router.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 5e16073ed3..854e852493 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -176,7 +176,12 @@ module.exports = class Router webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage - webRouter.get /learn(\/.*)?/, WikiController.getPage + webRouter.get /learn(\/.*)?/, RateLimiterMiddlewear.rateLimit({ + endpointName: "wiki" + params: [] + maxRequests: 60 + timeInterval: 60 + }), WikiController.getPage webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll From aa301dacb974f426483b16394240be08fb120bd8 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 4 May 2016 09:32:13 +0100 Subject: [PATCH 147/208] Return callback with error if an error is emitted on filestore stream. --- .../web/app/coffee/Features/FileStore/FileStoreHandler.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee index eaf9c87554..1b927113dc 100644 --- a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee +++ b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee @@ -45,6 +45,7 @@ module.exports = FileStoreHandler = readStream = request(opts) readStream.on "error", (err) -> logger.err {err, project_id, file_id, query}, "error in file stream" + return callback(err) callback(null, readStream) deleteFile: (project_id, file_id, callback)-> From 9738703424786f5105e5539a198e36af16ed18e0 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 4 May 2016 09:32:59 +0100 Subject: [PATCH 148/208] If image preview fails to load, just show the "no preview" message --- services/web/app/views/project/editor/binary-file.jade | 8 +++++++- .../binary-files/controllers/BinaryFileController.coffee | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/binary-file.jade b/services/web/app/views/project/editor/binary-file.jade index ca25a6d806..6e81c7a107 100644 --- a/services/web/app/views/project/editor/binary-file.jade +++ b/services/web/app/views/project/editor/binary-file.jade @@ -4,15 +4,21 @@ div.binary-file.full-size( ng-if="openFile" ) img( + ng-show="!failedLoad" ng-src="/project/{{ project_id }}/file/{{ openFile.id }}" ng-if="['png', 'jpg', 'jpeg', 'gif'].indexOf(extension(openFile)) > -1" + onerror="sl_binaryFilePreviewError()" + onabort="sl_binaryFilePreviewError()" ) img( + ng-show="!failedLoad" ng-src="/project/{{ project_id }}/file/{{ openFile.id }}?format=png" ng-if="['pdf', 'eps'].indexOf(extension(openFile)) > -1" + onerror="sl_binaryFilePreviewError()" + onabort="sl_binaryFilePreviewError()" ) p.no-preview( - ng-if="['png', 'jpg', 'jpeg', 'gif', 'pdf', 'eps'].indexOf(extension(openFile)) == -1" + ng-if="failedLoad || ['png', 'jpg', 'jpeg', 'gif', 'pdf', 'eps'].indexOf(extension(openFile)) == -1" ) #{translate("no_preview_available")} a.btn.btn-info( ng-href="/project/{{ project_id }}/file/{{ openFile.id }}" diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index 5dcabe8000..29da05259c 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -2,6 +2,13 @@ define [ "base" ], (App) -> App.controller "BinaryFileController", ["$scope", ($scope) -> + + $scope.failedLoad = false + + window.sl_binaryFilePreviewError = () => + $scope.failedLoad = true + $scope.$apply() + $scope.extension = (file) -> return file.name.split(".").pop()?.toLowerCase() - ] \ No newline at end of file + ] From a1552aa872fc2af40375466c93d71dc80756a00a Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 4 May 2016 10:08:48 +0100 Subject: [PATCH 149/208] reset the 'failedLoad' state when a new entity is selected --- .../ide/binary-files/controllers/BinaryFileController.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index 29da05259c..1ad8ef91e3 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -1,9 +1,11 @@ define [ "base" ], (App) -> - App.controller "BinaryFileController", ["$scope", ($scope) -> + App.controller "BinaryFileController", ["$scope", "$rootScope", ($scope, $rootScope) -> $scope.failedLoad = false + $rootScope.$on 'entity:selected', () -> + $scope.failedLoad = false window.sl_binaryFilePreviewError = () => $scope.failedLoad = true From 48b7800c7f898b10cefd3e4285c90c2be4f66ba4 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 4 May 2016 10:43:40 +0100 Subject: [PATCH 150/208] Revert, because mistake. --- .../web/app/coffee/Features/FileStore/FileStoreHandler.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee index 1b927113dc..eaf9c87554 100644 --- a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee +++ b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee @@ -45,7 +45,6 @@ module.exports = FileStoreHandler = readStream = request(opts) readStream.on "error", (err) -> logger.err {err, project_id, file_id, query}, "error in file stream" - return callback(err) callback(null, readStream) deleteFile: (project_id, file_id, callback)-> From 618935b6436a542e92609f4badf3aa3748dc0405 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 4 May 2016 22:00:43 +0100 Subject: [PATCH 151/208] hide hidden domains in hreflang head of html --- services/web/app/views/layout.jade | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index 4346629ade..ae0e7550be 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -23,7 +23,8 @@ html(itemscope, itemtype='http://schema.org/Product') if settings.i18n.subdomainLang each subdomainDetails in settings.i18n.subdomainLang - link(rel="alternate", href=subdomainDetails.url+currentUrl, hreflang=subdomainDetails.lngCode) + if !subdomainDetails.hide + link(rel="alternate", href=subdomainDetails.url+currentUrl, hreflang=subdomainDetails.lngCode) meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor") From ed4fdd48d771a746148963e3d069451d0db168e6 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 5 May 2016 16:50:18 +0100 Subject: [PATCH 152/208] clsi cookies are given an expire time via settings file --- .../web/app/coffee/Features/Compile/ClsiCookieManager.coffee | 3 +-- .../UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee index ad8c908cee..9827e328b4 100644 --- a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee @@ -10,7 +10,6 @@ buildKey = (project_id)-> clsiCookiesEnabled = Settings.clsiCookieKey? and Settings.clsiCookieKey.length != 0 -ONE_WEEK_IN_SECONDS = 60 * 60 * 24 * 7 module.exports = ClsiCookieManager = @@ -45,7 +44,7 @@ module.exports = ClsiCookieManager = serverId = ClsiCookieManager._parseServerIdFromResponse(response) multi = rclient.multi() multi.set buildKey(project_id), serverId - multi.expire buildKey(project_id), ONE_WEEK_IN_SECONDS + multi.expire buildKey(project_id), Settings.clsi_cookie_expire_length multi.exec (err)-> callback(err, serverId) diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee index 28dad51d8b..f40078a1d2 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee @@ -30,6 +30,7 @@ describe "ClsiCookieManager", -> apis: clsi: url: "http://clsi.example.com" + clsi_cookie_expire_length: Math.random() clsiCookieKey: "coooookie" @requires = "redis-sharelatex" : @@ -89,7 +90,7 @@ describe "ClsiCookieManager", -> it "should set the server id with a ttl", (done)-> @ClsiCookieManager.setServerId @project_id, @response, (err)=> @redisMulti.set.calledWith("clsiserver:#{@project_id}", "clsi-8").should.equal true - @redisMulti.expire.calledWith("clsiserver:#{@project_id}", 60 * 60 * 24 * 7).should.equal true + @redisMulti.expire.calledWith("clsiserver:#{@project_id}", @settings.clsi_cookie_expire_length).should.equal true done() it "should return the server id", (done)-> From b8510301b603c9ddcc889801a52acaff34da2ee2 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 6 May 2016 12:19:22 +0100 Subject: [PATCH 153/208] expire cookie key from setting file length --- .../web/app/coffee/Features/Compile/ClsiCookieManager.coffee | 2 +- .../UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee index 9827e328b4..81e2e633e6 100644 --- a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee @@ -44,7 +44,7 @@ module.exports = ClsiCookieManager = serverId = ClsiCookieManager._parseServerIdFromResponse(response) multi = rclient.multi() multi.set buildKey(project_id), serverId - multi.expire buildKey(project_id), Settings.clsi_cookie_expire_length + multi.expire buildKey(project_id), Settings.clsi_cookie_expire_length_seconds multi.exec (err)-> callback(err, serverId) diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee index f40078a1d2..1fc8716657 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee @@ -30,7 +30,7 @@ describe "ClsiCookieManager", -> apis: clsi: url: "http://clsi.example.com" - clsi_cookie_expire_length: Math.random() + clsi_cookie_expire_length_seconds: Math.random() clsiCookieKey: "coooookie" @requires = "redis-sharelatex" : @@ -90,7 +90,7 @@ describe "ClsiCookieManager", -> it "should set the server id with a ttl", (done)-> @ClsiCookieManager.setServerId @project_id, @response, (err)=> @redisMulti.set.calledWith("clsiserver:#{@project_id}", "clsi-8").should.equal true - @redisMulti.expire.calledWith("clsiserver:#{@project_id}", @settings.clsi_cookie_expire_length).should.equal true + @redisMulti.expire.calledWith("clsiserver:#{@project_id}", @settings.clsi_cookie_expire_length_seconds).should.equal true done() it "should return the server id", (done)-> From aafdc794cdb489fb27e89d65ca440e59b9ec93ae Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 10 May 2016 10:44:51 +0100 Subject: [PATCH 154/208] Remove text limit in content box of contact us --- services/web/app/views/contact-us-modal.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/contact-us-modal.jade b/services/web/app/views/contact-us-modal.jade index e78670d911..46090eec1b 100644 --- a/services/web/app/views/contact-us-modal.jade +++ b/services/web/app/views/contact-us-modal.jade @@ -23,7 +23,7 @@ script(type='text/ng-template', id='supportModalTemplate') label.desc | #{translate("suggestion")} .form-group - textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', maxlength='255', tabindex='4', onkeyup='') + textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='') .form-group.text-center input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value='#{translate("contact_us")}') span(ng-show="sent") From ab0bcd4f579a78a4db241e440205ae54cb0829aa Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 10 May 2016 15:53:37 +0100 Subject: [PATCH 155/208] increase default pdfjs chunk size from 64K to 128K --- .../web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee index bd7883c156..5639d670f0 100644 --- a/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee +++ b/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee @@ -16,7 +16,7 @@ define [ # PDFJS.disableStream # PDFJS.disableRange @scale = @options.scale || 1 - @pdfjs = PDFJS.getDocument @url + @pdfjs = PDFJS.getDocument {url: @url, rangeChunkSize: 2*65536} @pdfjs.onProgress = @options.progressCallback @document = $q.when(@pdfjs) @navigateFn = @options.navigateFn From 41a6fa148007489f57dc003b045c4092ca26e1cd Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 12 May 2016 11:02:24 +0100 Subject: [PATCH 156/208] Fix uncompiled pdf view --- .../public/coffee/ide/pdf/controllers/PdfController.coffee | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 64553085f5..8a244495c6 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -41,7 +41,6 @@ define [ $scope.pdf.error = false $scope.pdf.timedout = false $scope.pdf.failure = false - $scope.pdf.uncompiled = false $scope.pdf.projectTooLarge = false $scope.pdf.url = null $scope.pdf.clsiMaintenance = false @@ -52,8 +51,7 @@ define [ $scope.pdf.view = 'errors' $scope.pdf.timedout = true else if response.status == "autocompile-backoff" - $scope.pdf.view = 'errors' - $scope.pdf.uncompiled = true + $scope.pdf.view = 'uncompiled' else if response.status == "project-too-large" $scope.pdf.view = 'errors' $scope.pdf.projectTooLarge = true From 9eb2e933cd9a27edda4c851839857c890de2769a Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 12 May 2016 12:03:08 +0100 Subject: [PATCH 157/208] enable autocomplete on citations like `citeauthor` --- .../aceEditor/auto-complete/AutoCompleteManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index d256960165..db6e3f4e8a 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -47,7 +47,7 @@ define [ lineUpToCursor = editor.getSession().getTextRange(upToCursorRange) commandFragment = getLastCommandFragment(lineUpToCursor) if commandFragment - citeMatch = commandFragment.match(/^~?\\([a-z]*cite[a-z]?(?:\[.*])?){([^}]*,)?(\w*)/) + citeMatch = commandFragment.match(/^~?\\([a-z]*cite[a-z]*(?:\[.*])?){([^}]*,)?(\w*)/) if citeMatch beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999) lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange) From ae85dc3f5d69f2376dd0cde5b732fcb06958d939 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 12 May 2016 15:13:00 +0100 Subject: [PATCH 158/208] Enable spaces after commas in references autocomplete --- .../aceEditor/auto-complete/AutoCompleteManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index db6e3f4e8a..75e00a37f6 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -47,7 +47,7 @@ define [ lineUpToCursor = editor.getSession().getTextRange(upToCursorRange) commandFragment = getLastCommandFragment(lineUpToCursor) if commandFragment - citeMatch = commandFragment.match(/^~?\\([a-z]*cite[a-z]*(?:\[.*])?){([^}]*,)?(\w*)/) + citeMatch = commandFragment.match(/^~?\\([a-z]*cite[a-z]*(?:\[.*])?){([^}]*, *)?(\w*)/) if citeMatch beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999) lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange) From ee15bbeee07a53e867858ed0be815fc0b8239759 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 13 May 2016 10:33:38 +0100 Subject: [PATCH 159/208] support the direct path to a clsi output file /project/project_id/build/build_id/output/* this avoids use of the query string ?build=... and so we can match the url directly with the nginx location directive --- .../coffee/Features/Compile/CompileController.coffee | 7 ++++++- services/web/app/coffee/router.coffee | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index 89fbc87e2e..a273d38a35 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -93,7 +93,12 @@ module.exports = CompileController = getFileFromClsi: (req, res, next = (error) ->) -> project_id = req.params.Project_id - CompileController.proxyToClsi(project_id, "/project/#{project_id}/output/#{req.params.file}", req, res, next) + build = req.params.build + if build? + url = "/project/#{project_id}/build/#{build}/output/#{req.params.file}" + else + url = "/project/#{project_id}/output/#{req.params.file}" + CompileController.proxyToClsi(project_id, url, req, res, next) proxySync: (req, res, next = (error) ->) -> CompileController.proxyToClsi(req.params.Project_id, req.url, req, res, next) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 854e852493..709b2541e1 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -113,6 +113,17 @@ module.exports = class Router req.params = params next() ), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi + # direct url access to output files for a specific build (query string not required) + webRouter.get /^\/project\/([^\/]*)\/build\/([0-9a-f-]+)\/output\/(.*)$/, + ((req, res, next) -> + params = + "Project_id": req.params[0] + "build": req.params[1] + "file": req.params[2] + req.params = params + next() + ), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi + webRouter.delete "/project/:Project_id/output", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.deleteAuxFiles webRouter.get "/project/:Project_id/sync/code", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySync webRouter.get "/project/:Project_id/sync/pdf", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySync From ac7aa5f936988683b0d41559681a87238f6b3190 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 13 May 2016 11:45:09 +0100 Subject: [PATCH 160/208] request output files using /project/project_id/build/build_id/output/* route avoids using query string for build_id --- .../ide/pdf/controllers/PdfController.coffee | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 64553085f5..6d6fecfa49 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -71,20 +71,28 @@ define [ else if response.status == "success" $scope.pdf.view = 'pdf' $scope.shouldShowLogs = false - # define the base url - $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf?cache_bust=#{Date.now()}" - # add a query string parameter for the compile group - if response.compileGroup? - $scope.pdf.compileGroup = response.compileGroup - $scope.pdf.url = $scope.pdf.url + "&compileGroup=#{$scope.pdf.compileGroup}" # make a cache to look up files by name fileByPath = {} for file in response.outputFiles fileByPath[file.path] = file - # if the pdf file has a build number, pass it to the clsi + # prepare query string + qs = {} + # define the base url. if the pdf file has a build number, pass it to the clsi in the url if fileByPath['output.pdf']?.build? build = fileByPath['output.pdf'].build - $scope.pdf.url = $scope.pdf.url + "&build=#{build}" + $scope.pdf.url = "/project/#{$scope.project_id}/build/#{build}/output/output.pdf" + # no need to bust cache, build id is unique + else + $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf" + qs = { cache_bust : "#{Date.now()}" } + # add a query string parameter for the compile group + if response.compileGroup? + $scope.pdf.compileGroup = response.compileGroup + qs.compileGroup = "#{$scope.pdf.compileGroup}" + # convert the qs hash into a query string and append it + qs_args = ("#{k}=#{v}" for k, v of qs) + $scope.pdf.qs = if qs_args.length then "?" + qs_args.join("&") else "" + $scope.pdf.url += $scope.pdf.qs fetchLogs(fileByPath['output.log']) @@ -103,8 +111,11 @@ define [ $scope.pdf.outputFiles.push file fetchLogs = (outputFile) -> - qs = if outputFile?.build? then "?build=#{outputFile.build}" else "" - $http.get "/project/#{$scope.project_id}/output/output.log" + qs + if outputFile?.build? + logUrl = "/project/#{$scope.project_id}/build/#{outputFile.build}/output/output.log" + else + logUrl = "/project/#{$scope.project_id}/output/output.log" + $http.get logUrl .success (log) -> #console.log ">>", log $scope.pdf.rawLog = log @@ -126,8 +137,12 @@ define [ type: if entry.level == "error" then "error" else "warning" text: entry.message } - # Get the biber log and parse it too - $http.get "/project/#{$scope.project_id}/output/output.blg" + qs + # Get the biber log and parse it + if outputFile?.build? + biberLogUrl = "/project/#{$scope.project_id}/build/#{outputFile.build}/output/output.blg" + else + biberLogUrl = "/project/#{$scope.project_id}/output/output.blg" + $http.get biberLogUrl .success (log) -> window._s = $scope biberLogEntries = BibLogParser.parse(log, {}) From 30a778c8e8590d121074dffc912c096af020290a Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 13 May 2016 13:30:00 +0100 Subject: [PATCH 161/208] add temp module --- services/web/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/package.json b/services/web/package.json index a2940f911f..13c513c386 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -52,6 +52,7 @@ "sanitizer": "0.1.1", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", "sixpack-client": "^1.0.0", + "temp": "^0.8.3", "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master", "underscore": "1.6.0", "underscore.string": "^3.0.2", From 4f533147b194a51fbc5a3ddc5f309b0304dc13de Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 17 May 2016 16:59:47 +0100 Subject: [PATCH 162/208] enable range requests on files via `?range=m-n` query string. --- .../app/coffee/Features/FileStore/FileStoreHandler.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee index eaf9c87554..1512b53e7e 100644 --- a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee +++ b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee @@ -42,6 +42,11 @@ module.exports = FileStoreHandler = method : "get" uri: "#{@_buildUrl(project_id, file_id)}#{queryString}" timeout:fiveMinsInMs + headers: {} + if query? and query['range']? + rangeText = query['range'] + if rangeText && rangeText.match? && rangeText.match(/\d+-\d+/) + opts.headers['range'] = "bytes=#{query['range']}" readStream = request(opts) readStream.on "error", (err) -> logger.err {err, project_id, file_id, query}, "error in file stream" From eae82a2b207e8c8307377f4e0860295c1ca57603 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 17 May 2016 17:00:14 +0100 Subject: [PATCH 163/208] Preview bib files. --- .../app/views/project/editor/binary-file.jade | 18 +++++++++- .../controllers/BinaryFileController.coffee | 34 ++++++++++++++++++- .../stylesheets/app/editor/binary-file.less | 16 +++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/binary-file.jade b/services/web/app/views/project/editor/binary-file.jade index 6e81c7a107..19741ec6d9 100644 --- a/services/web/app/views/project/editor/binary-file.jade +++ b/services/web/app/views/project/editor/binary-file.jade @@ -10,6 +10,7 @@ div.binary-file.full-size( onerror="sl_binaryFilePreviewError()" onabort="sl_binaryFilePreviewError()" ) + img( ng-show="!failedLoad" ng-src="/project/{{ project_id }}/file/{{ openFile.id }}?format=png" @@ -17,9 +18,24 @@ div.binary-file.full-size( onerror="sl_binaryFilePreviewError()" onabort="sl_binaryFilePreviewError()" ) + + p( + ng-if="(['bib'].indexOf(extension(openFile)) > -1) && !bibtex.error" + ng-init="loadBibtexFilePreview()" + ) + div(ng-if="bibtex.loading") + | #{translate('loading')}... + div.bib-preview(ng-if="!bibtex.loading && !bibtex.error") + div.scroll-container + p + | {{ bibtexPreview.data }} + p + | ... + p.no-preview( - ng-if="failedLoad || ['png', 'jpg', 'jpeg', 'gif', 'pdf', 'eps'].indexOf(extension(openFile)) == -1" + ng-if="failedLoad || bibtex.error || ['bib', 'png', 'jpg', 'jpeg', 'gif', 'pdf', 'eps'].indexOf(extension(openFile)) == -1" ) #{translate("no_preview_available")} + a.btn.btn-info( ng-href="/project/{{ project_id }}/file/{{ openFile.id }}" ) #{translate("download")} {{ openFile.name }} diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index 1ad8ef91e3..2e179c5209 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -1,7 +1,12 @@ define [ "base" ], (App) -> - App.controller "BinaryFileController", ["$scope", "$rootScope", ($scope, $rootScope) -> + App.controller "BinaryFileController", ["$scope", "$rootScope", "$http", "$timeout", ($scope, $rootScope, $http, $timeout) -> + + $scope.bibtexPreview = + loading: false + error: false + data: "" $scope.failedLoad = false $rootScope.$on 'entity:selected', () -> @@ -13,4 +18,31 @@ define [ $scope.extension = (file) -> return file.name.split(".").pop()?.toLowerCase() + + $scope.loadBibtexFilePreview = () -> + url = "/project/#{project_id}/file/#{$scope.openFile.id}?range=0-5000" + $scope.bibtexPreview.loading = true + $http.get(url) + .success (data) -> + $scope.bibtexPreview.loading = false + $scope.bibtexPreview.error = false + try + # remove last partial line + data = data.replace(/\n.*$/, '') + finally + $scope.bibtexPreview.data = data + $timeout($scope.setHeight, 0) + .error (err) -> + $scope.bibtexPreview.error = true + $scope.bibtexPreview.loading = false + + $scope.setHeight = () -> + # Behold, a ghastly hack + guide = document.querySelector('.file-tree-inner') + table_wrap = document.querySelector('.bib-preview .scroll-container') + if table_wrap + desired_height = guide.offsetHeight - 60 + if table_wrap.offsetHeight > desired_height + table_wrap.style.height = desired_height + 'px' + table_wrap.style['max-height'] = desired_height + 'px' ] diff --git a/services/web/public/stylesheets/app/editor/binary-file.less b/services/web/public/stylesheets/app/editor/binary-file.less index 8e8467e5f9..69b8c9f08d 100644 --- a/services/web/public/stylesheets/app/editor/binary-file.less +++ b/services/web/public/stylesheets/app/editor/binary-file.less @@ -17,5 +17,21 @@ font-size: 24px; color: @gray; } + .bib-preview { + margin-bottom: 12px; + .scroll-container { + font-size: 0.8em; + line-height: 1.1em; + overflow: auto; + border: 1px solid @gray-lighter; + padding-left: 12px; + padding-right: 12px; + padding-top: 8px; + padding-bottom: 8px; + text-align: left; + white-space: pre; + font-family: monospace; + } + } } From 78e52d8c96d46b6e984421d748b7749b70af20cd Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 18 May 2016 09:58:57 +0100 Subject: [PATCH 164/208] Update FileStoreHandlerTests --- .../FileStore/FileStoreHandlerTests.coffee | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/services/web/test/UnitTests/coffee/FileStore/FileStoreHandlerTests.coffee b/services/web/test/UnitTests/coffee/FileStore/FileStoreHandlerTests.coffee index 3b546c74a6..caae9c0ae8 100644 --- a/services/web/test/UnitTests/coffee/FileStore/FileStoreHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/FileStore/FileStoreHandlerTests.coffee @@ -111,29 +111,56 @@ describe "FileStoreHandler", -> describe "getFileStream", -> beforeEach -> + @query = {} @request.returns(@readStream) it "should get the stream with the correct params", (done)-> - @handler.getFileStream @project_id, @file_id, {}, (err, stream)=> + @handler.getFileStream @project_id, @file_id, @query, (err, stream)=> @request.args[0][0].method.should.equal "get" @request.args[0][0].uri.should.equal @handler._buildUrl() done() it "should get stream from request", (done)-> - @handler.getFileStream @project_id, @file_id, {}, (err, stream)=> + @handler.getFileStream @project_id, @file_id, @query, (err, stream)=> stream.should.equal @readStream done() it "builds the correct url", (done)-> - @handler.getFileStream @project_id, @file_id, {}, (err, stream)=> + @handler.getFileStream @project_id, @file_id, @query, (err, stream)=> @handler._buildUrl.calledWith(@project_id, @file_id).should.equal true done() - + it "should add an error handler", (done) -> - @handler.getFileStream @project_id, @file_id, {}, (err, stream)=> + @handler.getFileStream @project_id, @file_id, @query, (err, stream)=> stream.on.calledWith("error").should.equal true done() - + + describe 'when range is specified in query', -> + + beforeEach -> + @query = {'range': '0-10'} + + it 'should add a range header', (done) -> + @handler.getFileStream @project_id, @file_id, @query, (err, stream)=> + @request.callCount.should.equal 1 + headers = @request.firstCall.args[0].headers + expect(headers).to.have.keys('range') + expect(headers['range']).to.equal 'bytes=0-10' + done() + + describe 'when range is invalid', -> + + ['0-', '-100', 'one-two', 'nonsense'].forEach (r) => + + beforeEach -> + @query = {'range': "#{r}"} + + it "should not add a range header for '#{r}'", (done) -> + @handler.getFileStream @project_id, @file_id, @query, (err, stream)=> + @request.callCount.should.equal 1 + headers = @request.firstCall.args[0].headers + expect(headers).to.not.have.keys('range') + done() describe "copyFile", -> From b0baea5073d061d761dd046ea966905a8af761e5 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 18 May 2016 10:09:22 +0100 Subject: [PATCH 165/208] add query string to end of project resources --- .../Features/Compile/ClsiManager.coffee | 22 +++++++++++++------ .../Features/Compile/CompileManager.coffee | 4 ++-- .../coffee/Compile/ClsiManagerTests.coffee | 2 ++ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 473df0f5cd..df7b5749cf 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -20,11 +20,13 @@ module.exports = ClsiManager = logger.err err:error, project_id:project_id, "error sending request to clsi" return callback(error) logger.log project_id: project_id, response: response, "received compile response from CLSI" - callback( - null - response?.compile?.status - ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles) - ) + ClsiCookieManager._getServerId project_id, (err, clsiServerId)-> + if err? + logger.err err:err, project_id:project_id, "error getting server id" + return callback(err) + outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles, clsiServerId) + console.log outputFiles + callback(null, response?.compile?.status, outputFiles) deleteAuxFiles: (project_id, options, callback = (error) ->) -> compilerUrl = @_getCompilerUrl(options?.compileGroup) @@ -74,11 +76,17 @@ module.exports = ClsiManager = logger.error err: error, project_id: project_id, "CLSI returned failure code" callback error, body - _parseOutputFiles: (project_id, rawOutputFiles = []) -> + _parseOutputFiles: (project_id, rawOutputFiles = [], clsiServer) -> + # console.log rawOutputFiles outputFiles = [] for file in rawOutputFiles + console.log path + path = url.parse(file.url).path + path = path.replace("/project/#{project_id}/output/", "") + if clsiServer? + path = "#{path}?clsiserver=#{clsiServer}" outputFiles.push - path: url.parse(file.url).path.replace("/project/#{project_id}/output/", "") + path: path type: file.type build: file.build return outputFiles diff --git a/services/web/app/coffee/Features/Compile/CompileManager.coffee b/services/web/app/coffee/Features/Compile/CompileManager.coffee index c89e7107dd..8249352e5d 100755 --- a/services/web/app/coffee/Features/Compile/CompileManager.coffee +++ b/services/web/app/coffee/Features/Compile/CompileManager.coffee @@ -1,8 +1,6 @@ Settings = require('settings-sharelatex') - redis = require("redis-sharelatex") rclient = redis.createClient(Settings.redis.web) - DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler" Project = require("../../models/Project").Project ProjectRootDocManager = require "../Project/ProjectRootDocManager" @@ -13,6 +11,8 @@ logger = require("logger-sharelatex") rateLimiter = require("../../infrastructure/RateLimiter") module.exports = CompileManager = + + compile: (project_id, user_id, options = {}, _callback = (error) ->) -> timer = new Metrics.Timer("editor.compile") callback = (args...) -> diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee index ba4f717e5f..89f312441d 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee @@ -11,6 +11,7 @@ describe "ClsiManager", -> @ClsiCookieManager = getCookieJar: sinon.stub().callsArgWith(1, null, @jar) setServerId: sinon.stub().callsArgWith(2) + _getServerId:sinon.stub() @ClsiManager = SandboxedModule.require modulePath, requires: "settings-sharelatex": @settings = apis: @@ -32,6 +33,7 @@ describe "ClsiManager", -> describe "sendRequest", -> beforeEach -> @ClsiManager._buildRequest = sinon.stub().callsArgWith(2, null, @request = "mock-request") + @ClsiCookieManager._getServerId.callsArgWith(1, null, "clsi3") describe "with a successful compile", -> beforeEach -> From 8621d497b46a6cb71dd74d0cf0ef7e8a116ae9cc Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 18 May 2016 11:14:24 +0100 Subject: [PATCH 166/208] Fix view so that bib preview only shows for bib files --- services/web/app/views/project/editor/binary-file.jade | 5 +---- .../ide/binary-files/controllers/BinaryFileController.coffee | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/services/web/app/views/project/editor/binary-file.jade b/services/web/app/views/project/editor/binary-file.jade index 19741ec6d9..c9665503d9 100644 --- a/services/web/app/views/project/editor/binary-file.jade +++ b/services/web/app/views/project/editor/binary-file.jade @@ -19,10 +19,7 @@ div.binary-file.full-size( onabort="sl_binaryFilePreviewError()" ) - p( - ng-if="(['bib'].indexOf(extension(openFile)) > -1) && !bibtex.error" - ng-init="loadBibtexFilePreview()" - ) + div(ng-if="(['bib'].indexOf(extension(openFile)) > -1) && !bibtex.error", ng-init="loadBibtexFilePreview()") div(ng-if="bibtex.loading") | #{translate('loading')}... div.bib-preview(ng-if="!bibtex.loading && !bibtex.error") diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index 2e179c5209..e914c41401 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -41,7 +41,7 @@ define [ guide = document.querySelector('.file-tree-inner') table_wrap = document.querySelector('.bib-preview .scroll-container') if table_wrap - desired_height = guide.offsetHeight - 60 + desired_height = guide.offsetHeight - 44 if table_wrap.offsetHeight > desired_height table_wrap.style.height = desired_height + 'px' table_wrap.style['max-height'] = desired_height + 'px' From dba8d96d1143220613c79f7be928ccaa4feb5bb3 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 18 May 2016 12:50:50 +0100 Subject: [PATCH 167/208] pass clsiServerId to the client and use it as query stirng for requests --- .../Features/Compile/ClsiManager.coffee | 5 +--- .../Features/Compile/CompileController.coffee | 3 ++- .../Features/Compile/CompileManager.coffee | 4 ++-- .../web/app/views/project/editor/pdf.jade | 2 +- .../ide/pdf/controllers/PdfController.coffee | 24 ++++++++++++++++--- .../WordCountModalController.coffee | 8 +++++-- 6 files changed, 33 insertions(+), 13 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index df7b5749cf..3285026cfa 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -25,8 +25,7 @@ module.exports = ClsiManager = logger.err err:err, project_id:project_id, "error getting server id" return callback(err) outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles, clsiServerId) - console.log outputFiles - callback(null, response?.compile?.status, outputFiles) + callback(null, response?.compile?.status, outputFiles, clsiServerId) deleteAuxFiles: (project_id, options, callback = (error) ->) -> compilerUrl = @_getCompilerUrl(options?.compileGroup) @@ -83,8 +82,6 @@ module.exports = ClsiManager = console.log path path = url.parse(file.url).path path = path.replace("/project/#{project_id}/output/", "") - if clsiServer? - path = "#{path}?clsiserver=#{clsiServer}" outputFiles.push path: path type: file.type diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index 6ffca6affe..66f98c5974 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -30,13 +30,14 @@ module.exports = CompileController = if req.body?.draft options.draft = req.body.draft logger.log {options, project_id}, "got compile request" - CompileManager.compile project_id, user_id, options, (error, status, outputFiles, output, limits) -> + CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits) -> return next(error) if error? res.contentType("application/json") res.status(200).send JSON.stringify { status: status outputFiles: outputFiles compileGroup: limits?.compileGroup + clsiServerId:clsiServerId } downloadPdf: (req, res, next = (error) ->)-> diff --git a/services/web/app/coffee/Features/Compile/CompileManager.coffee b/services/web/app/coffee/Features/Compile/CompileManager.coffee index 8249352e5d..0d8d480b7b 100755 --- a/services/web/app/coffee/Features/Compile/CompileManager.coffee +++ b/services/web/app/coffee/Features/Compile/CompileManager.coffee @@ -37,10 +37,10 @@ module.exports = CompileManager = return callback(error) if error? for key, value of limits options[key] = value - ClsiManager.sendRequest project_id, options, (error, status, outputFiles, output) -> + ClsiManager.sendRequest project_id, options, (error, status, outputFiles, clsiServerId) -> return callback(error) if error? logger.log files: outputFiles, "output files" - callback(null, status, outputFiles, output, limits) + callback(null, status, outputFiles, clsiServerId, limits) deleteAuxFiles: (project_id, callback = (error) ->) -> CompileManager.getProjectCompileLimits project_id, (error, limits) -> diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index f633060aaf..186391893f 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -121,7 +121,7 @@ div.full-size.pdf(ng-controller="PdfController") ul.dropdown-menu.dropdown-menu-right li(ng-repeat="file in pdf.outputFiles") a( - href="/project/{{project_id}}/output/{{file.path}}" + href="{{file.url}}" target="_blank" ) {{ file.name }} a.btn.btn-info.btn-sm(href, ng-click="toggleRawLog()") diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 5847b58e3e..82be226d42 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -37,6 +37,9 @@ define [ } parseCompileResponse = (response) -> + if response.clsiServerId? and response.clsiServerId != $scope.pdf.clsiServerId + ide.clsiServerId = response.clsiServerId + # Reset everything $scope.pdf.error = false $scope.pdf.timedout = false @@ -76,6 +79,8 @@ define [ if response.compileGroup? $scope.pdf.compileGroup = response.compileGroup $scope.pdf.url = $scope.pdf.url + "&compileGroup=#{$scope.pdf.compileGroup}" + if response.clsiServerId? + $scope.pdf.url = $scope.pdf.url + "&clsiserverid=#{response.clsiServerId}" # make a cache to look up files by name fileByPath = {} for file in response.outputFiles @@ -99,11 +104,19 @@ define [ file.name = "#{file.path.replace(/^output\./, "")} file" else file.name = file.path + file.url = "/project/#{project_id}/output/#{file.path}" + if response.clsiServerId? + file.url = file.url + "?clsiserverid=#{response.clsiServerId}" $scope.pdf.outputFiles.push file fetchLogs = (outputFile) -> - qs = if outputFile?.build? then "?build=#{outputFile.build}" else "" - $http.get "/project/#{$scope.project_id}/output/output.log" + qs + opts = + method:"GET" + url:"/project/#{$scope.project_id}/output/output.log" + params: + build:outputFile.build + clsiserverid:ide.clsiServerId + $http opts .success (log) -> #console.log ">>", log $scope.pdf.rawLog = log @@ -126,7 +139,8 @@ define [ text: entry.message } # Get the biber log and parse it too - $http.get "/project/#{$scope.project_id}/output/output.blg" + qs + opts.url = "/project/#{$scope.project_id}/output/output.blg" + $http opts .success (log) -> window._s = $scope biberLogEntries = BibLogParser.parse(log, {}) @@ -189,6 +203,8 @@ define [ $http { url: "/project/#{$scope.project_id}/output" method: "DELETE" + params: + clsiserverid:ide.clsiServerId headers: "X-Csrf-Token": window.csrfToken } @@ -271,6 +287,7 @@ define [ file: path line: row + 1 column: column + clsiserverid:ide.clsiServerId } }) .success (data) -> @@ -298,6 +315,7 @@ define [ page: position.page + 1 h: position.offset.left.toFixed(2) v: position.offset.top.toFixed(2) + clsiserverid:ide.clsiServerId } }) .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 3c69d3e276..e880a25eef 100644 --- a/services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee +++ b/services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee @@ -5,11 +5,15 @@ define [ $scope.status = loading:true - $http.get("/project/#{ide.project_id}/wordcount") + opts = + url:"/project/#{ide.project_id}/wordcount" + method:"GET" + params: + clsiserverid:ide.clsiServerId + $http opts .success (data) -> $scope.status.loading = false $scope.data = data.texcount - console.log $scope.data .error () -> $scope.status.error = true From d2cc75b73f5fd3f7f832a1c946aa9400239f0a6a Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 18 May 2016 14:38:17 +0100 Subject: [PATCH 168/208] changed $scope.pdf.clsiServerId to ide.clsiServerId --- .../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 82be226d42..32159750e5 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -37,7 +37,7 @@ define [ } parseCompileResponse = (response) -> - if response.clsiServerId? and response.clsiServerId != $scope.pdf.clsiServerId + if response.clsiServerId? and response.clsiServerId != ide.clsiServerId ide.clsiServerId = response.clsiServerId # Reset everything From d813f45d28ba6ac9b77bbc50709e9af228c4c8c4 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 17 Jul 2015 16:30:04 +0100 Subject: [PATCH 169/208] Cherry-pick 0e70320 (Select icon based on file extension) from `datajoy` --- services/web/app/views/project/editor/file-tree.jade | 3 +-- .../file-tree/controllers/FileTreeController.coffee | 2 +- .../controllers/FileTreeEntityController.coffee | 11 +++++++++++ .../web/public/stylesheets/app/editor/file-tree.less | 8 ++++---- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/services/web/app/views/project/editor/file-tree.jade b/services/web/app/views/project/editor/file-tree.jade index 630cd1aa17..77cbd10da9 100644 --- a/services/web/app/views/project/editor/file-tree.jade +++ b/services/web/app/views/project/editor/file-tree.jade @@ -107,8 +107,7 @@ script(type='text/ng-template', id='entityListItemTemplate') //- Just a spacer to align with folders i.fa.fa-fw.toggle(ng-if="entity.type != 'folder'") - i.fa.fa-fw.fa-file(ng-if="entity.type == 'doc'") - i.fa.fa-fw.fa-image(ng-if="entity.type == 'file'") + i.fa.fa-fw(ng-if="entity.type != 'folder'", ng-class="'fa-' + iconTypeFromName(entity.name)") span( ng-hide="entity.renaming" ) {{ entity.name }} diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee index cdc261053f..a0dcaa1367 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -1,7 +1,7 @@ define [ "base" ], (App) -> - App.controller "FileTreeController", ["$scope", "$modal", "ide", ($scope, $modal, ide) -> + App.controller "FileTreeController", ["$scope", "$modal", "ide", "$rootScope", ($scope, $modal, ide, $rootScope) -> $scope.openNewDocModal = () -> $modal.open( templateUrl: "newDocModalTemplate" diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee index b5c96408c7..9190267fad 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee @@ -51,6 +51,17 @@ define [ $scope.$on "delete:selected", () -> $scope.openDeleteModal() if $scope.entity.selected + + $scope.iconTypeFromName = (name) -> + ext = name.split(".").pop()?.toLowerCase() + if ext in ["png", "pdf", "jpg", "jpeg", "gif"] + return "image" + else if ext in ["csv", "xls", "xlsx"] + return "table" + else if ext in ["py", "r"] + return "file-text" + else + return "file" ] App.controller "DeleteEntityModalController", [ diff --git a/services/web/public/stylesheets/app/editor/file-tree.less b/services/web/public/stylesheets/app/editor/file-tree.less index 6f70c3299f..55d46c1e5a 100644 --- a/services/web/public/stylesheets/app/editor/file-tree.less +++ b/services/web/public/stylesheets/app/editor/file-tree.less @@ -52,13 +52,13 @@ aside#file-tree { } } - i.fa-folder-open, i.fa-folder { - color: lighten(desaturate(@link-color, 10%), 5%); + i.fa { + color: @gray-light; font-size: 14px; } - i.fa-file, i.fa-image, i.fa-file-pdf-o { - color: @gray-light; + i.fa-folder-open, i.fa-folder { + color: lighten(desaturate(@link-color, 10%), 5%); font-size: 14px; } From 10f64004fcd3ec19c3d5697ce27b4914ef1c7c36 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 18 May 2016 15:58:24 +0100 Subject: [PATCH 170/208] Show a book icon for `*.bib` files. --- .../ide/file-tree/controllers/FileTreeEntityController.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee index 9190267fad..f0813cf03b 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee @@ -60,6 +60,8 @@ define [ return "table" else if ext in ["py", "r"] return "file-text" + else if ext in ['bib'] + return 'book' else return "file" ] From 6305cea3e39595b68e5c8951fd0bee83c1048622 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 18 May 2016 16:17:38 +0100 Subject: [PATCH 171/208] clean up log requests, avoid requesting blg file when not present --- .../ide/pdf/controllers/PdfController.coffee | 118 ++++++++++-------- 1 file changed, 67 insertions(+), 51 deletions(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 0eab163e99..1b9407bc27 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -92,7 +92,7 @@ define [ $scope.pdf.qs = if qs_args.length then "?" + qs_args.join("&") else "" $scope.pdf.url += $scope.pdf.qs - fetchLogs(fileByPath['output.log']) + fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) IGNORE_FILES = ["output.fls", "output.fdb_latexmk"] $scope.pdf.outputFiles = [] @@ -108,56 +108,72 @@ define [ file.name = file.path $scope.pdf.outputFiles.push file - fetchLogs = (outputFile) -> - if outputFile?.build? - logUrl = "/project/#{$scope.project_id}/build/#{outputFile.build}/output/output.log" - else - logUrl = "/project/#{$scope.project_id}/output/output.log" - $http.get logUrl - .success (log) -> - #console.log ">>", log - $scope.pdf.rawLog = log - logEntries = LogParser.parse(log, ignoreDuplicates: true) - #console.log ">>", logEntries - $scope.pdf.logEntries = logEntries - $scope.pdf.logEntries.all = logEntries.errors.concat(logEntries.warnings).concat(logEntries.typesetting) - # # # # - proceed = () -> - $scope.pdf.logEntryAnnotations = {} - for entry in logEntries.all - if entry.file? - entry.file = normalizeFilePath(entry.file) - entity = ide.fileTreeManager.findEntityByPath(entry.file) - if entity? - $scope.pdf.logEntryAnnotations[entity.id] ||= [] - $scope.pdf.logEntryAnnotations[entity.id].push { - row: entry.line - 1 - type: if entry.level == "error" then "error" else "warning" - text: entry.message - } - # Get the biber log and parse it - if outputFile?.build? - biberLogUrl = "/project/#{$scope.project_id}/build/#{outputFile.build}/output/output.blg" - else - biberLogUrl = "/project/#{$scope.project_id}/output/output.blg" - $http.get biberLogUrl - .success (log) -> - window._s = $scope - biberLogEntries = BibLogParser.parse(log, {}) - if $scope.pdf.logEntries - entries = $scope.pdf.logEntries - all = biberLogEntries.errors.concat(biberLogEntries.warnings) - entries.all = entries.all.concat(all) - entries.errors = entries.errors.concat(biberLogEntries.errors) - entries.warnings = entries.warnings.concat(biberLogEntries.warnings) - proceed() - .error (e) -> - # it's not an error for the output.blg file to not be present - proceed() - # # # # - .error () -> - $scope.pdf.logEntries = [] - $scope.pdf.rawLog = "" + fetchLogs = (logFile, blgFile) -> + + getFile = (name, file) -> + if file?.build? + url = "/project/#{$scope.project_id}/build/#{file.build}/output/#{name}" + else + url = "/project/#{$scope.project_id}/output/#{name}" + return $http.get url + + # accumulate the log entries + logEntries = + all: [] + errors: [] + warnings: [] + + accumulateResults = (newEntries) -> + for key in ['all', 'errors', 'warnings'] + logEntries[key] = logEntries[key].concat newEntries[key] + + # use the parsers for each file type + processLog = (log) -> + $scope.pdf.rawLog = log + {errors, warnings, typesetting} = LogParser.parse(log, ignoreDuplicates: true) + all = [].concat errors, warnings, typesetting + accumulateResults {all, errors, warnings} + + processBiber = (log) -> + {errors, warnings} = BibLogParser.parse(log, {}) + all = [].concat errors, warnings + accumulateResults {all, errors, warnings} + + # output the results + handleError = () -> + $scope.pdf.logEntries = [] + $scope.pdf.rawLog = "" + + annotateFiles = () -> + $scope.pdf.logEntries = logEntries + $scope.pdf.logEntryAnnotations = {} + for entry in logEntries.all + if entry.file? + entry.file = normalizeFilePath(entry.file) + entity = ide.fileTreeManager.findEntityByPath(entry.file) + if entity? + $scope.pdf.logEntryAnnotations[entity.id] ||= [] + $scope.pdf.logEntryAnnotations[entity.id].push { + row: entry.line - 1 + type: if entry.level == "error" then "error" else "warning" + text: entry.message + } + + # retrieve the logfile and process it + response = getFile('output.log', logFile) + .success processLog + .error handleError + + if blgFile? # retrieve the blg file if present + response.success () -> + getFile('output.blg', blgFile) + # ignore errors in biber file + .success processBiber + # display the combined result + .then annotateFiles + else # otherwise just display the result + response.success annotateFiles + getRootDocOverride_id = () -> doc = ide.editorManager.getCurrentDocValue() From 4509642914fcd5968f9c14d1e461ca4e009c0ffd Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 18 May 2016 16:34:50 +0100 Subject: [PATCH 172/208] use highlight colour for any font-awesome icons in file-tree. --- services/web/public/stylesheets/app/editor/file-tree.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/app/editor/file-tree.less b/services/web/public/stylesheets/app/editor/file-tree.less index 55d46c1e5a..5a4d7feed1 100644 --- a/services/web/public/stylesheets/app/editor/file-tree.less +++ b/services/web/public/stylesheets/app/editor/file-tree.less @@ -112,7 +112,7 @@ aside#file-tree { border-right: 4px solid @link-color; font-weight: bold; padding-right: 32px; - i.fa-folder-open, i.fa-folder, i.fa-file, i.fa-image, i.fa-file-pdf-o { + i.fa-folder-open, i.fa { color: @link-color; } .entity-menu-toggle { From 5df5ba868c500afe525c915b5ab74a0c211112f0 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 19 May 2016 10:02:07 +0100 Subject: [PATCH 173/208] Fix logic for showing error or loading message. --- services/web/app/views/project/editor/binary-file.jade | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/binary-file.jade b/services/web/app/views/project/editor/binary-file.jade index c9665503d9..5783244f79 100644 --- a/services/web/app/views/project/editor/binary-file.jade +++ b/services/web/app/views/project/editor/binary-file.jade @@ -20,9 +20,10 @@ div.binary-file.full-size( ) div(ng-if="(['bib'].indexOf(extension(openFile)) > -1) && !bibtex.error", ng-init="loadBibtexFilePreview()") - div(ng-if="bibtex.loading") + div.bib-loading(ng-if="bibtexPreview.loading") | #{translate('loading')}... - div.bib-preview(ng-if="!bibtex.loading && !bibtex.error") + + div.bib-preview(ng-if="!bibtexPreview.loading && !bibtexPreview.error") div.scroll-container p | {{ bibtexPreview.data }} From 3c298ed6ad5550d2d3b8d2eb8ec922ca332101e9 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 19 May 2016 10:02:25 +0100 Subject: [PATCH 174/208] Preview up to Two Megabytes of bib files --- .../ide/binary-files/controllers/BinaryFileController.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index e914c41401..473878bc29 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -3,6 +3,8 @@ define [ ], (App) -> App.controller "BinaryFileController", ["$scope", "$rootScope", "$http", "$timeout", ($scope, $rootScope, $http, $timeout) -> + TWO_MEGABYTES = 2 * 1024 * 1024 + $scope.bibtexPreview = loading: false error: false @@ -20,7 +22,7 @@ define [ return file.name.split(".").pop()?.toLowerCase() $scope.loadBibtexFilePreview = () -> - url = "/project/#{project_id}/file/#{$scope.openFile.id}?range=0-5000" + url = "/project/#{project_id}/file/#{$scope.openFile.id}?range=0-#{TWO_MEGABYTES}" $scope.bibtexPreview.loading = true $http.get(url) .success (data) -> From 199844db2d0982cf81cf127b99b5496cf5001741 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 19 May 2016 10:02:46 +0100 Subject: [PATCH 175/208] Fix styling of the bib-loading div. --- services/web/public/stylesheets/app/editor/binary-file.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/web/public/stylesheets/app/editor/binary-file.less b/services/web/public/stylesheets/app/editor/binary-file.less index 69b8c9f08d..7b42bb9b26 100644 --- a/services/web/public/stylesheets/app/editor/binary-file.less +++ b/services/web/public/stylesheets/app/editor/binary-file.less @@ -17,6 +17,11 @@ font-size: 24px; color: @gray; } + .bib-loading { + font-size: 24px; + color: @gray; + margin-bottom: 12px; + } .bib-preview { margin-bottom: 12px; .scroll-container { From b70a993572d4d199a7e48258ef19615c21301464 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 19 May 2016 11:11:23 +0100 Subject: [PATCH 176/208] Make bib previews more robust. --- .../web/app/views/project/editor/binary-file.jade | 8 ++++---- .../controllers/BinaryFileController.coffee | 13 ++++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/services/web/app/views/project/editor/binary-file.jade b/services/web/app/views/project/editor/binary-file.jade index 5783244f79..1420fb4e9f 100644 --- a/services/web/app/views/project/editor/binary-file.jade +++ b/services/web/app/views/project/editor/binary-file.jade @@ -19,11 +19,11 @@ div.binary-file.full-size( onabort="sl_binaryFilePreviewError()" ) - div(ng-if="(['bib'].indexOf(extension(openFile)) > -1) && !bibtex.error", ng-init="loadBibtexFilePreview()") - div.bib-loading(ng-if="bibtexPreview.loading") + div(ng-if="(['bib'].indexOf(extension(openFile)) > -1) && !bibtexPreview.error") + div.bib-loading(ng-show="bibtexPreview.loading == true") | #{translate('loading')}... - div.bib-preview(ng-if="!bibtexPreview.loading && !bibtexPreview.error") + div.bib-preview(ng-show="bibtexPreview.data && !bibtexPreview.loading && !bibtexPreview.error") div.scroll-container p | {{ bibtexPreview.data }} @@ -31,7 +31,7 @@ div.binary-file.full-size( | ... p.no-preview( - ng-if="failedLoad || bibtex.error || ['bib', 'png', 'jpg', 'jpeg', 'gif', 'pdf', 'eps'].indexOf(extension(openFile)) == -1" + ng-if="failedLoad || bibtexPreview.error || ['bib', 'png', 'jpg', 'jpeg', 'gif', 'pdf', 'eps'].indexOf(extension(openFile)) == -1" ) #{translate("no_preview_available")} a.btn.btn-info( diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index 473878bc29..147a0b829f 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -8,11 +8,18 @@ define [ $scope.bibtexPreview = loading: false error: false - data: "" + data: null $scope.failedLoad = false + $rootScope.$on 'entity:selected', () -> $scope.failedLoad = false + $scope.loadBibtexIfRequired() + + $scope.loadBibtexIfRequired = () -> + if $scope.extension($scope.openFile) == 'bib' + $scope.bibtexPreview.data = null + $timeout($scope.loadBibtexFilePreview, 0) window.sl_binaryFilePreviewError = () => $scope.failedLoad = true @@ -24,6 +31,7 @@ define [ $scope.loadBibtexFilePreview = () -> url = "/project/#{project_id}/file/#{$scope.openFile.id}?range=0-#{TWO_MEGABYTES}" $scope.bibtexPreview.loading = true + $scope.$apply() $http.get(url) .success (data) -> $scope.bibtexPreview.loading = false @@ -47,4 +55,7 @@ define [ if table_wrap.offsetHeight > desired_height table_wrap.style.height = desired_height + 'px' table_wrap.style['max-height'] = desired_height + 'px' + + $scope.loadBibtexIfRequired() + ] From b649c13c17f2572f5ebc3ee9ba9ef24002e39d59 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 19 May 2016 11:25:24 +0100 Subject: [PATCH 177/208] Eliminate weird flicker when loading bibtex --- services/web/app/views/project/editor/binary-file.jade | 3 ++- .../ide/binary-files/controllers/BinaryFileController.coffee | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/binary-file.jade b/services/web/app/views/project/editor/binary-file.jade index 1420fb4e9f..4f451f0020 100644 --- a/services/web/app/views/project/editor/binary-file.jade +++ b/services/web/app/views/project/editor/binary-file.jade @@ -20,7 +20,8 @@ div.binary-file.full-size( ) div(ng-if="(['bib'].indexOf(extension(openFile)) > -1) && !bibtexPreview.error") - div.bib-loading(ng-show="bibtexPreview.loading == true") + + div.bib-loading(ng-show="bibtexPreview.loading && !bibtexPreview.error") | #{translate('loading')}... div.bib-preview(ng-show="bibtexPreview.data && !bibtexPreview.loading && !bibtexPreview.error") diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index 147a0b829f..124393d037 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -19,7 +19,7 @@ define [ $scope.loadBibtexIfRequired = () -> if $scope.extension($scope.openFile) == 'bib' $scope.bibtexPreview.data = null - $timeout($scope.loadBibtexFilePreview, 0) + $scope.loadBibtexFilePreview() window.sl_binaryFilePreviewError = () => $scope.failedLoad = true From 036b179ffe1473f9cf96f6168689d6707433a001 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 19 May 2016 13:28:20 +0100 Subject: [PATCH 178/208] put the clsiserverid onto qs object for correct server allocation --- .../ide/pdf/controllers/PdfController.coffee | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 478fed3ecc..0ce85686cf 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -36,9 +36,7 @@ define [ _csrf: window.csrfToken } - parseCompileResponse = (response) -> - if response.clsiServerId? and response.clsiServerId != ide.clsiServerId - ide.clsiServerId = response.clsiServerId + parseCompileResponse = (response) -> # Reset everything $scope.pdf.error = false @@ -72,14 +70,7 @@ define [ else if response.status == "success" $scope.pdf.view = 'pdf' $scope.shouldShowLogs = false - # define the base url - $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf?cache_bust=#{Date.now()}" - # add a query string parameter for the compile group - if response.compileGroup? - $scope.pdf.compileGroup = response.compileGroup - $scope.pdf.url = $scope.pdf.url + "&compileGroup=#{$scope.pdf.compileGroup}" - if response.clsiServerId? - $scope.pdf.url = $scope.pdf.url + "&clsiserverid=#{response.clsiServerId}" + # make a cache to look up files by name fileByPath = {} for file in response.outputFiles @@ -98,6 +89,9 @@ define [ if response.compileGroup? $scope.pdf.compileGroup = response.compileGroup qs.compileGroup = "#{$scope.pdf.compileGroup}" + if response.clsiServerId? + qs.clsiserverid = response.clsiServerId + ide.clsiServerId = response.clsiServerId # convert the qs hash into a query string and append it qs_args = ("#{k}=#{v}" for k, v of qs) $scope.pdf.qs = if qs_args.length then "?" + qs_args.join("&") else "" From 789257fd4aa963b75a4950bec54f6f8647617511 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 19 May 2016 13:45:44 +0100 Subject: [PATCH 179/208] use cookie key when setting cookie for jar --- .../app/coffee/Features/Compile/ClsiCookieManager.coffee | 2 +- .../UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee index 81e2e633e6..3372f9c871 100644 --- a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee @@ -56,7 +56,7 @@ module.exports = ClsiCookieManager = if err? logger.err err:err, project_id:project_id, "error getting server id" return callback(err) - serverCookie = request.cookie("clsiserver=#{serverId}") + serverCookie = request.cookie("#{Settings.clsiCookieKey}=#{serverId}") jar = request.jar() jar.setCookie serverCookie, Settings.apis.clsi.url callback(null, jar) diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee index 1fc8716657..c2feb2a2c2 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee @@ -112,9 +112,9 @@ describe "ClsiCookieManager", -> @ClsiCookieManager._getServerId = sinon.stub().callsArgWith(1, null, "clsi-11") it "should return a jar with the cookie set populated from redis", (done)-> - @ClsiCookieManager.getCookieJar @project_id, (err, jar)-> - jar._jar.store.idx["clsi.example.com"]["/"].clsiserver.key.should.equal "clsiserver" - jar._jar.store.idx["clsi.example.com"]["/"].clsiserver.value.should.equal "clsi-11" + @ClsiCookieManager.getCookieJar @project_id, (err, jar)=> + jar._jar.store.idx["clsi.example.com"]["/"][@settings.clsiCookieKey].key.should.equal + jar._jar.store.idx["clsi.example.com"]["/"][@settings.clsiCookieKey].value.should.equal "clsi-11" done() From 0ea16f0bcca80db4734edf2551af68cdd5f19f5d Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 19 May 2016 14:58:12 +0100 Subject: [PATCH 180/208] Index '.bib' file objects. --- .../References/ReferencesHandler.coffee | 30 ++++++++++--- .../ide/references/ReferencesManager.coffee | 3 ++ .../References/ReferencesHandlerTests.coffee | 43 ++++++++++++++++++- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index c033a4999f..7894ab6915 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -16,19 +16,31 @@ module.exports = ReferencesHandler = _buildDocUrl: (projectId, docId) -> "#{settings.apis.docstore.url}/project/#{projectId}/doc/#{docId}/raw" + _buildFileUrl: (projectId, fileId) -> + "#{settings.apis.filestore.url}/project/#{projectId}/file/#{fileId}" + + _findBibFileIds: (project) -> + ids = [] + _process = (folder) -> + (folder.fileRefs or []).forEach (file) -> + if file?.name?.match(/^.*\.bib$/) + ids.push(file._id) + (folder.folders or []).forEach (folder) -> + _process(folder) + (project.rootFolder or []).forEach (rootFolder) -> + _process(rootFolder) + return ids + _findBibDocIds: (project) -> ids = [] - _process = (folder) -> (folder.docs or []).forEach (doc) -> if doc?.name?.match(/^.*\.bib$/) ids.push(doc._id) (folder.folders or []).forEach (folder) -> _process(folder) - (project.rootFolder or []).forEach (rootFolder) -> _process(rootFolder) - return ids _isFullIndex: (project, callback = (err, result) ->) -> @@ -43,16 +55,17 @@ module.exports = ReferencesHandler = return callback(err) logger.log {projectId}, "indexing all bib files in project" docIds = ReferencesHandler._findBibDocIds(project) - ReferencesHandler._doIndexOperation(projectId, project, docIds, callback) + fileIds = ReferencesHandler._findBibFileIds(project) + ReferencesHandler._doIndexOperation(projectId, project, docIds, fileIds, callback) index: (projectId, docIds, callback=(err, data)->) -> ProjectGetter.getProject projectId, {rootFolder: true, owner_ref: 1}, (err, project) -> if err logger.err {err, projectId}, "error finding project" return callback(err) - ReferencesHandler._doIndexOperation(projectId, project, docIds, callback) + ReferencesHandler._doIndexOperation(projectId, project, docIds, [], callback) - _doIndexOperation: (projectId, project, docIds, callback) -> + _doIndexOperation: (projectId, project, docIds, fileIds, callback) -> ReferencesHandler._isFullIndex project, (err, isFullIndex) -> if err logger.err {err, projectId}, "error checking whether to do full index" @@ -67,11 +80,14 @@ module.exports = ReferencesHandler = return callback(err) bibDocUrls = docIds.map (docId) -> ReferencesHandler._buildDocUrl projectId, docId + bibFileUrls = fileIds.map (fileId) -> + ReferencesHandler._buildFileUrl projectId, fileId + allUrls = bibDocUrls.concat(bibFileUrls) logger.log {projectId, isFullIndex, docIds, bibDocUrls}, "sending request to references service" request.post { url: "#{settings.apis.references.url}/project/#{projectId}/index" json: - docUrls: bibDocUrls + docUrls: allUrls fullIndex: isFullIndex }, (err, res, data) -> if err diff --git a/services/web/public/coffee/ide/references/ReferencesManager.coffee b/services/web/public/coffee/ide/references/ReferencesManager.coffee index b0f49f54f8..2f1e95c5b1 100644 --- a/services/web/public/coffee/ide/references/ReferencesManager.coffee +++ b/services/web/public/coffee/ide/references/ReferencesManager.coffee @@ -11,6 +11,9 @@ define [ if entity?.name?.match /.*\.bib$/ @indexReferences([doc.doc_id], true) + @$scope.$on 'references:should-reindex', (e, data) => + @indexAllReferences(true) + # When we join the project: # index all references files # and don't broadcast to all clients diff --git a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee index 53e064d821..c46e71a542 100644 --- a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee @@ -21,7 +21,11 @@ describe 'ReferencesHandler', -> {name: 'two.txt', _id: 'bbb'}, ] folders: [ - {docs: [{name: 'three.bib', _id: 'ccc'}], folders: []} + { + docs: [{name: 'three.bib', _id: 'ccc'}], + fileRefs: [{name: 'four.bib', _id: 'ghg'}], + folders: [] + } ] ] @docIds = ['aaa', 'ccc'] @@ -34,6 +38,7 @@ describe 'ReferencesHandler', -> apis: references: {url: 'http://some.url/references'} docstore: {url: 'http://some.url/docstore'} + filestore: {url: 'http://some.url/filestore'} } 'request': @request = { get: sinon.stub() @@ -56,6 +61,7 @@ describe 'ReferencesHandler', -> beforeEach -> sinon.stub(@handler, '_findBibDocIds') + sinon.stub(@handler, '_findBibFileIds') sinon.stub(@handler, '_isFullIndex').callsArgWith(1, null, true) @request.post.callsArgWith(1, null, {statusCode: 200}, @fakeResponseData) @call = (callback) => @@ -198,6 +204,7 @@ describe 'ReferencesHandler', -> beforeEach -> sinon.stub(@handler, '_findBibDocIds').returns(['aaa', 'ccc']) + sinon.stub(@handler, '_findBibFileIds').returns(['fff', 'ggg']) sinon.stub(@handler, '_isFullIndex').callsArgWith(1, null, true) @request.post.callsArgWith(1, null, {statusCode: 200}, @fakeResponseData) @call = (callback) => @@ -209,6 +216,12 @@ describe 'ReferencesHandler', -> @handler._findBibDocIds.calledWith(@fakeProject).should.equal true done() + it 'should call _findBibFileIds', (done) -> + @call (err, data) => + @handler._findBibDocIds.callCount.should.equal 1 + @handler._findBibDocIds.calledWith(@fakeProject).should.equal true + done() + it 'should call DocumentUpdaterHandler.flushDocToMongo', (done) -> @call (err, data) => @DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal 2 @@ -219,7 +232,7 @@ describe 'ReferencesHandler', -> @request.post.callCount.should.equal 1 arg = @request.post.firstCall.args[0] expect(arg.json).to.have.all.keys 'docUrls', 'fullIndex' - expect(arg.json.docUrls.length).to.equal 2 + expect(arg.json.docUrls.length).to.equal 4 expect(arg.json.fullIndex).to.equal true done() @@ -309,6 +322,32 @@ describe 'ReferencesHandler', -> result = @handler._findBibDocIds(@fakeProject) expect(result).to.deep.equal @expectedIds + describe '_findBibFileIds', -> + + beforeEach -> + @fakeProject = + rootFolder: [ + docs: [ + {name: 'one.bib', _id: 'aaa'}, + {name: 'two.txt', _id: 'bbb'}, + ] + fileRefs: [ + {name: 'other.bib', _id: 'ddd'} + ], + folders: [ + { + docs: [{name: 'three.bib', _id: 'ccc'}], + fileRefs: [{name: 'four.bib', _id: 'ghg'}], + folders: [] + } + ] + ] + @expectedIds = ['ddd', 'ghg'] + + it 'should select the correct docIds', -> + result = @handler._findBibFileIds(@fakeProject) + expect(result).to.deep.equal @expectedIds + describe '_isFullIndex', -> beforeEach -> From f03a0766697d488e420e0faf649f2f1b9dd36a6e Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 19 May 2016 16:23:56 +0100 Subject: [PATCH 181/208] make cash bust add onto object not recreate it --- .../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 0ce85686cf..4c00ced0b8 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -84,7 +84,7 @@ define [ # no need to bust cache, build id is unique else $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf" - qs = { cache_bust : "#{Date.now()}" } + qs.cache_bust = "#{Date.now()}" # add a query string parameter for the compile group if response.compileGroup? $scope.pdf.compileGroup = response.compileGroup From 61b9a683aa1a0ad0bd0cc3411519ea1ff169b32f Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 19 May 2016 16:55:58 +0100 Subject: [PATCH 182/208] put clsiCookie vals into subobject in settings --- .../Features/Compile/ClsiCookieManager.coffee | 8 ++++---- .../coffee/Compile/ClsiCookieManagerTests.coffee | 15 ++++++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee index 3372f9c871..b6270cfce9 100644 --- a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee @@ -8,7 +8,7 @@ logger = require "logger-sharelatex" buildKey = (project_id)-> return "clsiserver:#{project_id}" -clsiCookiesEnabled = Settings.clsiCookieKey? and Settings.clsiCookieKey.length != 0 +clsiCookiesEnabled = Settings.clsiCookie?.key? and Settings.clsiCookie.key.length != 0 module.exports = ClsiCookieManager = @@ -36,7 +36,7 @@ module.exports = ClsiCookieManager = _parseServerIdFromResponse : (response)-> cookies = Cookie.parse(response.headers["set-cookie"]?[0] or "") - return cookies?[Settings.clsiCookieKey] + return cookies?[Settings.clsiCookie.key] setServerId: (project_id, response, callback = (err, serverId)->)-> if !clsiCookiesEnabled @@ -44,7 +44,7 @@ module.exports = ClsiCookieManager = serverId = ClsiCookieManager._parseServerIdFromResponse(response) multi = rclient.multi() multi.set buildKey(project_id), serverId - multi.expire buildKey(project_id), Settings.clsi_cookie_expire_length_seconds + multi.expire buildKey(project_id), Settings.clsiCookie.ttl multi.exec (err)-> callback(err, serverId) @@ -56,7 +56,7 @@ module.exports = ClsiCookieManager = if err? logger.err err:err, project_id:project_id, "error getting server id" return callback(err) - serverCookie = request.cookie("#{Settings.clsiCookieKey}=#{serverId}") + serverCookie = request.cookie("#{Settings.clsiCookie.key}=#{serverId}") jar = request.jar() jar.setCookie serverCookie, Settings.apis.clsi.url callback(null, jar) diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee index c2feb2a2c2..01d4ad0002 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiCookieManagerTests.coffee @@ -30,8 +30,9 @@ describe "ClsiCookieManager", -> apis: clsi: url: "http://clsi.example.com" - clsi_cookie_expire_length_seconds: Math.random() - clsiCookieKey: "coooookie" + clsiCookie: + ttl:Math.random() + key: "coooookie" @requires = "redis-sharelatex" : createClient: => @@ -90,7 +91,7 @@ describe "ClsiCookieManager", -> it "should set the server id with a ttl", (done)-> @ClsiCookieManager.setServerId @project_id, @response, (err)=> @redisMulti.set.calledWith("clsiserver:#{@project_id}", "clsi-8").should.equal true - @redisMulti.expire.calledWith("clsiserver:#{@project_id}", @settings.clsi_cookie_expire_length_seconds).should.equal true + @redisMulti.expire.calledWith("clsiserver:#{@project_id}", @settings.clsiCookie.ttl).should.equal true done() it "should return the server id", (done)-> @@ -100,7 +101,7 @@ describe "ClsiCookieManager", -> it "should not set the server id if clsiCookies are not enabled", (done)-> - delete @settings.clsiCookieKey + delete @settings.clsiCookie.key @ClsiCookieManager = SandboxedModule.require modulePath, requires:@requires @ClsiCookieManager.setServerId @project_id, @response, (err, serverId)=> @redisMulti.exec.called.should.equal false @@ -113,13 +114,13 @@ describe "ClsiCookieManager", -> it "should return a jar with the cookie set populated from redis", (done)-> @ClsiCookieManager.getCookieJar @project_id, (err, jar)=> - jar._jar.store.idx["clsi.example.com"]["/"][@settings.clsiCookieKey].key.should.equal - jar._jar.store.idx["clsi.example.com"]["/"][@settings.clsiCookieKey].value.should.equal "clsi-11" + jar._jar.store.idx["clsi.example.com"]["/"][@settings.clsiCookie.key].key.should.equal + jar._jar.store.idx["clsi.example.com"]["/"][@settings.clsiCookie.key].value.should.equal "clsi-11" done() it "should return empty cookie jar if clsiCookies are not enabled", (done)-> - delete @settings.clsiCookieKey + delete @settings.clsiCookie.key @ClsiCookieManager = SandboxedModule.require modulePath, requires:@requires @ClsiCookieManager.getCookieJar @project_id, (err, jar)-> assert.deepEqual jar, realRequst.jar() From 9a6876d2379b3f7402495cb612591cd2a04eea22 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 20 May 2016 09:17:25 +0100 Subject: [PATCH 183/208] Toggle the dots at the end of bib preview, based on size of payload. --- services/web/app/views/project/editor/binary-file.jade | 2 +- .../ide/binary-files/controllers/BinaryFileController.coffee | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/binary-file.jade b/services/web/app/views/project/editor/binary-file.jade index 4f451f0020..eaaa6e6c65 100644 --- a/services/web/app/views/project/editor/binary-file.jade +++ b/services/web/app/views/project/editor/binary-file.jade @@ -28,7 +28,7 @@ div.binary-file.full-size( div.scroll-container p | {{ bibtexPreview.data }} - p + p(ng-show="bibtexPreview.shouldShowDots") | ... p.no-preview( diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index 124393d037..ef1fb77389 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -7,6 +7,7 @@ define [ $scope.bibtexPreview = loading: false + shouldShowDots: false error: false data: null @@ -31,11 +32,15 @@ define [ $scope.loadBibtexFilePreview = () -> url = "/project/#{project_id}/file/#{$scope.openFile.id}?range=0-#{TWO_MEGABYTES}" $scope.bibtexPreview.loading = true + $scope.bibtexPreview.shouldShowDots = false $scope.$apply() $http.get(url) .success (data) -> $scope.bibtexPreview.loading = false $scope.bibtexPreview.error = false + # show dots when payload is closs to cutoff + if data.length >= (TWO_MEGABYTES - 200) + $scope.bibtexPreview.shouldShowDots = true try # remove last partial line data = data.replace(/\n.*$/, '') From 939c7a8c720344b88af2332b04ca3f519e4f122a Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 20 May 2016 11:20:00 +0100 Subject: [PATCH 184/208] Force reload BinaryFile view when a new object is selected. --- .../ide/binary-files/BinaryFilesManager.coffee | 10 +++++++++- .../controllers/BinaryFileController.coffee | 14 +++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee b/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee index 9b80c0e0dc..02283bc49e 100644 --- a/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee +++ b/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee @@ -9,4 +9,12 @@ define [ openFile: (file) -> @$scope.ui.view = "file" - @$scope.openFile = file \ No newline at end of file + @$scope.openFile = null + @$scope.$apply() + window.setTimeout( + () => + @$scope.openFile = file + @$scope.$apply() + , 0 + , this + ) diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index ef1fb77389..7dceeff965 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -13,15 +13,6 @@ define [ $scope.failedLoad = false - $rootScope.$on 'entity:selected', () -> - $scope.failedLoad = false - $scope.loadBibtexIfRequired() - - $scope.loadBibtexIfRequired = () -> - if $scope.extension($scope.openFile) == 'bib' - $scope.bibtexPreview.data = null - $scope.loadBibtexFilePreview() - window.sl_binaryFilePreviewError = () => $scope.failedLoad = true $scope.$apply() @@ -61,6 +52,11 @@ define [ table_wrap.style.height = desired_height + 'px' table_wrap.style['max-height'] = desired_height + 'px' + $scope.loadBibtexIfRequired = () -> + if $scope.extension($scope.openFile) == 'bib' + $scope.bibtexPreview.data = null + $scope.loadBibtexFilePreview() + $scope.loadBibtexIfRequired() ] From eaa41e43614bc59f26323db2c5eabf1928155b35 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 20 May 2016 10:09:42 +0100 Subject: [PATCH 185/208] support displaying output files using .url parameter from clsi --- .../coffee/ide/pdf/controllers/PdfController.coffee | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 1b9407bc27..43725df381 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -76,12 +76,15 @@ define [ # prepare query string qs = {} # define the base url. if the pdf file has a build number, pass it to the clsi in the url - if fileByPath['output.pdf']?.build? + if fileByPath['output.pdf']?.url? + $scope.pdf.url = fileByPath['output.pdf'].url + else if fileByPath['output.pdf']?.build? build = fileByPath['output.pdf'].build $scope.pdf.url = "/project/#{$scope.project_id}/build/#{build}/output/output.pdf" - # no need to bust cache, build id is unique else $scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf" + # check if we need to bust cache (build id is unique so don't need it in that case) + if not fileByPath['output.pdf']?.build? qs = { cache_bust : "#{Date.now()}" } # add a query string parameter for the compile group if response.compileGroup? @@ -111,7 +114,9 @@ define [ fetchLogs = (logFile, blgFile) -> getFile = (name, file) -> - if file?.build? + if file.url? # FIXME clean this up when we have file.urls out consistently + url = file.url + else if file?.build? url = "/project/#{$scope.project_id}/build/#{file.build}/output/#{name}" else url = "/project/#{$scope.project_id}/output/#{name}" From c918028d14beac7343040460ed9fa947e6fd148e Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 20 May 2016 12:46:14 +0100 Subject: [PATCH 186/208] removed clsi priorty url --- .../Features/Compile/ClsiManager.coffee | 7 ++----- services/web/config/settings.defaults.coffee | 2 -- .../coffee/Compile/ClsiManagerTests.coffee | 20 ------------------- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 3285026cfa..ef347a17e0 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -53,13 +53,10 @@ module.exports = ClsiManager = _getCompilerUrl: (compileGroup) -> - if compileGroup == "priority" - return Settings.apis.clsi_priority.url - else - return Settings.apis.clsi.url + return Settings.apis.clsi.url _postToClsi: (project_id, req, compileGroup, callback = (error, response) ->) -> - compilerUrl = @_getCompilerUrl(compileGroup) + compilerUrl = Settings.apis.clsi.url opts = url: "#{compilerUrl}/project/#{project_id}/compile" json: req diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 026c451d0c..beb933e4c9 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -88,8 +88,6 @@ module.exports = url: "http://localhost:3009" clsi: url: "http://localhost:3013" - clsi_priority: - url: "http://localhost:3013" templates: url: "http://localhost:3007" githubSync: diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee index 89f312441d..9889bcb87b 100644 --- a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee @@ -101,14 +101,6 @@ describe "ClsiManager", -> it "should call the callback", -> @callback.called.should.equal true - describe "with the priority compileGroup", -> - beforeEach -> - @ClsiManager.deleteAuxFiles @project_id, {compileGroup: "priority"}, @callback - - it "should call the delete method in the CLSI", -> - @ClsiManager._makeRequest - .calledWith(@project_id, { method:"DELETE", url:"#{@settings.apis.clsi_priority.url}/project/#{@project_id}"}) - .should.equal true describe "_buildRequest", -> beforeEach -> @@ -264,18 +256,6 @@ describe "ClsiManager", -> it "should call the callback with the body and the error", -> @callback.calledWith(new Error("CLSI returned non-success code: 500"), @body).should.equal true - describe "when the compiler is priority", -> - beforeEach -> - @ClsiManager._makeRequest = sinon.stub() - @ClsiManager._postToClsi @project_id, @req, "priority", @callback - - it "should use the clsi_priority url", -> - url = "#{@settings.apis.clsi_priority.url}/project/#{@project_id}/compile" - @ClsiManager._makeRequest.calledWith(@project_id, { - method: "POST", - url: url - json: @req - }).should.equal true describe "wordCount", -> beforeEach -> From 2441930fe98339cd6f14fe3cd3d97eb5ecb4918e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 20 May 2016 14:28:51 +0100 Subject: [PATCH 187/208] Add a spinner when waiting for image preview to load. --- .../app/views/project/editor/binary-file.jade | 4 ++++ .../controllers/BinaryFileController.coffee | 10 +++++++++- services/web/public/img/spinner.gif | Bin 0 -> 14742 bytes .../stylesheets/app/editor/binary-file.less | 5 +++++ 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 services/web/public/img/spinner.gif diff --git a/services/web/app/views/project/editor/binary-file.jade b/services/web/app/views/project/editor/binary-file.jade index eaaa6e6c65..d0effce668 100644 --- a/services/web/app/views/project/editor/binary-file.jade +++ b/services/web/app/views/project/editor/binary-file.jade @@ -7,16 +7,20 @@ div.binary-file.full-size( ng-show="!failedLoad" ng-src="/project/{{ project_id }}/file/{{ openFile.id }}" ng-if="['png', 'jpg', 'jpeg', 'gif'].indexOf(extension(openFile)) > -1" + ng-class="{'img-preview': !imgLoaded}" onerror="sl_binaryFilePreviewError()" onabort="sl_binaryFilePreviewError()" + onload="sl_binaryFilePreviewLoaded()" ) img( ng-show="!failedLoad" ng-src="/project/{{ project_id }}/file/{{ openFile.id }}?format=png" ng-if="['pdf', 'eps'].indexOf(extension(openFile)) > -1" + ng-class="{'img-preview': !imgLoaded}" onerror="sl_binaryFilePreviewError()" onabort="sl_binaryFilePreviewError()" + onload="sl_binaryFilePreviewLoaded()" ) div(ng-if="(['bib'].indexOf(extension(openFile)) > -1) && !bibtexPreview.error") diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index 7dceeff965..b1457532b5 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -11,12 +11,20 @@ define [ error: false data: null + # Callback fired when the `img` tag fails to load, + # `failedLoad` used to show the "No Preview" message $scope.failedLoad = false - window.sl_binaryFilePreviewError = () => $scope.failedLoad = true $scope.$apply() + # Callback fired when the `img` tag is done loading, + # `imgLoaded` is used to show the spinner gif while loading + $scope.imgLoaded = false + window.sl_binaryFilePreviewLoaded = () => + $scope.imgLoaded = true + $scope.$apply() + $scope.extension = (file) -> return file.name.split(".").pop()?.toLowerCase() diff --git a/services/web/public/img/spinner.gif b/services/web/public/img/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..22220a221447ac15cb9690fe78375e17620c04dc GIT binary patch literal 14742 zcmd73dpy&P`#=8Ld2<}e`7kqQ38~O&a~vT_LMo&}l%(#IYG#;Yb3QZYIW}iGHOJ*nTW4u{j*+xzO(tF^VY;o)Hxi#0JZ@$TKb_V)Ibm6d^kf!5a6p`oGB&`=tU*5BXX z+1XiDRmEg784N~7Mn+0X3Y|_*NJz-f&(F=xO-f2CD=RB0DXFWgOG``3%E~G%EUc)g zsHv%GXlQ6`Y>bGAh>D5|3=E`DC?pc;-o1NNDm5l1#?R01=FOYFzP`6_-zJmE{{H?K zFJ8QL>(n2F4ezpZD|h_4mAb<@Z~*#-_hs-ZHr5Ny#ax zY3Ui6S=scQ+`RmP!Xid-NoiSm#Y1LgRdr2mU46r&#-`?$*2k>2_Kwc3?kDV?-oE~) z1J5{vL(hj_jEs(rPrQ6J`Fe^wJu^G^W`1FDY5DES>bv)AAJ#u^eER%l^Xs?oKmPo= z1weSDiA;K1Fmi`lKa8H!5r$T<$RJkcc14P7ll$0}vqm5>tZ@T|mDdv`b*v;Kg=I)e z25I0N%y+o;rs={OC|`#R%`;iI>lo(p-n!XB9oHkthO`nc@YR}_lbC; zAc$WIsoRxSI}0$~!fx)>rvt~zCAJ3V}?jIh9o+e^}lMnd&!#enN8$5mu*XzpW3omh+AxzkvFM&2FuEb>DdZ(*W3iO&nTy1S_2uZx{~;)WPKEuk z&u;F-ddmWUDk({8OE@!Ggh_v7Dxzi|4F=+fI6{rev8103ZOArwDNtEqT}p@Z0z9O8 z5MwkJV7BAl8lhZhK6NnPw=TxPvVHMzDS&7CsAQRxXJe&_E(t2pEVaVe_h^C!dNA=l z4*HV^P=dhxjPY9?0&6PQAn?*N}x_B6f=EN-)C4#8=?fyw$?s-mXtMd;N+ z`QOVEPMhoE0i7>jgP`j52Jef*Cel5g4Y^ABr;0atR=Z2EEi-sBV5R2t z#_d&Q6dn(5{_^ho`_>)4?iTRvkKFMiE2_yzh($9(a?E!BxiP!|K%Au{d=IXFv43?N zinrPQb8Bp!}##xXj_v@65Q@8nit zT|oRbsyF$nNzhw`20mP4Kj!kOw*u(yz{Dq!y5{&Ex2i`u8ht8Aq_bH*k+dNZa`oi+ zixUZ78pf@ThwNNQm5I&`LN;)2-V0>Utz6hw$Zu)7@{a|M}P(@)xXfBAStz%D<9Xy~5cw!wxC)%M5wuo4embJo2C6*_e9GDs7b z{78?idGSvV5H}E|6dBTd$#yQ<)N!=hv|Yik9C=Nwv`F>Bk_F1e&$`BsT7Jt=+{9j4 z>oRT*-}9A5T`z_B498^Vf0UpkTAX;|Peh1p%HB^@tr8Kj<#Ap= z@&vumOE<@Mr|?Yvrh*e3pYc3O2v{x6IQ$H>g*_e=;emFtZJ(`M;fMILFfHkJ; zEpGQZ8=wg1+=xb6x(-=&uwLK}w20}(&Yc3p@dUrXx8|pGWPU6@IlAL`%>9~(xHj9) z49r3I6Gv>19P}C~)b}j^1Ajnh_;Y7@!CehA;?q$piOQ)38a~Qk z2g6X^`t-d$CtMZMdk^O`qkT^MA2RG4e!8zxME}aMffW7>j$cE6wWizt6YA=?>o0zJEBFi^wr@!_(3{)l6_rKA+^R>@K zpgQG=<-AV%*M1M{>v%BeA428_G!6nffHQF6FGA-3hmb>s!~Y{8$HEFIk&M`q0^YFl z(yHp@%G|n|@P0pEu0(hPVmJ$X{2}38602JTUB4{v#-L)D8 z6G#fC@k%AiR;69bCmpl9qy2OdQ#@h}b}lpmK6&Rs4sAVlM5Y~GR))Ydh*X!6 z@~Oup$bNA=ojKh>6%^bwxNZa&*To*qvV5cy#rLV(=hpE@)eAkl4Wjzi9@Q)e&SKKf z?vjAdUV=FE#gzH5!pBQpqq=!hq_x6bRX4|4tFr6Vk#@FPGgx9fn(6^w5vw}Q0hh7Sg zwhn-lXEew{g?;N$QK?8G9&S8_Jd579I3~1TD^NL~3XWeJV+BBTm}HuPxTA%JP5UfJ zP))c}UN^!&4iJ#jN$E&l=_Fbn-ld{H1sz1`kf7LpF)*drKi3v_$KH?xK12~C#|J5L z+Yll+mp3@aZ#pcuYo^P5IbK&3mkLrYmMj&f+V(E(j<(`>de8s+MygZ8-*@DVS(7QOn^W+DWqyD zu6G;X{o%9VFqa&wA;2mUKw-RZomKZ?UF) zK^~k1{MctosU79>b!Zzai-aR0F6v10;GxZlj|XmXMnz_bMKBbNgWWhKo^d>WVjni- zZ7IL*0f?`Q;ft5Mp7|Vp9Rlu0W`h0nz&zfNi_^~Ng)s);P5tnF1(^rt$Q09`Lq8{A+2qo${lE11RPw- z+0ycK_G)lfOUO#j%2ej3H8S#R!k-D6>{1F#Iv<)4vlI-qIn!~!TJ(y}Ef20z_zRkA z`=a@XLgawy@xTq=?4J1ep=%nHwb&4PN$eqObuqYqs=yucWG*?6_bGcY=xiU;w7wq|-WkJeX& zf}vx=SfIXQ>apM6xffD70Jr1*qnoRd4cDCcFOP$LE%mRsR|4Xv;C!jf>8D}5&Z2!{ z8|Qd~QB5!SzG(&xQb6o1tERfuamw|#uP4jwgK|m@RH#9-L7=f_MST_5+(+T!lJJ`6 zY7uz8g|Q=g9*9pizzWw&g_ZV$Mqc9{b`4yp&;ef~{cjm#a1U)t&>1=lXx*(zOQ(WjSUB=Y?RDR^H}QJJ04f0EqTTqG`8wy*k9=lMDnOtv z5VBlZZXOQOz$%JV&QGF(WF1gh@I-e#YQZrRMJhD1kywnAkw(6T8Y)#?P~s870(_Z` zBu}XN#P}=dQ2{bKB5wFLv1CZ^5d-XJs>3=B<0C047xA>e#ef4jo=7PdKaD+oXG@HV zv=b!lYlBIgV!<%mS&@1&A7>_amtJFeVTcxINkANJT7dzla8#iUwvlR0An(&EX@w}N?m`YpNjv3N}zX$9;ExJzBWNSBG_bV8w>XsAQP`#!@~&!(e;-9R3+UzJJD#j0%km|DVn~ zPADUro|=`DUqH?)EM(*smuHk#B=HtAE9+uw>l>+$np%Tf9=Eo&H+FW_Jz-b$_SJ+} z4?GKt9x54m{B(>y!FrieJ2@3UGs_)$@c=Q`JT^G{ex9;a|0enKdgW&L#@Cz=Ka*BZ z7xB8_t^c%GJ-}W&*I|Q_voNmnbyFvc?(urXuFQM#0Amy(ZC;h%6R&cj#JQ)c;Ao`K zZYLI+)dnPr;TMO)<;kRYi0q2AB)9!o9B-%^OkN&DiWN@M+~vE+-LBX|ZnI3}1jfD? zByeyQiSIa7jO2^*5wpPuRUh5b)?iGOyv>L3ev@e6`C}niz)aiK?t)P$pNQNd57<{8 zKb0f7BB~zQ&U_;kgaA=V(;uu6^TG_KrBjEKA>{RK_+VQyiC5xmGgm8x01V|H>$G%f zyG}Y43xy*6@;8MPM3F%(t46Xycg(b#-s&>=+H6;}ojU7PRm4aPRAceSUf>1+zW*94 zCbJU*Qfid4=JN!|4kL79+~1XVhrYDB}4lHdsVyia+AbERND_r<4ulI}-p+2zJ-v^2)&H6jNmDv@l3S zW-1cs>ltl_T|B&5pa5ys!XWy_h}85Lj24E+vJT8p*w42~h#8SiBO;u1%pkEha~+A0 z^-R{H(vsDT$`K9!WKe;Eg`d~c4xQN7zT`XO#dv&wV=XX0}|7H`Xf7%)QFPr>- z+WCJ5Nhty)|1C($k;-kXZLVu=$SiAPM0Y-6Cn9?KpFU1vmGlkN3_j?7fErHdU_7Ty zrjF!}z3iN&Ow}&aR^r|~c$czXJUySbm_PBUV555NO9^bweYaRiU>IO($nKxQyYn3| z5vhHj(`B|}_y2!8E2@U<=5?T<3HV-!BzTdJEh=+ za9-2O(SkQngo$J72|=$M1$d5T;iqXNes2lHB8FV53YJqks;NR`k^Aq+#2Ij>I}ki? zc&JI1?W$PtMF|C3{zU9KqIe$V2V#P@;TVc$;h(mI7`(Gg_ z0EuvC=c}&MHU{RjOymRrN7$5>WkDKr($P-nOfM87#DXQR> z5;)0;#}E%+mB-JVilHqL{a#*k$=0}{gvCQvVa$1TNm1KNSRuag{%k6MC)^R|D{)C$ zWic3GGyu)IiA%A~!cmkA0YvX1X3_o>O{YTns&J)+JPqsIHg`T$)??hXKa?~JON zqUqf37OWeVH`3;^uE&Wi$; z0`WqDznoVT1P%__jzST8j55^uV(AayFi?R2kW!ok(afkY70Jm#NEXz=7$uE76)i+m z6%AZlSBGdUK^`ccy^Q~)LakP3hxW)Z>Fj{5vG zc3s5ZC-Ig9ffgxuCSD*BpYH@x(d;EgNxa?Hv}G-XF?fa~`#d~zMPcou(;@I-fp@{J zr|qHPqENJp_A|4S?^5egOqw){1RGCPMf0mQ4h0qZGe?!>Lzah-!9|+s|+ue|bBmQ$1o*Y#~D=rByD`!?FJ}j)M3ahLCm4(M3 zTU%x59c`4Zf2{X*7M}ZPD4hLbtaUgGG0qy#9VJgSz08?=lQdJs7d_8i`;h&%961@W zn*25M`})uBjh`ry%`5aachDh4?;E=(+roD8gf1ig%EJF;?ccqfDi`i4z=*oQwic1vN@;0`EogxBxX}x!2#9@ zBM&wC4nm!RfQX0ZAR>}n07OQDw*ih8_`ZFAR{Qb2NdPBqV~R+HUfq6Dou0N^vJKRB5R?8v-~)~h+B?K{+r z^AEL230!|i_3iK)3^2UB`o0ac#?kKzHGcB}maT)x=ZqsGpyE<`JN!q;4#0e}i;e)E zGk$Wh5bG^vfxh3lej`GQSNQ3>e*I$w0PUBg;{T5)LVK)|aUwqOU*ZFTPiGwon{9>?q73kaVQ99x^Nzsg$z5&5CAlH8@w zdsq&|VUn*+^VDLm3ZsKU-?Xb_DK!kvUOy+Njm%%!@6ze^{Kjb!QWHQ5LLxjaLoA2@ zS%mpE;ubv(9mJy)7te8Km*r5>ke$eZBm}BCSQ0mNwmm>$J{_)0Zyhm4Op?*w=j6__ zl-8(N9t?i}CLi>jAb&&Uy%rmgm@$W7yk>GL%@a>@Az=kK>dzX&9ndTa$ie27?$j9_ z0G++_1K~;p&LuebWjU5a5wT{cYk~t^B>05rm)J<0(i4!pSbmCI5FO2OJ*VohNrdiA zvoTbcStPn{9ZANYqC$?mqjey8Ej|-s6*x)xaUd;}xg}Z8lMM*!;xllWTVTqL|BB)FkCFL}Y)#xQmOm4?lkRk3l0e63Ag(0cu779VSd z&0UO3bQ$djPK^+QqIkUxsny>waUlBk<3)l->0?z-%zaqbfge>%XNP?phl2a1ksg$Jn^o*S7 ztknG6B1TSOaas9;(u&He#Om5QW(d8mDXyWpsIjc|aV;{XqqFB>Z+|lTDJOHVv#sy> zDD~MG?M24K%buwkZguI*TzTQ-;^T!CDk8SwU26CH=C|>mzYIiwZ`#XjpLliB(_wJ7_-+v8JA_)ZLV`rFG7g|A}aHNYWiv-6U zjX}!%8WKh9&m3twA$+jNCS1*xF04#rIOG_L+wAoid1#dApz8sYIp&<_6<)h7S{j$B zx`m*6r0;CE03kDm?izFwP7+|4#mPIyDClF=QhqI8E_wC|P2rrLFAI2;l(Oe?A%M48 zc$y%rOCM}E4SGhG;Rgl{LoZ9Hpy}4pNKn*hgJZtW_%!_8ln&nIrA#kASb}wbif%<@ zK}36kW?cjtOgH909*_pD>Dzu%HK-{3JDD$hFlV7q9(-Q&`bmfYI^~cb0rjQf9ENxV zxtqEriY|Jl0kI>2?^V+!s-M9W#$|*Wpi;+Jj&D4p zBW~M5?=}{f|1*_CqNR!{&74z+VFSGxA&mS-gl_NSwYHh!(mzZg_nYUJ2bOc>Mv4JQn+yu<5pxo`AMBG6(P6b#McH zqKhoPL#E6GEd;q&tW*aHcqVLaEtGe`vMzS0Co$Vt)@wXmDJLPz`>=0qM3GSn9woG< zAD=itCzi`tsm12_RI3~8OW5`;^2Zg7(lCLk^@{tjQ zVGM&8#`N(5_qJm;p0SQ&c$baXch*pFAX+X9{Kb$@k0~bTHs)^*7}%veYsCkc_qfIZ z$RCvpZF{b`>X6`XcZz4hgFYycAUM6T*C5tpjf@T8&CCvYrivy|D0*mCIMY>_3y}%J zZbQT2WDs~x%XcPP(%H^d9+lgUj~alv;6o|SzO%8Cw{p*KDIVp)vTk4`&2r^Mr33O~ z1owus1JrpqEh@Cm=}1iY()aPD9q zrh7N_0*hW+Z(G99%r^K)d#*(@(o9;LC2XeQ3G3=Y@Ay3#+l#TkC*yxE#=^t@Z|Xb| zR`ejMq_jMoH>~VoO>J6r9e=~4(5B}9x>RYe+zuE9p2doBo@as~kc8IZXfQ%nPZl*W zRLFPGXz?%)^ku|E3~FKVBVKfDHAC;?cO$|sA+Tla^_sF_68gs`>XH1F;R9zO6r3fy z1JYzAxQD+Vhkw!;t}YHW-nf#d5dTOdFhhK>Bowl@m-^g!|u0N3%gR`Cu8sB$~*O6w)Yjt21^_$<-JN|_B6 zmJehz_#&<@JZGG!A36}ecNQsU&G|JMt1p#xyn&07C!pf>!MqRo&+k7Ek_%NA%aT9! z-m)tBXc2$M;;W^xOMi%>4>kFT0|uAYTAi;<+ewk)1%I|*ea0(2{ICcV9Q@t0rCez2 z+2hAsKOvN&iC?=1`Y|CQod9wzUYFLA^7y5pJhaAxB5)W`h_nS)G88Jpnx=xp!=^D{ zV~l*b^kF>=Amz=Tp=u5pN-Ci+S_+Y0ZkUe5XpFEUX~+sBE=1B8!|jX|W8cDsz!-`B z(P82&R=hOUmlYR=E~X~u0ccQ?h|`*FaylKVq^uR^I1L`D9XhWY1JD(cVB)L`@^-@H zp^QSnWiCe8nYp0Yun{1fZe7J7Amr?4B6CapnX_P9*@9SYK-WMXf|9;HgMsQfG8hR= zSa?aIZUAcgY{u8si*(y?iP7L^d%5EBz8SZNkKpPn%udnE-byUl?|tRjTBjqbGZ{Rq zN@cAxeHC=ex8_y7H{Y(-7r7ju*TLex{~H8L{%UN!ZOH$7sV4aw@*^T61)_dKJ~bx! zKb>HerdItv!3qU~TPjf?1rW5YTu2SuuObd%7eo8{Uns*^g9STZOk&a6V!)%CLh!EF zuQhp|OwhraOG|q|oOfBe>+8GUd`TAkHb+SDb&=LT4oE^1lHDketpv_$SdO+E?lb1o zy&y9DY^d8DD%+p3_|zC0ET|09<6vaXFRJU*9Mw0+Q^a?dwlRxJXA42d^*8zWkHR1x zowG@J^IqjLh=jeRvy)vs38pA&Q1d<7%E^hMM zcUiCxErQcu${Qz0m&_SblhZfwv_uxo;%1&UC>ySk;p4@lK%hLwY$?`U}T*-?utdti*C zW5(e^D6Ej4yjDoRd5{mG;B-SpH1I%Be^Fjb6Z@>@;5frMz%Zzm41a|lqaQ!w2OOp; zS-Eu#8j1NWeq`C(*$`uQ%BNU0LPMqs4tCDQ-cl}X>r{Wjp-Hp@JBI^Z@R_}~rvMo} z#*6zDSg>TgEGK@kFgiq7Ah~k_Vgvfg;W{(oLTX==Juj zkrW{*96jpgMoK!x5loqEsQBPU`r zRfVplqZXd|ptYwcpNuwQWhX{>1_h-^>fnvwS&@rNRba+-5mzdyti7rfQ9gZ(hlX`sy$?fdN@HvH$oHL8Hc-k{u0h{W zH}{RxT_}%ZyWI(I%+@RQp*G$w5_;5iP&fb5j3m}ONAx!Q3S;G6&HKk>vCF24H%_s#(N;k4MK(Y13@k4sNn#a=4C+7r4M?pv}T+f=0}Hv6rr z|6yj-dYg4f;2zwbNa^aQ_^Y~?SRotDdwNW+u3ytW`ZmMA!$#9bx7-TBUxgd6Kf^bu zr||T8j>P4aPlNUf#G3=>v&-v-E}wEEja)v*X=}~h#x5_?2Ma>T9x=nUU;naD{ICg+l(gX#IsM)?JP(8gzpPsr5pC z50T{Jz%!Rfft2k9A>gA7X0Wh2qcXd~skEwIN4=;l1#=ozV`g!=xH^fNR#;o@_Oi4-Gqlg6 zw8iVR)$a^N;a7zHUyZHsyRi|nJLF^$(D1*`xf79C`E+?SGI{%)I~ksqk&Z^?m6nx? z6cu60t83!rm>CkaO>uxiYx0hU^v33HikL7GL2vKqdeRIR+3{kBWLt+kXRrn?Ju(7I z8fbVmSq6wtk4VHOykRaZmU35KNV4KS6n)&t{rv8(&ED{H75A%0CQD5T+ow~V=sE)? z$B9Co_+j3>L7$<(!JjX@V)x?V53+>`uarC?J;ng;mVc4ooJ#VxQC{0*$>$_Wn(5ur zS0z6|iOEjrx|0@hpkcM!NKYi=`ALyOXUiHB(AA~!;9K5Wq_=yRd6u^mTt=kFAW~Zb zn=4(c*|5SRO}5`AB1jZ~vvGA2PoVr}hog!T_X!BiEf}6wnH8!ZXk7sweYCLv zi)fbN7hh;OCLXSzSgYUT&Mz}BYOvO(a2{M}O-N}z9(;LJcoSPiin9rNmC3{2=l_-F zYmU6PrO9eS2a#6nc+>3D!EJ0gY^XoXKN#BM7if-%|>iM3{AtR3Zu|LR|Ua^ZT23=hoUrWk<$N0^FIZtD?{zxtIr@ zTJ#gdb>SeWsLjG$lJm`_H-D3idy?mTsz+h*e42ON(tP?~ja@oSTu46oy4WqNdaby~ z_@1AHiu}z`y~S*5uA@h8RPK(fsEdaUBJyz|X2mCxt^HK&y*1T@A}8Iw`lGT0Y`s*9 zLQ!gprJ>jomGsX`US-iEk|pI0^Cjx?H9yDSR#q{{j$8|sy18htC(wXHm> zsm04n8tVUj6-V4|CG*Va|Gn%gdrgi7-$3}#7^w3?N00gK@JPMu!E41eQld+h4Zma?K~0d9sH@q0s!7mv$NIkih)> zBv;zm2X?#?Uc^sVQx#FBYB3*UE#!LoPL0NA0BnoKAL zd>rKhJ2~%^e<_?JyhmpREjptsb?EL~&G4tf&?nS0JJc}wMPwVVaD%bZ(`eAnLqUt> z2_z2yLh%TY7NK84JLCc>2g3o!(zB*8eqbmb_HKKS4dv{5Mbws4Wh#~}?Upf?@80l! z<9vjB+3-y2O2|VUSX3C?BRG|4Ql`v%R`SpN=HQDb=RlfdzPbl+NQoig>L<2umIm1S z6w5Cc!)@0iFtwyYgOM&QY9$&S1d}3q5P##C@NXP*`ir22f8!X07l{;rg8#uWgaUs$ zzk(Q$y#2M5B*-?9DM0ee%FB2e455me+IVDT247us3%TJDe^U-XZ+#NXdeo65BdZ_~ z%Wmzh>`%aBbb*#fANl?$xsQ>9b*AxsMAM%i2dR zvb8J(?15LIZWw|s{OeAvi`#UaxF9e5fQl-~L`ePOg@Y9Ch-rARFk>^t)jd}r4yz<= zGo2R&k#IFU_|-Pc1g-h;!$Do(iJ7QCaA;frgJmY9m>A<`Ak=LpwUU1}*8s^1exUB} zb;clp6C@!g^-Ah;cqtTwn?IbZ#X7pOzg|ehsgrfn%Mk_f4m=oZr#?_8mB-_@CS(Zu z!$QGZ0oCsK~)H*9{`uvG0z;_g~r%Mo3OO2x~G&Q0f$Ywwz%s2mT%^h4fDXzHI& zNzVd6x9XqMzhs2>lUIa|nUdpV$&E#xC;MHDg65xBE7(@FG<|u`?T9?~llI+cW%haL zxo52{-@mNQ_GKS?-1_JD&5v)VewqC(06vx-FpP9S1+`*4xPN(~;L4n$VNdj5o(Rt1 z-itWSc9qvNcbE#5Nb;BoZ>8Q63&v@VVIy!sMc7??V*d3>0)Lm@`5$d2@VoS2F(Dx_ zFz|Qjli{MEbdV_g7lq}3w<|9S-lni}v7#ceZ3;`2VP?qGHZ@a(w)v~2qvkQIJqaNx z$?mL6?rwM*2Z$>wjqOCm4m1xkpN9*N%_;3iOj3Bur;4V-;M#BIWJ{L1SfKZzn2&RM zU%YDjOguaG-dFM+<7IofFwG$%n9hAd=BKGlf) zd_d-Wpj){1xgN;6k`sNLql5*?8eDW>?@8!0k}*i5+SJTf8bS2@%Q(ZuHAZ{}N%&K> z1^5ifY8!fWj;KoSkxG4s)dft^pltl{K%?> z@6yO~f>2Eo&KMsY*^%-Gbxhq{`{60pD+8LrHK-zp*Tem+%vcaKthm26XCTW(72fE_sT2 z_Ad-0j&v@#?K1j23i&Dkk<~v6|8tPZdQh$FI1G~4MFhd^4D=%aaXQrz$%9+GunSD2 z8zQlt=($u69**LF)@D?tlEvKk=I+Hj+Q|CF%=mAPp81nBc2Qx z!i$&uZDdQtPC5yf#z7I6i|DsRIc4cF; Date: Fri, 20 May 2016 16:34:45 +0100 Subject: [PATCH 188/208] clean up whitespace --- .../web/app/coffee/Features/Compile/CompileController.coffee | 5 ----- 1 file changed, 5 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index 13ce7f47ad..d961de554a 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -41,17 +41,12 @@ module.exports = CompileController = } downloadPdf: (req, res, next = (error) ->)-> - Metrics.inc "pdf-downloads" project_id = req.params.Project_id isPdfjsPartialDownload = req.query?.pdfng - - - rateLimit = (callback)-> if isPdfjsPartialDownload callback null, true - else rateLimitOpts = endpointName: "full-pdf-download" From 7f332c04360e152e3a024d0b4e3a61508a187e01 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 20 May 2016 16:31:16 +0100 Subject: [PATCH 189/208] support url object with query-string when proxying to clsi --- .../app/coffee/Features/Compile/CompileController.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index d961de554a..bca23d75d9 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -114,6 +114,9 @@ module.exports = CompileController = if err? logger.err err:err, "error getting cookie jar for clsi request" return callback(err) + # expand any url parameter passed in as {url:..., qs:...} + if typeof url is "object" + {url, qs} = url if limits.compileGroup == "priority" compilerUrl = Settings.apis.clsi_priority.url else @@ -123,9 +126,11 @@ module.exports = CompileController = oneMinute = 60 * 1000 # the base request options = { url: url, method: req.method, timeout: oneMinute, jar : jar } + # add any provided query string + options.qs = qs if qs? # if we have a build parameter, pass it through to the clsi if req.query?.pdfng && req.query?.build? # only for new pdf viewer - options.qs = {} + options.qs ?= {} options.qs.build = req.query.build # if we are byte serving pdfs, pass through If-* and Range headers # do not send any others, there's a proxying loop if Host: is passed! From a24f635531b4252d07b2e9f3583aa63aeb26eb5f Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Sat, 21 May 2016 10:23:17 +0100 Subject: [PATCH 190/208] removed uneeded passing of clsiServer to _parseOutputFiles and improve logging --- .../web/app/coffee/Features/Compile/ClsiManager.coffee | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index ef347a17e0..4a33a1a00c 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -19,12 +19,12 @@ module.exports = ClsiManager = if error? logger.err err:error, project_id:project_id, "error sending request to clsi" return callback(error) - logger.log project_id: project_id, response: response, "received compile response from CLSI" + logger.log project_id: project_id, outputFilesLength: response?.outputFiles?.length, status: response?.status, "received compile response from CLSI" ClsiCookieManager._getServerId project_id, (err, clsiServerId)-> if err? logger.err err:err, project_id:project_id, "error getting server id" return callback(err) - outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles, clsiServerId) + outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles) callback(null, response?.compile?.status, outputFiles, clsiServerId) deleteAuxFiles: (project_id, options, callback = (error) ->) -> @@ -72,11 +72,9 @@ module.exports = ClsiManager = logger.error err: error, project_id: project_id, "CLSI returned failure code" callback error, body - _parseOutputFiles: (project_id, rawOutputFiles = [], clsiServer) -> - # console.log rawOutputFiles + _parseOutputFiles: (project_id, rawOutputFiles = []) -> outputFiles = [] for file in rawOutputFiles - console.log path path = url.parse(file.url).path path = path.replace("/project/#{project_id}/output/", "") outputFiles.push From 887631e661e16e545cc6091bf291a7310c50fbc8 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 23 May 2016 14:58:28 +0100 Subject: [PATCH 191/208] Don't error if the rootDoc_id no longer exists in the project when copying --- .../app/coffee/Features/Project/ProjectDuplicator.coffee | 2 +- .../web/app/coffee/Features/Project/ProjectLocator.coffee | 8 +++++++- .../UnitTests/coffee/Project/ProjectLocatorTests.coffee | 7 +++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee index bf775caeeb..6cae962b04 100644 --- a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee @@ -75,7 +75,7 @@ module.exports = ProjectDuplicator = return callback(err) {originalProject, newProject, originalRootDoc, docContentsArray} = results - originalRootDoc = originalRootDoc[0] + originalRootDoc = originalRootDoc?[0] docContents = {} for docContent in docContentsArray diff --git a/services/web/app/coffee/Features/Project/ProjectLocator.coffee b/services/web/app/coffee/Features/Project/ProjectLocator.coffee index 36d9203ebe..9cd80d7c72 100644 --- a/services/web/app/coffee/Features/Project/ProjectLocator.coffee +++ b/services/web/app/coffee/Features/Project/ProjectLocator.coffee @@ -61,7 +61,13 @@ module.exports = ProjectLocator = findRootDoc : (opts, callback)-> getRootDoc = (project)=> if project.rootDoc_id? - @findElement {project:project, element_id:project.rootDoc_id, type:"docs"}, callback + @findElement {project:project, element_id:project.rootDoc_id, type:"docs"}, (error, args...) -> + if error? + if error instanceof Errors.NotFoundError + return callback null, null + else + return callback error + return callback null, args... else callback null, null {project, project_id} = opts diff --git a/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee index 15a86d235c..437c32b122 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee @@ -169,6 +169,13 @@ describe 'ProjectLocator', -> assert !err? expect(doc).to.equal null done() + + it 'should return null when the rootDoc_id no longer exists', (done) -> + project.rootDoc_id = "doesntexist" + @locator.findRootDoc project, (err, doc)-> + assert !err? + expect(doc).to.equal null + done() describe 'findElementByPath', -> From 35442f7ce93e9a18ee3339e228cfb3e7e974a4d7 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 23 May 2016 15:03:26 +0100 Subject: [PATCH 192/208] Prompt the user to open a file on the left if no doc is open --- services/web/app/views/project/editor/editor.jade | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index f79f6e3c19..d6c0876f96 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -10,8 +10,12 @@ div.full-size( ) .ui-layout-center .loading-panel(ng-show="!editor.sharejs_doc || editor.opening") - i.fa.fa-spin.fa-refresh - |   #{translate("loading")}... + span(ng-show="editor.open_doc_id") + i.fa.fa-spin.fa-refresh + |   #{translate("loading")}... + span(ng-show="!editor.open_doc_id") + i.fa.fa-arrow-left + |   #{translate("open_a_file_on_the_left")} #editor( ace-editor="editor", From 3452a9870e153a8726f87a6990f2a48f66f28d27 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 24 May 2016 15:10:55 +0100 Subject: [PATCH 193/208] Show logs when compile fails correctly --- .../coffee/ide/pdf/controllers/PdfController.coffee | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 4db21cd6f3..15c3e34664 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -48,6 +48,11 @@ define [ $scope.pdf.tooRecentlyCompiled = false $scope.pdf.renderingError = false + # make a cache to look up files by name + fileByPath = {} + for file in response.outputFiles + fileByPath[file.path] = file + if response.status == "timedout" $scope.pdf.view = 'errors' $scope.pdf.timedout = true @@ -60,7 +65,7 @@ define [ $scope.pdf.view = 'errors' $scope.pdf.failure = true $scope.shouldShowLogs = true - fetchLogs() + fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) else if response.status == 'clsi-maintenance' $scope.pdf.view = 'errors' $scope.pdf.clsiMaintenance = true @@ -71,10 +76,6 @@ define [ $scope.pdf.view = 'pdf' $scope.shouldShowLogs = false - # make a cache to look up files by name - fileByPath = {} - for file in response.outputFiles - fileByPath[file.path] = file # prepare query string qs = {} # define the base url. if the pdf file has a build number, pass it to the clsi in the url @@ -123,6 +124,7 @@ define [ fetchLogs = (logFile, blgFile) -> getFile = (name, file) -> + console.log "[getFile]", name, file opts = method:"GET" params: From 36ba88297bc48b6e367db62e7967b2c8947cfff3 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 24 May 2016 15:11:14 +0100 Subject: [PATCH 194/208] Don't hang request in the event of a spelling backend error --- .../web/app/coffee/Features/Spelling/SpellingController.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/app/coffee/Features/Spelling/SpellingController.coffee b/services/web/app/coffee/Features/Spelling/SpellingController.coffee index 86683e2341..973655701b 100644 --- a/services/web/app/coffee/Features/Spelling/SpellingController.coffee +++ b/services/web/app/coffee/Features/Spelling/SpellingController.coffee @@ -12,4 +12,5 @@ module.exports = SpellingController = request(url: Settings.apis.spelling.url + url, method: req.method, headers: req.headers, json: req.body, timeout:TEN_SECONDS) .on "error", (error) -> logger.error err: error, "Spelling API error" + res.status(500).end() .pipe(res) From a196531a58f50ce49a751194aeed0a33e8fc55b2 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 24 May 2016 15:12:58 +0100 Subject: [PATCH 195/208] Move spinner add logic into same code pathway as spinner remove logic --- .../coffee/ide/pdfng/directives/pdfViewer.coffee | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee index 1e6a2791d2..f0ec8d61b1 100644 --- a/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee +++ b/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee @@ -290,11 +290,18 @@ define [ rescaleTimer = null , 0 + spinnerTimer = null doRescale = (scale) -> # console.log 'doRescale', scale return unless scale? origposition = angular.copy scope.position # console.log 'origposition', origposition + + if not spinnerTimer? + spinnerTimer = setTimeout () -> + spinner.add(element) + spinnerTimer = null + , 100 layoutReady.promise.then (parentSize) -> [h, w] = parentSize # console.log 'in promise', h, w @@ -312,7 +319,6 @@ define [ scope.$emit 'pdf:error', error elementTimer = null - spinnerTimer = null updateLayout = () -> # if element is zero-sized keep checking until it is ready # console.log 'checking element ready', element.height(), element.width() @@ -329,11 +335,6 @@ define [ ] # console.log 'resolving layoutReady with', scope.parentSize $timeout () -> - if not spinnerTimer? - spinnerTimer = setTimeout () -> - spinner.add(element) - spinnerTimer = null - , 100 layoutReady.resolve scope.parentSize scope.$emit 'flash-controls' From f937f0bc5cd918bb2d6a98d60f3368cc233e540b Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 25 May 2016 10:38:40 +0100 Subject: [PATCH 196/208] Remove missed debugging console.log --- .../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 1 - 1 file changed, 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 15c3e34664..05d99124c7 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -124,7 +124,6 @@ define [ fetchLogs = (logFile, blgFile) -> getFile = (name, file) -> - console.log "[getFile]", name, file opts = method:"GET" params: From c7363a9df67c54bf0e01ada983ffbbb75de2ae68 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 20 May 2016 16:32:51 +0100 Subject: [PATCH 197/208] provide separate routes for sync-pdf and sync-code with error checking --- .../Features/Compile/CompileController.coffee | 27 ++++++++++++++++--- services/web/app/coffee/router.coffee | 4 +-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index bca23d75d9..b50c6ffd1a 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -9,7 +9,7 @@ AuthenticationController = require "../Authentication/AuthenticationController" UserGetter = require "../User/UserGetter" RateLimiter = require("../../infrastructure/RateLimiter") ClsiCookieManager = require("./ClsiCookieManager") - +Path = require("path") module.exports = CompileController = compile: (req, res, next = (error) ->) -> @@ -98,8 +98,29 @@ module.exports = CompileController = url = "/project/#{project_id}/output/#{req.params.file}" CompileController.proxyToClsi(project_id, url, req, res, next) - proxySync: (req, res, next = (error) ->) -> - CompileController.proxyToClsi(req.params.Project_id, req.url, req, res, next) + proxySyncPdf: (req, res, next = (error) ->) -> + project_id = req.params.Project_id + {page, h, v} = req.query + if not page?.match(/^\d+$/) + return next(new Error("invalid page parameter")) + if not h?.match(/^\d+\.\d+$/) + return next(new Error("invalid h parameter")) + if not v?.match(/^\d+\.\d+$/) + return next(new Error("invalid v parameter")) + destination = {url: "/project/#{project_id}/sync/pdf", qs: {page, h, v}} + CompileController.proxyToClsi(project_id, destination, req, res, next) + + proxySyncCode: (req, res, next = (error) ->) -> + project_id = req.params.Project_id + {file, line, column} = req.query + if not file? or Path.resolve("/", file) isnt "/#{file}" + return next(new Error("invalid file parameter")) + if not line?.match(/^\d+$/) + return next(new Error("invalid line parameter")) + if not column?.match(/^\d+$/) + return next(new Error("invalid column parameter")) + destination = {url:"/project/#{project_id}/sync/code", qs: {file, line, column}} + CompileController.proxyToClsi(project_id, destination, req, res, next) proxyToClsi: (project_id, url, req, res, next = (error) ->) -> if req.query?.compileGroup diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 709b2541e1..abdc5750e5 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -125,8 +125,8 @@ module.exports = class Router ), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi webRouter.delete "/project/:Project_id/output", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.deleteAuxFiles - webRouter.get "/project/:Project_id/sync/code", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySync - webRouter.get "/project/:Project_id/sync/pdf", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySync + webRouter.get "/project/:Project_id/sync/code", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySyncCode + webRouter.get "/project/:Project_id/sync/pdf", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySyncPdf webRouter.get "/project/:Project_id/wordcount", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.wordCount webRouter.delete '/Project/:Project_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.deleteProject From b55c469b91e6459a6036b44414123745d645a3ca Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 26 May 2016 16:24:18 +0100 Subject: [PATCH 198/208] use Url instead for url for "url" module to avoid possible conflicts with use of url as a variable --- services/web/app/coffee/Features/Compile/ClsiManager.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 4a33a1a00c..9af1602d2c 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -5,7 +5,7 @@ request = require('request') Project = require("../../models/Project").Project ProjectEntityHandler = require("../Project/ProjectEntityHandler") logger = require "logger-sharelatex" -url = require("url") +Url = require("url") ClsiCookieManager = require("./ClsiCookieManager") @@ -75,7 +75,7 @@ module.exports = ClsiManager = _parseOutputFiles: (project_id, rawOutputFiles = []) -> outputFiles = [] for file in rawOutputFiles - path = url.parse(file.url).path + path = Url.parse(file.url).path path = path.replace("/project/#{project_id}/output/", "") outputFiles.push path: path From 41f789a313105d4f06a54a2fa4e4e52718578591 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 26 May 2016 11:48:22 +0100 Subject: [PATCH 199/208] Add in metric to track client side error count --- services/web/app/coffee/router.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index abdc5750e5..80c52c5a24 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -249,6 +249,7 @@ module.exports = class Router webRouter.post '/error/client', (req, res, next) -> logger.warn err: req.body.error, meta: req.body.meta, "client side error" + metrics.inc("client-side-error") res.sendStatus(204) webRouter.get '*', ErrorController.notFound \ No newline at end of file From 0175a86d4c792396997fee38393facf9047d2c41 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 26 May 2016 13:54:34 +0100 Subject: [PATCH 200/208] Add in debugging console.logs which only display if ?debug=true is in URL --- services/web/public/coffee/base.coffee | 4 ++++ .../public/coffee/ide/connection/ConnectionManager.coffee | 3 +++ services/web/public/coffee/ide/editor/Document.coffee | 5 +++-- services/web/public/coffee/ide/editor/ShareJsDoc.coffee | 5 +++-- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/services/web/public/coffee/base.coffee b/services/web/public/coffee/base.coffee index 98cdb3a871..b62635170a 100644 --- a/services/web/public/coffee/base.coffee +++ b/services/web/public/coffee/base.coffee @@ -23,5 +23,9 @@ define [ baseUrl: window.sharelatex.sixpackDomain client_id: window.user_id }) + + sl_debugging = window.location?.search?.match(/debug=true/)? + window.sl_console = + log: (args...) -> console.log(args...) if sl_debugging return App diff --git a/services/web/public/coffee/ide/connection/ConnectionManager.coffee b/services/web/public/coffee/ide/connection/ConnectionManager.coffee index 7a52aa16de..4db3efdbe6 100644 --- a/services/web/public/coffee/ide/connection/ConnectionManager.coffee +++ b/services/web/public/coffee/ide/connection/ConnectionManager.coffee @@ -61,6 +61,8 @@ define [], () -> if @$scope.state.loading @$scope.state.load_progress = 70 + sl_console.log "[socket.io connect] Connected" + setTimeout(() => @joinProject() , 100) @@ -97,6 +99,7 @@ define [], () -> , 10 * 1000 joinProject: () -> + sl_console.log "[joinProject] joining..." @ide.socket.emit 'joinProject', { project_id: @ide.project_id }, (err, project, permissionsLevel, protocolVersion) => diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index e00573aef1..eb8843629c 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -172,14 +172,14 @@ define [ update: update if window.disconnectOnAck? and Math.random() < window.disconnectOnAck - console.log "Disconnecting on ack", update + sl_console.log "Disconnecting on ack", update window._ide.socket.socket.disconnect() # Pretend we never received the ack return if window.dropAcks? and Math.random() < window.dropAcks if !update.op? # Only drop our own acks, not collaborator updates - console.log "Simulating a lost ack", update + sl_console.log "Simulating a lost ack", update return if update?.doc == @doc_id and @doc? @@ -194,6 +194,7 @@ define [ @doc?.updateConnectionState "disconnected" _onReconnect: () -> + sl_console.log "[onReconnect] reconnected (joined project)" @ide.pushEvent "reconnected:afterJoinProject" @connected = true diff --git a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee index 61b9ffb6cf..176ceee51f 100644 --- a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee +++ b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee @@ -29,10 +29,10 @@ define [ send: (update) => @_startInflightOpTimeout(update) if window.disconnectOnUpdate? and Math.random() < window.disconnectOnUpdate - console.log "Disconnecting on update", update + sl_console.log "Disconnecting on update", update window._ide.socket.socket.disconnect() if window.dropUpdates? and Math.random() < window.dropUpdates - console.log "Simulating a lost update", update + sl_console.log "Simulating a lost update", update return @socket.emit "applyOtUpdate", @doc_id, update state: "ok" @@ -95,6 +95,7 @@ define [ @_doc.flush() updateConnectionState: (state) -> + sl_console.log "[updateConnectionState] Setting state to #{state}" @connection.state = state @connection.id = @socket.socket.sessionid @_doc.autoOpen = false From 39e57a988621b45232fb49c7a7477e009feed517 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 26 May 2016 13:55:22 +0100 Subject: [PATCH 201/208] Throw fatal error if document update actually returns an error from the real-time service --- services/web/public/coffee/ide/editor/ShareJsDoc.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee index 176ceee51f..ae07a8ca7a 100644 --- a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee +++ b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee @@ -34,7 +34,8 @@ define [ if window.dropUpdates? and Math.random() < window.dropUpdates sl_console.log "Simulating a lost update", update return - @socket.emit "applyOtUpdate", @doc_id, update + @socket.emit "applyOtUpdate", @doc_id, update, (error) => + return @_handleError(error) if error? state: "ok" id: @socket.socket.sessionid } From 6ea58d4d6c72225ca55acc09c9f8692e25dff1bb Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 26 May 2016 13:55:53 +0100 Subject: [PATCH 202/208] Don't try to resend an update if the editor is not joined to the project --- .../public/coffee/ide/editor/ShareJsDoc.coffee | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee index ae07a8ca7a..27d325676c 100644 --- a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee +++ b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee @@ -112,12 +112,14 @@ define [ detachFromAce: () -> @_doc.detach_ace?() INFLIGHT_OP_TIMEOUT: 5000 # Retry sending ops after 5 seconds without an ack + WAIT_FOR_CONNECTION_TIMEOUT: 500 # If we're waiting for the project to join, try again in 0.5 seconds _startInflightOpTimeout: (update) -> @_startFatalTimeoutTimer(update) - timer = setTimeout () => + retryOp = () => # Only send the update again if inflightOp is still populated # This can be cleared when hard reloading the document in which # case we don't want to keep trying to send it. + sl_console.log "[inflightOpTimeout] Trying op again" if @_doc.inflightOp? # When there is a socket.io disconnect, @_doc.inflightSubmittedIds # is updated with the socket.io client id of the current op in flight @@ -126,8 +128,18 @@ define [ # So we need both depending on whether the op was submitted before # one or more disconnects, or if it was submitted during the current session. update.dupIfSource = [@connection.id, @_doc.inflightSubmittedIds...] - @connection.send(update) - , @INFLIGHT_OP_TIMEOUT + + # We must be joined to a project for applyOtUpdate to work on the real-time + # service, so don't send an op if we're not. Connection state is set to 'ok' + # when we've joined the project + if @connection.state != "ok" + sl_console.log "[inflightOpTimeout] Not connected, retrying in 0.5s" + timer = setTimeout retryOp, @WAIT_FOR_CONNECTION_TIMEOUT + else + sl_console.log "[inflightOpTimeout] Sending" + @connection.send(update) + + timer = setTimeout retryOp, @INFLIGHT_OP_TIMEOUT @_doc.inflightCallbacks.push () => @_clearFatalTimeoutTimer() clearTimeout timer From 4bc31ae2b953b24a04cb39983861ecdb03523b3a Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 26 May 2016 17:31:21 +0100 Subject: [PATCH 203/208] Don't throw an error if we get an ack for our update more than once, since we may try sending it more than once --- .../coffee/ide/editor/sharejs/vendor/client/doc.coffee | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee b/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee index 4ac09ad4da..dd4f26ceb0 100644 --- a/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee +++ b/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee @@ -192,6 +192,13 @@ class Doc callback error for callback in @inflightCallbacks else # The op applied successfully. + + # We may get multiple acks of the same message if we retried it, + # so its ok if we receive an ack for a version that we've already gone past. + # If so, just ignore it + if msg.v < @version + return + throw new Error('Invalid version from server') unless msg.v == @version @serverOps[@version] = oldInflightOp From a605dae7bafe1f8a95e7c64dda832f0358e9fe92 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 27 May 2016 14:08:46 +0100 Subject: [PATCH 204/208] Don't show saving just because of pending ops When typing continuously, there is always a pending op present. The only time we're not saving is if the inflight op isn't changing. So long as this has changed, it means the previous one has been processed. --- services/web/public/coffee/ide/editor/Document.coffee | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index eb8843629c..4465d2e2fe 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -146,12 +146,15 @@ define [ if !inflightOp? and !pendingOp? # there's nothing going on saved = true - else if inflightOp == @oldInflightOp - saved = false - else if pendingOp? + sl_console.log "[pollSavedStatus] no inflight or pending ops" + else if inflightOp? and inflightOp == @oldInflightOp + # The same inflight op has been sitting unacked since we + # last checked. saved = false + sl_console.log "[pollSavedStatus] inflight op is same as before" else saved = true + sl_console.log "[pollSavedStatus] assuming saved (inflightOp?: #{inflightOp?}, pendingOp?: #{pendingOp?})" @oldInflightOp = inflightOp return saved From fec326672672f0ef7f0850c631e4c9184749254f Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 27 May 2016 14:13:53 +0100 Subject: [PATCH 205/208] Don't remove new instance of Document from openDocs when cleaning up old instance --- services/web/public/coffee/ide/editor/Document.coffee | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index 4465d2e2fe..01fa323da2 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -238,7 +238,14 @@ define [ callback(error) _cleanUp: () -> - delete Document.openDocs[@doc_id] + if Document.openDocs[@doc_id] == @ + sl_console.log "[_cleanUp] Removing self (#{@doc_id}) from in openDocs" + delete Document.openDocs[@doc_id] + else + # It's possible that this instance has error, and the doc has been reloaded. + # This creates a new instance in Document.openDoc with the same id. We shouldn't + # clear it because it's not use. + sl_console.log "[_cleanUp] New instance of (#{@doc_id}) created. Not removing" @_unBindFromEditorEvents() @_unBindFromSocketEvents() From 36c8c197e1a1b8a99ffb1168009d2bd21677042d Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 27 May 2016 14:14:08 +0100 Subject: [PATCH 206/208] Add more debug log lines --- services/web/public/coffee/ide/editor/Document.coffee | 9 +++++++++ .../web/public/coffee/ide/editor/EditorManager.coffee | 3 +++ 2 files changed, 12 insertions(+) diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index 01fa323da2..49e6822c7e 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -6,7 +6,10 @@ define [ @getDocument: (ide, doc_id) -> @openDocs ||= {} if !@openDocs[doc_id]? + sl_console.log "[getDocument] Creating new document instance for #{doc_id}" @openDocs[doc_id] = new Document(ide, doc_id) + else + sl_console.log "[getDocument] Returning existing document instance for #{doc_id}" return @openDocs[doc_id] @hasUnsavedChanges: () -> @@ -107,11 +110,14 @@ define [ @wantToBeJoined = false @_cancelJoin() if (@doc? and @doc.hasBufferedOps()) + sl_console.log "[leave] Doc has buffered ops, pushing callback for later" @_leaveCallbacks ||= [] @_leaveCallbacks.push callback else if !@connected + sl_console.log "[leave] Not connected, returning now" callback() else + sl_console.log "[leave] Leaving now" @_leaveDoc(callback) flush: () -> @@ -202,6 +208,7 @@ define [ @connected = true if @wantToBeJoined or @doc?.hasBufferedOps() + sl_console.log "[onReconnect] Rejoining (wantToBeJoined: #{@wantToBeJoined} OR hasBufferedOps: #{@doc?.hasBufferedOps()})" @_joinDoc (error) => return @_onError(error) if error? @doc.updateConnectionState "ok" @@ -229,10 +236,12 @@ define [ callback() _leaveDoc: (callback = (error) ->) -> + sl_console.log '[_leaveDoc] Sending leaveDoc request' @ide.socket.emit 'leaveDoc', @doc_id, (error) => return callback(error) if error? @joined = false for callback in @_leaveCallbacks or [] + sl_console.log '[_leaveDoc] Calling buffered callback', callback callback(error) delete @_leaveCallbacks callback(error) diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index e31955ef46..54d56cac19 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -41,6 +41,7 @@ define [ @openDoc(doc) openDoc: (doc, options = {}) -> + sl_console.log "[openDoc] Opening #{doc.id}" @$scope.ui.view = "editor" done = () => @@ -74,8 +75,10 @@ define [ done() _openNewDocument: (doc, callback = (error, sharejs_doc) ->) -> + sl_console.log "[_openNewDocument] Opening..." current_sharejs_doc = @$scope.editor.sharejs_doc if current_sharejs_doc? + sl_console.log "[_openNewDocument] Leaving existing open doc..." current_sharejs_doc.leaveAndCleanUp() @_unbindFromDocumentEvents(current_sharejs_doc) From 345d38c28d94857eae30c1598e16c50b26266593 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 27 May 2016 14:39:33 +0100 Subject: [PATCH 207/208] Don't clean up a documents event listener until after the socket has disconnected on error --- .../coffee/ide/connection/ConnectionManager.coffee | 4 ++-- services/web/public/coffee/ide/editor/Document.coffee | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/services/web/public/coffee/ide/connection/ConnectionManager.coffee b/services/web/public/coffee/ide/connection/ConnectionManager.coffee index 4db3efdbe6..1c6714f59a 100644 --- a/services/web/public/coffee/ide/connection/ConnectionManager.coffee +++ b/services/web/public/coffee/ide/connection/ConnectionManager.coffee @@ -52,6 +52,7 @@ define [], () -> "force new connection": true @ide.socket.on "connect", () => + sl_console.log "[socket.io connect] Connected" @connected = true @ide.pushEvent("connected") @@ -61,8 +62,6 @@ define [], () -> if @$scope.state.loading @$scope.state.load_progress = 70 - sl_console.log "[socket.io connect] Connected" - setTimeout(() => @joinProject() , 100) @@ -75,6 +74,7 @@ define [], () -> @ide.socket.on 'disconnect', () => + sl_console.log "[socket.io disconnect] Disconnected" @connected = false @ide.pushEvent("disconnected") diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index 49e6822c7e..78431b8b6b 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -198,6 +198,7 @@ define [ @leave() _onDisconnect: () -> + sl_console.log '[onDisconnect] disconnecting' @connected = false @joined = false @doc?.updateConnectionState "disconnected" @@ -253,7 +254,7 @@ define [ else # It's possible that this instance has error, and the doc has been reloaded. # This creates a new instance in Document.openDoc with the same id. We shouldn't - # clear it because it's not use. + # clear it because it's not this instance. sl_console.log "[_cleanUp] New instance of (#{@doc_id}) created. Not removing" @_unBindFromEditorEvents() @_unBindFromSocketEvents() @@ -296,5 +297,9 @@ define [ console.error "ShareJS error", error, meta ga?('send', 'event', 'error', "shareJsError", "#{error.message} - #{@ide.socket.socket.transport.name}" ) @doc?.clearInflightAndPendingOps() - @_cleanUp() @trigger "error", error, meta + # The clean up should run after the error is triggered because the error triggers a + # disconnect. If we run the clean up first, we remove our event handlers and miss + # the disconnect event, which means we try to leaveDoc when the connection comes back. + # This could intefere with the new connection of a new instance of this document. + @_cleanUp() From daf8f6f3cf76a9a34e3890ef631d35bb2d716935 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 31 May 2016 11:47:48 +0100 Subject: [PATCH 208/208] Refresh the page if the user is not logged in when joining a project --- .../coffee/ide/connection/ConnectionManager.coffee | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/services/web/public/coffee/ide/connection/ConnectionManager.coffee b/services/web/public/coffee/ide/connection/ConnectionManager.coffee index 1c6714f59a..f3d14a3e3d 100644 --- a/services/web/public/coffee/ide/connection/ConnectionManager.coffee +++ b/services/web/public/coffee/ide/connection/ConnectionManager.coffee @@ -103,6 +103,16 @@ define [], () -> @ide.socket.emit 'joinProject', { project_id: @ide.project_id }, (err, project, permissionsLevel, protocolVersion) => + if err? + if err.message == "not authorized" + window.location = "/login?redir=#{encodeURI(window.location.pathname)}" + else + @ide.socket.disconnect() + @ide.showGenericMessageModal("Something went wrong connecting", """ + Something went wrong connecting to your project. Please refresh is this continues to happen. + """) + return + if @$scope.protocolVersion? and @$scope.protocolVersion != protocolVersion location.reload(true)