Przeglądaj źródła

Merge branch 'master' into templatessearch

Henry Oswald 10 lat temu
rodzic
commit
cf45b8bd74
89 zmienionych plików z 2397 dodań i 1403 usunięć
  1. 1 0
      services/web/.gitignore
  2. 18 0
      services/web/Gruntfile.coffee
  3. 0 12
      services/web/app.coffee
  4. 6 29
      services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
  5. 0 16
      services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee
  6. 73 0
      services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee
  7. 111 0
      services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee
  8. 5 0
      services/web/app/coffee/Features/Authorization/PrivilegeLevels.coffee
  9. 4 0
      services/web/app/coffee/Features/Authorization/PublicAccessLevels.coffee
  10. 0 35
      services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
  11. 68 3
      services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
  12. 3 4
      services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
  13. 13 13
      services/web/app/coffee/Features/Compile/ClsiManager.coffee
  14. 2 1
      services/web/app/coffee/Features/Compile/CompileManager.coffee
  15. 9 5
      services/web/app/coffee/Features/Editor/EditorHttpController.coffee
  16. 9 9
      services/web/app/coffee/Features/Editor/EditorRouter.coffee
  17. 22 2
      services/web/app/coffee/Features/Errors/ErrorController.coffee
  18. 9 0
      services/web/app/coffee/Features/Errors/Errors.coffee
  19. 19 10
      services/web/app/coffee/Features/Project/ProjectController.coffee
  20. 4 9
      services/web/app/coffee/Features/Project/ProjectDeleter.coffee
  21. 2 1
      services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee
  22. 21 38
      services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
  23. 8 40
      services/web/app/coffee/Features/Project/ProjectGetter.coffee
  24. 3 1
      services/web/app/coffee/Features/Project/ProjectLocator.coffee
  25. 7 5
      services/web/app/coffee/Features/References/ReferencesHandler.coffee
  26. 0 38
      services/web/app/coffee/Features/Security/AuthorizationManager.coffee
  27. 2 6
      services/web/app/coffee/Features/Subscription/LimitationsManager.coffee
  28. 18 14
      services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
  29. 6 3
      services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee
  30. 8 10
      services/web/app/coffee/Features/Uploads/ArchiveManager.coffee
  31. 3 2
      services/web/app/coffee/Features/Uploads/UploadsRouter.coffee
  32. 0 4
      services/web/app/coffee/Features/User/UserInfoController.coffee
  33. 6 0
      services/web/app/coffee/infrastructure/ExpressLocals.coffee
  34. 4 0
      services/web/app/coffee/infrastructure/Server.coffee
  35. 0 194
      services/web/app/coffee/managers/SecurityManager.coffee
  36. 0 27
      services/web/app/coffee/models/Project.coffee
  37. 23 0
      services/web/app/coffee/models/ProjectInvite.coffee
  38. 0 26
      services/web/app/coffee/models/User.coffee
  39. 43 44
      services/web/app/coffee/router.coffee
  40. 30 0
      services/web/app/views/contact-us-modal.jade
  41. 21 0
      services/web/app/views/general/500.jade
  42. 6 11
      services/web/app/views/layout.jade
  43. 16 16
      services/web/app/views/project/editor/file-tree.jade
  44. 88 89
      services/web/app/views/project/editor/pdf.jade
  45. 4 2
      services/web/app/views/project/list/modals.jade
  46. 9 9
      services/web/app/views/project/list/project-list.jade
  47. 1 1
      services/web/config/settings.defaults.coffee
  48. 2 1
      services/web/package.json
  49. 3 5
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
  50. 159 0
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee
  51. 0 100
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/Snippets.coffee
  52. 1 2
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee
  53. 27 6
      services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
  54. 5 0
      services/web/public/coffee/ide/settings/services/settings.coffee
  55. 2 2
      services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
  56. 1 0
      services/web/public/coffee/libs.coffee
  57. 4 1
      services/web/public/coffee/main.coffee
  58. 59 0
      services/web/public/coffee/main/contact-us.coffee
  59. 3 3
      services/web/public/coffee/main/project-list/modal-controllers.coffee
  60. 0 26
      services/web/public/coffee/main/universties-site.coffee
  61. 1 0
      services/web/public/coffee/main/user-details.coffee
  62. BIN
      services/web/public/img/lion-sad-128.png
  63. 84 0
      services/web/public/js/libs/groove.js
  64. 6 0
      services/web/public/stylesheets/app/contact-us.less
  65. 1 0
      services/web/public/stylesheets/style.less
  66. 19 87
      services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
  67. 0 46
      services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee
  68. 385 0
      services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee
  69. 237 0
      services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee
  70. 0 111
      services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee
  71. 134 0
      services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee
  72. 4 4
      services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee
  73. 5 8
      services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee
  74. 20 7
      services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee
  75. 24 29
      services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee
  76. 5 6
      services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee
  77. 27 30
      services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee
  78. 14 52
      services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee
  79. 4 4
      services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee
  80. 26 22
      services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee
  81. 0 96
      services/web/test/UnitTests/coffee/Security/AuthorizationManagerTests.coffee
  82. 2 13
      services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee
  83. 7 5
      services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee
  84. 5 1
      services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee
  85. 7 7
      services/web/test/UnitTests/coffee/Uploads/ArchiveManagerTests.coffee
  86. 306 0
      services/web/test/acceptance/coffee/AuthorizationTests.coffee
  87. 21 0
      services/web/test/acceptance/coffee/ProjectCRUDTests.coffee
  88. 77 0
      services/web/test/acceptance/coffee/helpers/User.coffee
  89. 5 0
      services/web/test/acceptance/coffee/helpers/request.coffee

+ 1 - 0
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

+ 18 - 0
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,12 @@ 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:
+					timeout: 10000
+					reporter: grunt.option('reporter') or 'spec'
+					grep: grunt.option("grep")
 
 		"git-rev-parse":
 			version:
@@ -184,6 +199,7 @@ module.exports = (grunt) ->
 		            	]
 		            	"Test tasks": [
 		            		"test:unit"
+		            		"test:acceptance"
 		            	]
 		            	"Run tasks": [
 		            		"run"
@@ -290,6 +306,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 +314,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=<regex> or --feature=<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=<regex> or --feature=<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)

+ 0 - 12
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) ->

+ 6 - 29
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
 

+ 0 - 16
services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee

@@ -35,19 +35,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")

+ 73 - 0
services/web/app/coffee/Features/Authorization/AuthorizationManager.coffee

@@ -0,0 +1,73 @@
+CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
+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
+	# 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?
+					return callback new Errors.NotFoundError("no project found with id #{project_id}")
+				if project.publicAccesLevel == PublicAccessLevels.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
+					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 != PrivilegeLevels.NONE
+					# The user has direct access
+					callback null, privilegeLevel, false
+				else
+					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) ->
+			return callback(error) if error?
+			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 [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 == PrivilegeLevels.OWNER
+				return callback null, true
+			else if privilegeLevel == PrivilegeLevels.READ_AND_WRITE 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 == PrivilegeLevels.OWNER)
+	
+	isUserSiteAdmin: (user_id, callback = (error, isAdmin) ->) ->
+		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)

+ 111 - 0
services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee

@@ -0,0 +1,111 @@
+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) ->
+		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) ->
+		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) ->
+		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) ->
+		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) ->
+		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) ->
+		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"))
+		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)
+	
+	_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?
+			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'
+		

+ 5 - 0
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"

+ 4 - 0
services/web/app/coffee/Features/Authorization/PublicAccessLevels.coffee

@@ -0,0 +1,4 @@
+module.exports =
+	READ_ONLY: "readOnly"
+	READ_AND_WRITE: "readAndWrite"
+	PRIVATE: "private"

+ 0 - 35
services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee

@@ -7,15 +7,6 @@ UserGetter = require "../User/UserGetter"
 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) ->
-			return next(error) if error?
-			ProjectGetter.populateProjectWithUsers project, (error, project) ->
-				return next(error) if error?
-				CollaboratorsController._formatCollaborators project, (error, collaborators) ->
-					return next(error) if error?
-					res.send(JSON.stringify(collaborators))
-			
 	addUserToProject: (req, res, next) ->
 		project_id = req.params.Project_id
 		LimitationsManager.canAddXCollaborators project_id, 1, (error, allowed) =>
@@ -59,29 +50,3 @@ module.exports = CollaboratorsController =
 			EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id)
 			callback()
 
-	_formatCollaborators: (project, 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
-			}
-			
-		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
-				pushCollaborator(user, ["read", "write"], false)
-
-		if project.readOnly_refs? and project.readOnly_refs.length > 0
-			for user in project.readOnly_refs
-				pushCollaborator(user, ["read"], false)
-
-		callback null, collaborators
-			

+ 68 - 3
services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee

@@ -1,13 +1,77 @@
 UserCreator = require('../User/UserCreator')
 Project = require("../../models/Project").Project
-ProjectEntityHandler = require("../Project/ProjectEntityHandler")
 mimelib = require("mimelib")
 logger = require('logger-sharelatex')
 UserGetter = require "../User/UserGetter"
 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 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 []
+				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: PrivilegeLevels.READ_AND_WRITE }
+			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?
+			async.mapLimit members, 3,
+				(member, cb) ->
+					UserGetter.getUser member.id, (error, user) ->
+						return cb(error) if error?
+						return cb(null, { user: user, privilegeLevel: member.privilegeLevel })
+				callback
+	
+	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
+				if member.id == user_id?.toString()
+					return callback null, member.privilegeLevel
+			return callback null, PrivilegeLevels.NONE
+	
+	getMemberCount: (project_id, callback = (error, count) ->) ->
+		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 = []) ->
+			return callback(error) if error?
+			for member in members
+				if member.id.toString() == user_id.toString()
+					return callback null, true, member.privilegeLevel
+			return callback null, false, null
+			
+	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)=>
+				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"
 		conditions = _id:project_id
@@ -38,10 +102,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
@@ -57,6 +121,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"

+ 3 - 4
services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee

@@ -1,11 +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
-		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
+		webRouter.post   '/project/:Project_id/users', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.addUserToProject
+		webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject

+ 13 - 13
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) ->

+ 2 - 1
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?

+ 9 - 5
services/web/app/coffee/Features/Editor/EditorHttpController.coffee

@@ -5,14 +5,18 @@ 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")
+PrivilegeLevels = require "../Authorization/PrivilegeLevels"
 
 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) ->
@@ -29,17 +33,17 @@ 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?
-					AuthorizationManager.getPrivilegeLevelForProject project, user, (error, canAccess, privilegeLevel) ->
+					AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) ->
 						return callback(error) if error?
-						if !canAccess
+						if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
 							callback null, null, false
 						else
 							callback(null,
-								ProjectEditorHandler.buildProjectModelView(project),
+								ProjectEditorHandler.buildProjectModelView(project, members),
 								privilegeLevel
 							)
 

+ 9 - 9
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

+ 22 - 2
services/web/app/coffee/Features/Errors/ErrorController.coffee

@@ -1,5 +1,25 @@
+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"
+			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
+		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

+ 9 - 0
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

+ 19 - 10
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")
@@ -13,10 +12,12 @@ 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")
+ProjectGetter = require("./ProjectGetter")
+PrivilegeLevels = require("../Authorization/PrivilegeLevels")
 
 module.exports = ProjectController =
 
@@ -41,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
@@ -129,7 +138,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) ->
@@ -179,23 +188,23 @@ 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"
 
 		async.parallel {
 			project: (cb)->
-				Project.findPopulatedById project_id, 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)->
@@ -216,8 +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"
 
-			SecurityManager.userCanAccessProject user, project, (canAccess, privilegeLevel)->
-				if !canAccess
+			AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel)->
+				return next(error) if error?
+				if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
 					return res.sendStatus 401
 
 				if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt?
@@ -228,7 +238,6 @@ module.exports = ProjectController =
 					title:  project.name
 					priority_title: true
 					bodyClasses: ["editor"]
-					project : project
 					project_id : project._id
 					user : {
 						id    : user.id

+ 4 - 9
services/web/app/coffee/Features/Project/ProjectDeleter.coffee

@@ -5,6 +5,7 @@ documentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
 tagsHandler = require("../Tags/TagsHandler")
 async = require("async")
 FileStoreHandler = require("../FileStore/FileStoreHandler")
+CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
 
 module.exports = ProjectDeleter =
 
@@ -44,16 +45,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)->

+ 2 - 1
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()

+ 21 - 38
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,27 @@ module.exports = ProjectEditorHandler =
 			spellCheckLanguage: project.spellCheckLanguage
 			deletedByExternalDataSource : project.deletedByExternalDataSource || false
 			deletedDocs: project.deletedDocs
+			members:     []
+		
+		owner = null
+		for member in members
+			if member.privilegeLevel == "owner"
+				owner = member.user
+			else
+				result.members.push @buildUserModelView member.user, member.privilegeLevel
+		if owner?
+			result.owner = @buildUserModelView owner, "owner"
+
+		result.features = _.defaults(owner?.features or {}, {
+			collaborators: -1 # Infinite
+			versioning: false
+			dropbox:false
+			compileTimeout: 60
+			compileGroup:"standard"
+			templates: false
+			references: false
+		})
 
-		if options.includeUsers
-			result.features =
-				collaborators: -1 # Infinite
-				versioning: false
-				dropbox:false
-				compileTimeout: 60
-				compileGroup:"standard"
-				templates: false
-				references: false
-
-			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
-
-					
-			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) ->

+ 8 - 40
services/web/app/coffee/Features/Project/ProjectGetter.coffee

@@ -2,11 +2,10 @@ mongojs = require("../../infrastructure/mongojs")
 db = mongojs.db
 ObjectId = mongojs.ObjectId
 async = require "async"
+Project = require("../../models/Project").Project
 Errors = require("../../errors")
 logger = require("logger-sharelatex")
 
-
-
 module.exports = ProjectGetter =
 	EXCLUDE_DEPTH: 8
 
@@ -51,42 +50,11 @@ module.exports = ProjectGetter =
 				return callback(err)
 			callback(null, project?[0])
 
-	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) ->
+	
+	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.getProjectsUserIsCollaboratorOf user_id, fields, (error, readAndWriteProjects, readOnlyProjects) ->
 				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
+				callback null, projects, readAndWriteProjects, readOnlyProjects

+ 3 - 1
services/web/app/coffee/Features/Project/ProjectLocator.coffee

@@ -4,6 +4,7 @@ Errors = require "../../errors"
 _ = require('underscore')
 logger = require('logger-sharelatex')
 async = require('async')
+ProjectGetter = require "./ProjectGetter"
 
 module.exports = ProjectLocator =
 	findElement: (options, _callback = (err, element, path, parentFolder)->)->
@@ -131,7 +132,8 @@ module.exports = ProjectLocator =
 			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)->

+ 7 - 5
services/web/app/coffee/Features/References/ReferencesHandler.coffee

@@ -1,7 +1,8 @@
 logger = require("logger-sharelatex")
 request = require("request")
 settings = require("settings-sharelatex")
-Project = require("../../models/Project").Project
+ProjectGetter = require "../Project/ProjectGetter"
+UserGetter = require "../User/UserGetter"
 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)
+		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.findPopulatedById 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)
@@ -44,7 +46,7 @@ module.exports = ReferencesHandler =
 			ReferencesHandler._doIndexOperation(projectId, project, docIds, callback)
 
 	index: (projectId, docIds, callback=(err, data)->) ->
-		Project.findPopulatedById 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)

+ 0 - 38
services/web/app/coffee/Features/Security/AuthorizationManager.coffee

@@ -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
-		
-

+ 2 - 6
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

+ 18 - 14
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")
 SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder')
@@ -31,7 +31,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)->
@@ -80,12 +80,12 @@ 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)
 				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"		
@@ -109,15 +109,19 @@ module.exports = SubscriptionController =
 
 
 	userCustomSubscriptionPage: (req, res, next)->
-		SecurityManager.getCurrentUser req, (error, user) ->
+		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
 
 
 	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
@@ -138,7 +142,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
@@ -150,14 +154,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)->
@@ -166,7 +170,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"
@@ -176,7 +180,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)->
@@ -195,7 +199,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
@@ -212,7 +216,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"
@@ -225,7 +229,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?

+ 6 - 3
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)
@@ -122,9 +123,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)

+ 8 - 10
services/web/app/coffee/Features/Uploads/ArchiveManager.coffee

@@ -25,33 +25,31 @@ 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)
 
-		unzip.on "exit", () ->
+		unzip.on "close", (exitCode) ->
 			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()
 			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, exitCode:exitCode, "error getting bytes of zip"
+				return callback(new Error("error getting bytes of zip"))
 
 			isTooLarge = totalSizeInBytes > (ONE_MEG * 300)
 
 			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)

+ 3 - 2
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,7 @@ module.exports =
 				maxRequests: 200
 				timeInterval: 60 * 30
 			}),
-			SecurityManager.requestCanModifyProject,
+			AuthenticationController.requireLogin(),
+			AuthorizationMiddlewear.ensureUserCanWriteProjectContent,
 			ProjectUploadController.uploadFile
 

+ 0 - 4
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, {

+ 6 - 0
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"

+ 4 - 0
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

+ 0 - 194
services/web/app/coffee/managers/SecurityManager.coffee

@@ -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+''
-
-

+ 0 - 27
services/web/app/coffee/models/Project.coffee

@@ -43,33 +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])
-
-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)
-
-
-
-
-
 applyToAllFilesRecursivly = ProjectSchema.statics.applyToAllFilesRecursivly = (folder, fun)->
 	_.each folder.fileRefs, (file)->
 		fun(file)

+ 23 - 0
services/web/app/coffee/models/ProjectInvite.coffee

@@ -0,0 +1,23 @@
+mongoose = require 'mongoose'
+Settings = require 'settings-sharelatex'
+
+Schema = mongoose.Schema
+ObjectId = Schema.ObjectId
+
+ProjectInviteSchema = new Schema
+	project_id:     ObjectId
+	from_user_id:   ObjectId
+	privilegeLevel: String
+	# For existing users
+	to_user_id:     ObjectId
+	# For non-existant users
+	hashed_token:   String
+	email:          String
+
+conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10)
+
+ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema)
+
+mongoose.model 'ProjectInvite', ProjectInviteSchema
+exports.ProjectInvite = ProjectInvite
+exports.ProjectInviteSchema = ProjectInviteSchema

+ 0 - 26
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)

+ 43 - 44
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
@@ -88,8 +87,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
@@ -100,12 +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
+		}), 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
@@ -222,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")
 
@@ -233,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

+ 30 - 0
services/web/app/views/contact-us-modal.jade

@@ -0,0 +1,30 @@
+script(type='text/ng-template', id='supportModalTemplate')
+	.modal-header
+		button.close(
+			type="button"
+			data-dismiss="modal"
+			ng-click="close()"
+		) &times;
+		h3 Contact Us
+	.modal-body.contact-us-modal
+		span(ng-show="sent == false")
+			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",  ng-init="form.email = '#{getUserEmail()}'", 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.

+ 21 - 0
services/web/app/views/general/500.jade

@@ -0,0 +1,21 @@
+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

+ 6 - 11
services/web/app/views/layout.jade

@@ -25,6 +25,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 +61,8 @@ 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()}';
 
 		- if (typeof(settings.algolia) != "undefined")
 			script.
@@ -80,6 +81,7 @@ html(itemscope, itemtype='http://schema.org/Product')
 				}
 				
 	body
+
 		- if(typeof(suppressSystemMessages) == "undefined")
 			.system-messages(
 				ng-cloak
@@ -118,21 +120,14 @@ 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"
-				};
+		include contact-us-modal
 
 
 

+ 16 - 16
services/web/app/views/project/editor/file-tree.jade

@@ -70,23 +70,23 @@ 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",
+				ng-show="ui.view == 'track-changes'"
+			)
+				.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(

+ 88 - 89
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"
 		)
@@ -74,95 +74,10 @@ 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")
-		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")
-		| &nbsp;
-		i.fa.fa-level-up.fa-flip-horizontal.fa-2x
-		| &nbsp;&nbsp;#{translate('click_here_to_preview_pdf')}
-
-	.pdf-errors(ng-show="pdf.timedout || pdf.error")
-		.alert.alert-danger(ng-show="pdf.error")
-			strong #{translate("server_error")} 
-			span #{translate("somthing_went_wrong_compiling")}
-		
-
-		.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 &nbsp;
-						| #{translate("unlimited_projects")}
-					
-					li
-						i.fa.fa-check &nbsp;
-						| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
-					
-					li
-						i.fa.fa-check &nbsp;
-						| #{translate("full_doc_history")}
-					
-					li
-						i.fa.fa-check &nbsp;
-						| #{translate("sync_to_dropbox")}
-
-					li
-						i.fa.fa-check &nbsp;
-						| #{translate("sync_to_github")}
-
-					li
-						i.fa.fa-check &nbsp;
-						|#{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")
+	// logs view
+	.pdf-logs(ng-show="shouldShowLogs")
 		.alert.alert-success(ng-show="pdf.logEntries.all.length == 0")
 			| #{translate("no_errors_good_job")}
 
@@ -214,6 +129,90 @@ 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")
+	
+	
+	// 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")
+			| &nbsp;
+			i.fa.fa-level-up.fa-flip-horizontal.fa-2x
+			| &nbsp;&nbsp;#{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 &nbsp;
+							| #{translate("unlimited_projects")}
+						li
+							i.fa.fa-check &nbsp;
+							| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
+						li
+							i.fa.fa-check &nbsp;
+							| #{translate("full_doc_history")}
+						li
+							i.fa.fa-check &nbsp;
+							| #{translate("sync_to_dropbox")}
+						li
+							i.fa.fa-check &nbsp;
+							| #{translate("sync_to_github")}
+						li
+							i.fa.fa-check &nbsp;
+							|#{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")}
+	// // // //
+
+
 
 script(type='text/ng-template', id='clearCacheModalTemplate')
 	.modal-header

+ 4 - 2
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()"
 		) &times;
-		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")}
@@ -201,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

+ 9 - 9
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",

+ 1 - 1
services/web/config/settings.defaults.coffee

@@ -259,7 +259,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"] == 'true' then true else false
 	
 	# Maximum size of text documents in the real-time editing system.
 	max_doc_length: 2 * 1024 * 1024 # 2mb

+ 2 - 1
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",

+ 3 - 5
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee

@@ -1,9 +1,9 @@
 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, 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 =

+ 159 - 0
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SnippetManager.coffee

@@ -0,0 +1,159 @@
+define () ->
+	environments = [
+		"abstract",
+		"align", "align*",
+		"equation", "equation*",
+		"gather", "gather*",
+		"multline", "multline*",
+		"split",
+		"verbatim"
+	]
+
+	staticSnippets = for env in environments
+		{
+			caption: "\\begin{#{env}}..."
+			snippet: """
+				\\begin{#{env}}
+				\t$1
+				\\end{#{env}}
+			"""
+			meta: "env"
+		}
+
+	staticSnippets = staticSnippets.concat [{
+		caption: "\\begin{array}..."
+		snippet: """
+			\\begin{array}{${1:cc}}
+			\t$2 & $3 \\\\\\\\
+			\t$4 & $5
+			\\end{array}
+		"""
+		meta: "env"
+	}, {
+		caption: "\\begin{figure}..."
+		snippet: """
+			\\begin{figure}
+			\t\\centering
+			\t\\includegraphics{$1}
+			\t\\caption{${2:Caption}}
+			\t\\label{${3:fig:my_label}}
+			\\end{figure}
+		"""
+		meta: "env"
+	}, {
+		caption: "\\begin{tabular}..."
+		snippet: """
+			\\begin{tabular}{${1:c|c}}
+			\t$2 & $3 \\\\\\\\
+			\t$4 & $5
+			\\end{tabular}
+		"""
+		meta: "env"
+	}, {
+		caption: "\\begin{table}..."
+		snippet: """
+			\\begin{table}[$1]
+			\t\\centering
+			\t\\begin{tabular}{${2:c|c}}
+			\t\t$3 & $4 \\\\\\\\
+			\t\t$5 & $6
+			\t\\end{tabular}
+			\t\\caption{${7:Caption}}
+			\t\\label{${8:tab:my_label}}
+			\\end{table}
+		"""
+		meta: "env"
+	}, {
+		caption: "\\begin{list}..."
+		snippet: """
+			\\begin{list}
+			\t\\item $1
+			\\end{list}
+		"""
+		meta: "env"
+	}, {
+		caption: "\\begin{enumerate}..."
+		snippet: """
+			\\begin{enumerate}
+			\t\\item $1
+			\\end{enumerate}
+		"""
+		meta: "env"
+	}, {
+		caption: "\\begin{itemize}..."
+		snippet: """
+			\\begin{itemize}
+			\t\\item $1
+			\\end{itemize}
+		"""
+		meta: "env"
+	}, {
+		caption: "\\begin{frame}..."
+		snippet: """
+			\\begin{frame}{${1:Frame Title}}
+			\t$2
+			\\end{frame}
+		"""
+		meta: "env"
+	}]
+
+
+	parseCustomEnvironments = (text) ->
+		re = /^\\newenvironment{(\w+)}.*$/gm
+		result = []
+		iterations = 0
+		while match = re.exec(text)
+			result.push {name: match[1], whitespace: null}
+			iterations += 1
+			if iterations >= 1000
+				return result
+		return result
+
+
+	parseBeginCommands = (text) ->
+		re = /^\\begin{(\w+)}.*\n([\t ]*).*$/gm
+		result = []
+		iterations = 0
+		while match = re.exec(text)
+			result.push {name: match[1], whitespace: match[2]}
+			iterations += 1
+			if iterations >= 1000
+				return result
+		return result
+
+	class SnippetManager
+		getCompletions: (editor, session, pos, prefix, callback) ->
+			docText = session.getValue()
+			customEnvironments = parseCustomEnvironments(docText)
+			beginCommands = parseBeginCommands(docText)
+			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) ->
+					{
+						caption: "\\begin{#{item.name}}..."
+						snippet: """
+							\\begin{#{item.name}}
+							#{item.whitespace || ''}$0
+							\\end{#{item.name}}
+						"""
+						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*
+				parsedItems.map (item) ->
+					{
+						caption: "\\end{#{item.name}}"
+						value: "\\end{#{item.name}}"
+						meta: "env"
+					}
+			)
+			callback null, snippets
+
+	return SnippetManager

+ 0 - 100
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/Snippets.coffee

@@ -1,100 +0,0 @@
-define () ->
-	environments = [
-		"abstract", 
-		"align", "align*",
-		"equation", "equation*",
-		"gather", "gather*",
-		"multline", "multline*",
-		"split",
-		"verbatim"
-	]
-
-	snippets = for env in environments
-		{
-			caption: "\\begin{#{env}}..."
-			snippet: """
-				\\begin{#{env}}
-				\t$1
-				\\end{#{env}}
-			"""
-			meta: "env"
-		}
-
-	snippets = snippets.concat [{
-		caption: "\\begin{array}..."
-		snippet: """
-			\\begin{array}{${1:cc}}
-			\t$2 & $3 \\\\\\\\
-			\t$4 & $5
-			\\end{array}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{figure}..."
-		snippet: """
-			\\begin{figure}
-			\t\\centering
-			\t\\includegraphics{$1}
-			\t\\caption{${2:Caption}}
-			\t\\label{${3:fig:my_label}}
-			\\end{figure}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{tabular}..."
-		snippet: """
-			\\begin{tabular}{${1:c|c}}
-			\t$2 & $3 \\\\\\\\
-			\t$4 & $5
-			\\end{tabular}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{table}..."
-		snippet: """
-			\\begin{table}[$1]
-			\t\\centering
-			\t\\begin{tabular}{${2:c|c}}
-			\t\t$3 & $4 \\\\\\\\
-			\t\t$5 & $6
-			\t\\end{tabular}
-			\t\\caption{${7:Caption}}
-			\t\\label{${8:tab:my_label}}
-			\\end{table}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{list}..."
-		snippet: """
-			\\begin{list}
-			\t\\item $1
-			\\end{list}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{enumerate}..."
-		snippet: """
-			\\begin{enumerate}
-			\t\\item $1
-			\\end{enumerate}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{itemize}..."
-		snippet: """
-			\\begin{itemize}
-			\t\\item $1
-			\\end{itemize}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{frame}..."
-		snippet: """
-			\\begin{frame}{${1:Frame Title}}
-			\t$2
-			\\end{frame}
-		"""
-		meta: "env"
-	}]
-
-	return snippets

+ 1 - 2
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
 				}
-

+ 27 - 6
services/web/public/coffee/ide/pdf/controllers/PdfController.coffee

@@ -4,7 +4,13 @@ define [
 	"libs/bib-log-parser"
 ], (App, LogParser, BibLogParser) ->
 	App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, localStorage) ->
+
 		autoCompile = true
+
+		# pdf.view = uncompiled | pdf | errors
+		$scope.pdf.view = if $scope?.pdf?.url then 'pdf' else 'uncompiled'
+		$scope.shouldShowLogs = false
+
 		$scope.$on "project:joined", () ->
 			return if !autoCompile
 			autoCompile = false
@@ -12,7 +18,8 @@ define [
 			$scope.hasPremiumCompile = $scope.project.features.compileGroup == "priority"
 
 		$scope.$on "pdf:error:display", () ->
-			$scope.pdf.error = true
+			$scope.pdf.view = 'errors'
+			$scope.pdf.renderingError = true
 
 		$scope.draft = localStorage("draft:#{$scope.project_id}") or false
 		$scope.$watch "draft", (new_value, old_value) ->
@@ -37,17 +44,32 @@ define [
 			$scope.pdf.uncompiled = false
 			$scope.pdf.projectTooLarge = false
 			$scope.pdf.url        = null
+			$scope.pdf.clsiMaintenance = false
+			$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
+				$scope.shouldShowLogs = 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'
+				$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
@@ -116,7 +138,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 () ->
@@ -158,6 +180,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
@@ -171,13 +194,11 @@ define [
 			}
 
 		$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

+ 5 - 0
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
+				
 		}
 	]

+ 2 - 2
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 = () ->

+ 1 - 0
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"
 ], () ->

+ 4 - 1
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"
@@ -30,3 +30,6 @@ define [
 	"__MAIN_CLIENTSIDE_INCLUDES__"
 ], () ->
 	angular.bootstrap(document.body, ["SharelatexApp"])
+
+	
+

+ 59 - 0
services/web/public/coffee/main/contact-us.coffee

@@ -0,0 +1,59 @@
+define [
+	"base"
+], (App) ->
+
+
+	App.controller 'ContactModal', ($scope, $modal) ->
+		$scope.contactUsModal = () ->
+			modalInstance = $modal.open(
+				templateUrl: "supportModalTemplate"
+				controller: "SupportModalController"
+			)
+
+	App.controller 'SupportModalController', ($scope, $modalInstance) ->
+		$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 or ""
+				subject: $scope.form.subject + " - [#{ticketNumber}]"
+				about : $scope.form.project_url or ""
+				labels: "support"
+
+			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} accounts"
+				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()

+ 3 - 3
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()

+ 0 - 26
services/web/public/coffee/main/universties-site.coffee

@@ -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()

+ 1 - 0
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 = ->

BIN
services/web/public/img/lion-sad-128.png


+ 84 - 0
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"});
+

+ 6 - 0
services/web/public/stylesheets/app/contact-us.less

@@ -0,0 +1,6 @@
+.contact-us-modal {
+
+	textarea {
+		height: 120px;
+	}
+}

+ 1 - 0
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";

+ 19 - 87
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
+				@req.session =
+					user: @user = {
+						_id: "user-id-123"
+						email: "user@sharelatex.com"
+					}
+				@middleware(@req, @res, @next)
 
-			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 set the user property on the request", ->
+				@req.user.should.deep.equal @user
 
-				it "should redirect to the register page", ->
-					@AuthenticationController._redirectToLoginOrRegisterPage.calledWith(@req, @res).should.equal true
+			it "should call the next method in the chain", ->
+				@next.called.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", ->
+		describe "when the user is not logged in", ->
 			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 = {}
+				@AuthenticationController._redirectToLoginOrRegisterPage = sinon.stub()
+				@req.query = {}
 				@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 redirect to the register or login page", ->
+				@AuthenticationController._redirectToLoginOrRegisterPage.calledWith(@req, @res).should.equal true
 
 	describe "requireGlobalLogin", ->
 		beforeEach ->

+ 0 - 46
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
-			
-
-

+ 385 - 0
services/web/test/UnitTests/coffee/Authorization/AuthorizationManagerTests.coffee

@@ -0,0 +1,385 @@
+sinon = require('sinon')
+chai = require('chai')
+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 ->
+		@AuthorizationManager = SandboxedModule.require modulePath, requires:
+			"../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()
+
+	describe "getPrivilegeLevelForProject", ->
+		beforeEach ->
+			@Project.findOne = sinon.stub()
+			@AuthorizationManager.isUserSiteAdmin = 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 ->
+					@AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false)
+					@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 ->
+					@AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false)
+					@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 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
+				
+				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
+
+		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 ->
+					@AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false)
+					@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 ->
+					@AuthorizationManager.isUserSiteAdmin.withArgs(@user_id).yields(null, false)
+					@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 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
+				
+				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
+		
+		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 ->
+			@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()

+ 237 - 0
services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee

@@ -0,0 +1,237 @@
+sinon = require('sinon')
+chai = require('chai')
+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 = {
+		"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 "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 ->
+			@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

+ 0 - 111
services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee

@@ -24,35 +24,6 @@ describe "CollaboratorsController", ->
 		@project_id = "project-id-123"
 		@callback = sinon.stub()
 
-	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)
-			@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)
-
 	describe "addUserToProject", ->
 		beforeEach ->
 			@req.params =
@@ -179,85 +150,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
-				}
-					
-			
-
-		
-				

+ 134 - 0
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,12 +17,145 @@ describe "CollaboratorsHandler", ->
 			"../../models/Project": Project: @Project = {}
 			"../Project/ProjectEntityHandler": @ProjectEntityHandler = {}
 			"./CollaboratorsEmailHandler": @CollaboratorsEmailHandler = {}
+			"../Errors/Errors": Errors
 
 		@project_id = "mock-project-id"
 		@user_id = "mock-user-id"
 		@adding_user_id = "adding-user-id"
 		@email = "joe@sharelatex.com"
 		@callback = sinon.stub()
+	
+	describe "getMemberIdsWithPrivilegeLevels", ->
+		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
+		
+		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 ->
+			@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()
+			@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 "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()
+
+		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 "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.getProjectsUserIsCollaboratorOf @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 ->

+ 4 - 4
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) ->

+ 5 - 8
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)->

+ 20 - 7
services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee

@@ -10,12 +10,13 @@ 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() }
 			"./EditorController": @EditorController = {}
 			'../../infrastructure/Metrics': @Metrics = {inc: sinon.stub()}
+			"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {}
 			
 		@project_id = "mock-project-id"
 		@doc_id = "mock-doc-id"
@@ -76,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 ->
@@ -85,19 +97,20 @@ 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", ->
 			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", ->
@@ -105,8 +118,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
 			
@@ -117,7 +130,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", ->
@@ -126,7 +139,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", ->

+ 24 - 29
services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee

@@ -35,13 +35,10 @@ describe "ProjectController", ->
 			getAllTags: sinon.stub()
 		@NotificationsHandler =
 			getUserNotifications: sinon.stub()
-		@ProjectModel =
-			findAllUsersProjects: sinon.stub()
-			findPopulatedById: sinon.stub()
 		@UserModel =
 			findById: sinon.stub()
-		@SecurityManager =
-			userCanAccessProject:sinon.stub()
+		@AuthorizationManager =
+			getPrivilegeLevelForProject:sinon.stub()
 		@EditorController = 
 			renameProject:sinon.stub()
 		@InactiveProjectManager =
@@ -50,6 +47,9 @@ describe "ProjectController", ->
 			markAsOpened: sinon.stub()
 		@ReferencesSearchHandler =
 			indexProjectReferences: sinon.stub()
+		@ProjectGetter =
+			findAllUsersProjects: sinon.stub()
+			getProject: sinon.stub()
 		@ProjectController = SandboxedModule.require modulePath, requires:
 			"settings-sharelatex":@settings
 			"logger-sharelatex": 
@@ -67,12 +67,12 @@ describe "ProjectController", ->
 			"../Subscription/LimitationsManager": @LimitationsManager
 			"../Tags/TagsHandler":@TagsHandler
 			"../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
+			"./ProjectGetter": @ProjectGetter
 
 		@user = 
 			_id:"!£123213kjljkl"
@@ -128,18 +128,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 =
@@ -151,6 +139,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)->
@@ -224,7 +225,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)=>
@@ -297,10 +298,10 @@ describe "ProjectController", ->
 					fontSize:"massive"
 					theme:"sexy"
 				email: "bob@bob.com"
-			@ProjectModel.findPopulatedById.callsArgWith 1, null, @project
+			@ProjectGetter.getProject.callsArgWith 2, null, @project
 			@UserModel.findById.callsArgWith(1, null, @user)
 			@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {})
-			@SecurityManager.userCanAccessProject.callsArgWith 2, true, "owner"
+			@AuthorizationManager.getPrivilegeLevelForProject.callsArgWith 2, null, "owner"
 			@ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub()
 			@InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1)
 			@ProjectUpdateHandler.markAsOpened.callsArgWith(1)
@@ -312,12 +313,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
@@ -339,7 +334,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, null
 			@res.sendStatus = (resCode, opts)=>
 				resCode.should.equal 401
 				done()

+ 5 - 6
services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee

@@ -33,6 +33,7 @@ describe 'ProjectDeleter', ->
 			'../DocumentUpdater/DocumentUpdaterHandler': @documentUpdaterHandler
 			"../Tags/TagsHandler":@TagsHandler
 			"../FileStore/FileStoreHandler": @FileStoreHandler = {}
+			"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {}
 			"./ProjectGetter": @ProjectGetter
 			'logger-sharelatex':
 				log:->
@@ -92,6 +93,8 @@ describe 'ProjectDeleter', ->
 
 	describe "archiveProject", ->
 		beforeEach ->
+			@CollaboratorsHandler.getMemberIds = sinon.stub()
+			@CollaboratorsHandler.getMemberIds.withArgs(@project_id).yields(null, ["member-id-1", "member-id-2"])
 			@ProjectGetter.getProject.callsArgWith(2, null, @project)
 			@Project.update.callsArgWith(2)
 
@@ -111,12 +114,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", ->

+ 27 - 30
services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee

@@ -38,33 +38,41 @@ describe "ProjectEditorHandler", ->
 					folders  : []
 				}]
 			}]
-			owner_ref :
+			deletedDocs: [{
+				_id: "deleted-doc-id"
+				name: "main.tex"
+			}]
+		@members = [{
+			user: @owner = {
 				_id: "owner-id"
 				first_name : "Owner"
 				last_name  : "ShareLaTeX"
 				email      : "owner@sharelatex.com"
-			readOnly_refs: [{
+			},
+			privilegeLevel: "owner"
+		},{
+			user: {
 				_id: "read-only-id"
 				first_name : "Read"
 				last_name  : "Only"
 				email      : "read-only@sharelatex.com"
-			}]
-			collaberator_refs: [{
+			},
+			privilegeLevel: "readOnly"
+		},{
+			user: {
 				_id: "read-write-id"
 				first_name : "Read"
 				last_name  : "Write"
 				email      : "read-write@sharelatex.com"
-			}]
-			deletedDocs: [{
-				_id: "deleted-doc-id"
-				name: "main.tex"
-			}]
+			},
+			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
-			

+ 14 - 52
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 = {}
 			"logger-sharelatex":
 				err:->
 				log:->
@@ -134,56 +136,16 @@ describe "ProjectGetter", ->
 			@db.projects.find = sinon.stub().callsArgWith(2, null, [@project])
 
 
-		it "should call find with the project id when string id is passed", (done)->
-			@ProjectGetter.getProject @project_id, (err, project)=>
-				@db.projects.find.calledWith(_id: ObjectId(@project_id)).should.equal true
-				assert.deepEqual @project, project
-				done()
-
-		it "should call find with the project id when object id is passed", (done)->
-			@ProjectGetter.getProject ObjectId(@project_id), (err, project)=>
-				@db.projects.find.calledWith(_id: ObjectId(@project_id)).should.equal true
-				assert.deepEqual @project, project
-				done()
-
-		it "should call the db when a mongoose objectid is used", (done)->
-			mongooseID = require('mongoose').Types.ObjectId(@project_id)
-			@ProjectGetter.getProject mongooseID, (err, project)=>
-				@db.projects.find.calledWith(_id: ObjectId(@project_id)).should.equal true
-				assert.deepEqual @project, project
-				done()
-
-	describe "populateProjectWithUsers", ->
+	describe "findAllUsersProjects", ->
 		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
-					
+			@fields = {"mock": "fields"}
+			@Project.find = sinon.stub()
+			@Project.find.withArgs({owner_ref: @user_id}, @fields).yields(null, ["mock-owned-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", ->
+			@callback
+				.calledWith(null, ["mock-owned-projects"], ["mock-rw-projects"], ["mock-ro-projects"])
+				.should.equal true

+ 4 - 4
services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee

@@ -30,7 +30,7 @@ project.rootFolder[0] = rootFolder
 project.rootDoc_id = rootDoc._id
 
 
-describe 'ProjectLocatorTests', ->
+describe 'ProjectLocator', ->
 
 	beforeEach ->
 		Project.getProject = (project_id, fields, callback)=>
@@ -301,7 +301,7 @@ describe 'ProjectLocatorTests', ->
 			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()
@@ -310,7 +310,7 @@ describe 'ProjectLocatorTests', ->
 			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()
@@ -319,7 +319,7 @@ describe 'ProjectLocatorTests', ->
 			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()

+ 26 - 22
services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee

@@ -39,10 +39,11 @@ describe 'ReferencesHandler', ->
 				get: sinon.stub()
 				post: sinon.stub()
 			}
-			'../../models/Project': {
-				Project: @Project = {
-					findPopulatedById: sinon.stub().callsArgWith(1, null, @fakeProject)
-				}
+			'../Project/ProjectGetter': @ProjectGetter = {
+				getProject: sinon.stub().callsArgWith(2, null, @fakeProject)
+			}
+			'../User/UserGetter': @UserGetter = {
+				getUser: sinon.stub()
 			}
 			'../DocumentUpdater/DocumentUpdaterHandler': @DocumentUpdaterHandler = {
 				flushDocToMongo: sinon.stub().callsArgWith(2, null)
@@ -70,10 +71,10 @@ describe 'ReferencesHandler', ->
 					@handler._findBibDocIds.callCount.should.equal 0
 					done()
 
-			it 'should call Project.findPopulatedById', (done) ->
+			it 'should call ProjectGetter.getProject', (done) ->
 				@call (err, data) =>
-					@Project.findPopulatedById.callCount.should.equal 1
-					@Project.findPopulatedById.calledWith(@projectId).should.equal true
+					@ProjectGetter.getProject.callCount.should.equal 1
+					@ProjectGetter.getProject.calledWith(@projectId).should.equal true
 					done()
 
 			it 'should not call _findBibDocIds', (done) ->
@@ -109,10 +110,10 @@ describe 'ReferencesHandler', ->
 					expect(data).to.equal @fakeResponseData
 					done()
 
-		describe 'when Project.findPopulatedById produces an error', ->
+		describe 'when ProjectGetter.getProject produces an error', ->
 
 			beforeEach ->
-				@Project.findPopulatedById.callsArgWith(1, new Error('woops'))
+				@ProjectGetter.getProject.callsArgWith(2, new Error('woops'))
 
 			it 'should produce an error', (done) ->
 				@call (err, data) =>
@@ -129,7 +130,7 @@ describe 'ReferencesHandler', ->
 		describe 'when _isFullIndex produces an error', ->
 
 			beforeEach ->
-				@Project.findPopulatedById.callsArgWith(1, null, @fakeProject)
+				@ProjectGetter.getProject.callsArgWith(2, null, @fakeProject)
 				@handler._isFullIndex.callsArgWith(1, new Error('woops'))
 
 			it 'should produce an error', (done) ->
@@ -147,7 +148,7 @@ describe 'ReferencesHandler', ->
 		describe 'when flushDocToMongo produces an error', ->
 
 			beforeEach ->
-				@Project.findPopulatedById.callsArgWith(1, null, @fakeProject)
+				@ProjectGetter.getProject.callsArgWith(2, null, @fakeProject)
 				@handler._isFullIndex.callsArgWith(1, false)
 				@DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops'))
 
@@ -167,7 +168,7 @@ describe 'ReferencesHandler', ->
 		describe 'when request produces an error', ->
 
 			beforeEach ->
-				@Project.findPopulatedById.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'))
@@ -182,7 +183,7 @@ describe 'ReferencesHandler', ->
 		describe 'when request responds with error status', ->
 
 			beforeEach ->
-				@Project.findPopulatedById.callsArgWith(1, null, @fakeProject)
+				@ProjectGetter.getProject.callsArgWith(2, null, @fakeProject)
 				@handler._isFullIndex.callsArgWith(1, null, false)
 				@request.post.callsArgWith(1, null, {statusCode: 500}, null)
 
@@ -234,10 +235,10 @@ describe 'ReferencesHandler', ->
 				expect(data).to.equal @fakeResponseData
 				done()
 
-		describe 'when Project.findPopulatedById produces an error', ->
+		describe 'when ProjectGetter.getProject produces an error', ->
 
 			beforeEach ->
-				@Project.findPopulatedById.callsArgWith(1, new Error('woops'))
+				@ProjectGetter.getProject.callsArgWith(2, new Error('woops'))
 
 			it 'should produce an error', (done) ->
 				@call (err, data) =>
@@ -254,7 +255,7 @@ describe 'ReferencesHandler', ->
 		describe 'when _isFullIndex produces an error', ->
 
 			beforeEach ->
-				@Project.findPopulatedById.callsArgWith(1, null, @fakeProject)
+				@ProjectGetter.getProject.callsArgWith(2, null, @fakeProject)
 				@handler._isFullIndex.callsArgWith(1, new Error('woops'))
 
 			it 'should produce an error', (done) ->
@@ -272,7 +273,7 @@ describe 'ReferencesHandler', ->
 		describe 'when flushDocToMongo produces an error', ->
 
 			beforeEach ->
-				@Project.findPopulatedById.callsArgWith(1, null, @fakeProject)
+				@ProjectGetter.getProject.callsArgWith(2, null, @fakeProject)
 				@handler._isFullIndex.callsArgWith(1, false)
 				@DocumentUpdaterHandler.flushDocToMongo.callsArgWith(2, new Error('woops'))
 
@@ -312,16 +313,19 @@ describe 'ReferencesHandler', ->
 
 		beforeEach ->
 			@fakeProject =
-				owner_ref:
-					features:
-						references: false
+				owner_ref: @owner_ref = "owner-ref-123"
+			@owner =
+				features:
+					references: false
+			@UserGetter.getUser = sinon.stub()
+			@UserGetter.getUser.withArgs(@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 +335,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) =>

+ 0 - 96
services/web/test/UnitTests/coffee/Security/AuthorizationManagerTests.coffee

@@ -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()
-			

+ 2 - 13
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)

+ 7 - 5
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,14 +61,16 @@ describe "SubscriptionController sanboxed", ->
 		@SubscriptionDomainHandler = 
 			getDomainLicencePage:sinon.stub()	
 		@SubscriptionController = SandboxedModule.require modulePath, requires:
-			'../../managers/SecurityManager': @SecurityManager
+			'../Authentication/AuthenticationController': @AuthenticationController
 			'./SubscriptionHandler': @SubscriptionHandler
 			"./PlansLocator": @PlansLocator
 			'./SubscriptionViewModelBuilder': @SubscriptionViewModelBuilder
 			"./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)->

+ 5 - 1
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
 			"../../infrastructure/Metrics":
 				inc:-> 
 

+ 7 - 7
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 ->

+ 306 - 0
services/web/test/acceptance/coffee/AuthorizationTests.coffee

@@ -0,0 +1,306 @@
+expect = require("chai").expect
+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 [
+		(cb) ->
+			user.request.get "/project/#{project_id}", (error, response, body) ->
+				return cb(error) if error?
+				test(response, body)
+				cb()
+		(cb) ->
+			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 = (user, project_id, test, callback) ->
+	async.series [
+		(cb) ->
+			user.request.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 = (user, project_id, test, callback) ->
+	async.series [
+		(cb) ->
+			user.request.post {
+				uri: "/project/#{project_id}/rename"
+				json:
+					newProjectName: "new-name"
+			}, (error, response, body) ->
+				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) ->
+	# 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}
+		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_admin_access = (user, project_id, callback) ->
+	try_admin_access(user, project_id, (response, body) ->
+		expect(response.statusCode).to.be.oneOf [200, 204]
+	, callback)
+
+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_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.match new RegExp(options.redirect_to)
+	, callback)
+
+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.match new RegExp(options.redirect_to)
+	, callback)
+
+describe "Authorization", ->
+	before (done) ->
+		@timeout(10000)
+		@owner = new User()
+		@other1 = new User()
+		@other2 = new User()
+		@anon = new User()
+		@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", ->
+		before (done) ->
+			@owner.createProject "private-project", (error, project_id) =>
+				return done(error) if error?
+				@project_id = project_id
+				done()
+				
+		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
+		
+		it "should allow the owner admin access to it", (done) ->
+			expect_admin_access @owner, @project_id, 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 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
+			
+		it "should not allow another user admin access to it", (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 @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) ->
+			@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, @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, @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, @project_id, redirect_to: "/restricted", done
+
+		it "should allow the read-write user read access to it", (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, @project_id, done
+
+		it "should not allow the read-write user admin access to it", (done) ->
+			expect_no_admin_access @rw_user, @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, @project_id, done
+			
+		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 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 allow an anonymous user write access to its content", (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
+
+	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

+ 21 - 0
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()
+		

+ 77 - 0
services/web/test/acceptance/coffee/helpers/User.coffee

@@ -0,0 +1,77 @@
+request = require("./request")
+settings = require("settings-sharelatex")
+{db, ObjectId} = 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()
+	
+	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",
+			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

+ 5 - 0
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
+})