Просмотр исходного кода

Merge branch 'master' into angular_1.3.15

James Allen 11 лет назад
Родитель
Сommit
aa06eb55b7
83 измененных файлов с 1150 добавлено и 552 удалено
  1. 1 1
      services/web/Gruntfile.coffee
  2. 0 3
      services/web/app.coffee
  3. 5 0
      services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee
  4. 4 1
      services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
  5. 2 1
      services/web/app/coffee/Features/Blog/BlogController.coffee
  6. 2 2
      services/web/app/coffee/Features/Chat/ChatController.coffee
  7. 2 3
      services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
  8. 5 5
      services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
  9. 2 2
      services/web/app/coffee/Features/Compile/CompileController.coffee
  10. 28 0
      services/web/app/coffee/Features/Docstore/DocstoreManager.coffee
  11. 1 3
      services/web/app/coffee/Features/Documents/DocumentController.coffee
  12. 7 7
      services/web/app/coffee/Features/Editor/EditorHttpController.coffee
  13. 10 11
      services/web/app/coffee/Features/Editor/EditorRouter.coffee
  14. 22 4
      services/web/app/coffee/Features/FileStore/FileStoreController.coffee
  15. 2 2
      services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee
  16. 25 0
      services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee
  17. 56 0
      services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee
  18. 14 8
      services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee
  19. 5 5
      services/web/app/coffee/Features/PasswordReset/PasswordResetRouter.coffee
  20. 1 2
      services/web/app/coffee/Features/Project/ProjectApiController.coffee
  21. 26 12
      services/web/app/coffee/Features/Project/ProjectController.coffee
  22. 0 1
      services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee
  23. 4 1
      services/web/app/coffee/Features/Project/ProjectLocator.coffee
  24. 6 2
      services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee
  25. 22 0
      services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee
  26. 2 2
      services/web/app/coffee/Features/RealTimeProxy/RealTimeProxyRouter.coffee
  27. 6 6
      services/web/app/coffee/Features/ServerAdmin/AdminController.coffee
  28. 1 1
      services/web/app/coffee/Features/Spelling/SpellingController.coffee
  29. 12 12
      services/web/app/coffee/Features/StaticPages/StaticPagesRouter.coffee
  30. 2 0
      services/web/app/coffee/Features/StaticPages/UniversityController.coffee
  31. 6 6
      services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
  32. 3 3
      services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee
  33. 3 1
      services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee
  34. 22 23
      services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee
  35. 6 10
      services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee
  36. 32 7
      services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee
  37. 4 3
      services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee
  38. 3 3
      services/web/app/coffee/Features/Uploads/UploadsRouter.coffee
  39. 7 7
      services/web/app/coffee/Features/User/UserController.coffee
  40. 1 2
      services/web/app/coffee/Features/User/UserInfoController.coffee
  41. 21 21
      services/web/app/coffee/infrastructure/ExpressLocals.coffee
  42. 2 2
      services/web/app/coffee/infrastructure/Modules.coffee
  43. 16 0
      services/web/app/coffee/infrastructure/Mongoose.coffee
  44. 66 49
      services/web/app/coffee/infrastructure/Server.coffee
  45. 4 1
      services/web/app/coffee/models/Project.coffee
  46. 105 97
      services/web/app/coffee/router.coffee
  47. 28 9
      services/web/app/views/project/editor/header.jade
  48. 5 0
      services/web/app/views/project/editor/left-menu.jade
  49. 3 3
      services/web/app/views/project/editor/share.jade
  50. 26 12
      services/web/app/views/project/list/side-bar.jade
  51. 13 5
      services/web/package.json
  52. 5 1
      services/web/public/coffee/ide/online-users/OnlineUsersManager.coffee
  53. 24 9
      services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
  54. 0 1
      services/web/public/coffee/main.coffee
  55. 0 77
      services/web/public/coffee/main/templates.coffee
  56. BIN
      services/web/public/img/about/geri.jpg
  57. BIN
      services/web/public/img/about/kiri_channon.jpg
  58. BIN
      services/web/public/img/about/shane_kilkelly.jpg
  59. BIN
      services/web/public/img/enago.png
  60. 1 0
      services/web/public/stylesheets/app/editor.less
  61. 13 0
      services/web/public/stylesheets/app/editor/enago.less
  62. 26 1
      services/web/public/stylesheets/app/editor/online-users.less
  63. 3 0
      services/web/public/stylesheets/app/templates.less
  64. 4 0
      services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
  65. 2 2
      services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee
  66. 2 3
      services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee
  67. 40 1
      services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee
  68. 4 1
      services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee
  69. 8 7
      services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee
  70. 67 5
      services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee
  71. 86 0
      services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee
  72. 51 6
      services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee
  73. 1 8
      services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee
  74. 34 12
      services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee
  75. 7 0
      services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee
  76. 34 1
      services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee
  77. 11 11
      services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee
  78. 1 1
      services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupControllerTests.coffee
  79. 6 11
      services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee
  80. 44 21
      services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee
  81. 3 3
      services/web/test/UnitTests/coffee/Uploads/ProjectUploadControllerTests.coffee
  82. 9 9
      services/web/test/UnitTests/coffee/User/UserControllerTests.coffee
  83. 13 0
      services/web/test/UnitTests/coffee/helpers/MockResponse.coffee

+ 1 - 1
services/web/Gruntfile.coffee

@@ -280,7 +280,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:unit_tests', 'mochaTest:unit']
+	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: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 - 3
services/web/app.coffee

@@ -23,9 +23,6 @@ Server.app.use (error, req, res, next) ->
 		res.end()
 
 if Settings.catchErrors
-	# fairy cleans then exits on an uncaughtError, but we don't want
-	# to exit so it doesn't need to do this.
-	require "fairy"
 	process.removeAllListeners "uncaughtException"
 	process.on "uncaughtException", (error) ->
 		logger.error err: error, "uncaughtException"

+ 5 - 0
services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee

@@ -0,0 +1,5 @@
+AnalyticsController = require('./AnalyticsController')
+
+module.exports =
+	apply: (webRouter, apiRouter) ->
+		webRouter.post '/event/:event', AnalyticsController.recordEvent

+ 4 - 1
services/web/app/coffee/Features/Authentication/AuthenticationController.coffee

@@ -7,6 +7,8 @@ logger = require("logger-sharelatex")
 querystring = require('querystring')
 Url = require("url")
 Settings = require "settings-sharelatex"
+basicAuth = require('basic-auth-connect')
+
 
 module.exports = AuthenticationController =
 	login: (req, res, next = (error) ->) ->
@@ -101,7 +103,7 @@ module.exports = AuthenticationController =
 			logger.log url:req.url, "user trying to access endpoint not in global whitelist"
 			return res.redirect "/login"
 
-	httpAuth: require('express').basicAuth (user, pass)->
+	httpAuth: basicAuth (user, pass)->
 		isValid = Settings.httpAuthUsers[user] == pass
 		if !isValid
 			logger.err user:user, pass:pass, "invalid login details"
@@ -152,6 +154,7 @@ module.exports = AuthenticationController =
 		# Regenerate the session to get a new sessionID (cookie value) to
 		# protect against session fixation attacks
 		oldSession = req.session
+		req.session.destroy()
 		req.sessionStore.generate(req)
 		for key, value of oldSession
 			req.session[key] = value

+ 2 - 1
services/web/app/coffee/Features/Blog/BlogController.coffee

@@ -22,9 +22,10 @@ module.exports = BlogController =
 
 		logger.log url:url, "proxying request to blog api"
 		request.get blogUrl, (err, r, data)->
-			return next(err) if err?
 			if r?.statusCode == 404
 				return ErrorController.notFound(req, res, next)
+			if err?
+				return res.send 500
 			data = data.trim()
 			try
 				data = JSON.parse(data)

+ 2 - 2
services/web/app/coffee/Features/Chat/ChatController.coffee

@@ -12,7 +12,7 @@ module.exports =
 		ChatHandler.sendMessage project_id, user_id, messageContent, (err, builtMessge)->
 			if err?
 				logger.err err:err, project_id:project_id, user_id:user_id, messageContent:messageContent, "problem sending message to chat api"
-				return res.send(500)
+				return res.sendStatus(500)
 			EditorRealTimeController.emitToRoom project_id,  "new-chat-message", builtMessge, (err)->
 			res.send()
 
@@ -23,7 +23,7 @@ module.exports =
 		ChatHandler.getMessages project_id, query, (err, messages)->
 			if err?
 				logger.err err:err, query:query, "problem getting messages from chat api"
-				return res.send 500
+				return res.sendStatus 500
 			logger.log length:messages?.length, "sending messages to client"
 			res.set 'Content-Type', 'application/json'
 			res.send messages

+ 2 - 3
services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee

@@ -4,7 +4,6 @@ EditorController = require "../Editor/EditorController"
 
 module.exports = CollaboratorsController =
 	getCollaborators: (req, res, next = (error) ->) ->
-		req.session.destroy()
 		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) ->
@@ -19,7 +18,7 @@ module.exports = CollaboratorsController =
 			return next(new Error("User should be logged in"))
 		CollaboratorsHandler.removeUserFromProject req.params.project_id, user_id, (error) ->
 			return next(error) if error?
-			res.send 204
+			res.sendStatus 204
 			
 	addUserToProject: (req, res, next) ->
 		project_id = req.params.Project_id
@@ -33,7 +32,7 @@ module.exports = CollaboratorsController =
 		user_id    = req.params.user_id
 		EditorController.removeUserFromProject project_id, user_id, (error)->
 			return next(error) if error?
-			res.send 204
+			res.sendStatus 204
 
 	_formatCollaborators: (project, callback = (error, collaborators) ->) ->
 		collaborators = []

+ 5 - 5
services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee

@@ -3,9 +3,9 @@ SecurityManager = require('../../managers/SecurityManager')
 AuthenticationController = require('../Authentication/AuthenticationController')
 
 module.exports =
-	apply: (app) ->
-		app.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
-		app.get  '/project/:Project_id/collaborators', SecurityManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
+	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
 
-		app.post   '/project/:Project_id/users', SecurityManager.requestIsOwner, CollaboratorsController.addUserToProject
-		app.delete '/project/:Project_id/users/:user_id', SecurityManager.requestIsOwner, CollaboratorsController.removeUserFromProject
+		webRouter.post   '/project/:Project_id/users', SecurityManager.requestIsOwner, CollaboratorsController.addUserToProject
+		webRouter.delete '/project/:Project_id/users/:user_id', SecurityManager.requestIsOwner, CollaboratorsController.removeUserFromProject

+ 2 - 2
services/web/app/coffee/Features/Compile/CompileController.coffee

@@ -51,14 +51,14 @@ module.exports = CompileController =
 		project_id = req.params.Project_id
 		CompileManager.deleteAuxFiles project_id, (error) ->
 			return next(error) if error?
-			res.send(200)
+			res.sendStatus(200)
 
 	compileAndDownloadPdf: (req, res, next)->
 		project_id = req.params.project_id
 		CompileManager.compile project_id, null, {}, (err)->
 			if err?
 				logger.err err:err, project_id:project_id, "something went wrong compile and downloading pdf"
-				res.send 500
+				res.sendStatus 500
 			url = "/project/#{project_id}/output/output.pdf"
 			CompileController.proxyToClsi project_id, url, req, res, next
 

+ 28 - 0
services/web/app/coffee/Features/Docstore/DocstoreManager.coffee

@@ -67,3 +67,31 @@ module.exports = DocstoreManager =
 				error = new Error("docstore api responded with non-success code: #{res.statusCode}")
 				logger.error err: error, project_id: project_id, doc_id: doc_id, "error updating doc in docstore"
 				callback(error)
+
+	archiveProject: (project_id, callback)->
+		url = "#{settings.apis.docstore.url}/project/#{project_id}/archive"
+		logger.log project_id:project_id, "archiving project in docstore"
+		request.post url, (err, res, docs) ->
+			if err?
+				logger.err err:err, project_id:project_id, "error archving project in docstore"
+				return callback(err)
+			if 200 <= res.statusCode < 300
+				callback()	
+			else
+				error = new Error("docstore api responded with non-success code: #{res.statusCode}")
+				logger.err err: error, project_id: project_id, "error archiving project in docstore"
+				return callback(error)
+
+	unarchiveProject: (project_id, callback)->
+		url = "#{settings.apis.docstore.url}/project/#{project_id}/unarchive"
+		logger.log project_id:project_id, "unarchiving project in docstore"
+		request.post url, (err, res, docs) ->
+			if err?
+				logger.err err:err, project_id:project_id, "error unarchiving project in docstore"
+				return callback(err)
+			if 200 <= res.statusCode < 300
+				callback()	
+			else
+				error = new Error("docstore api responded with non-success code: #{res.statusCode}")
+				logger.err err: error, project_id: project_id, "error unarchiving project in docstore"
+				return callback(error)

+ 1 - 3
services/web/app/coffee/Features/Documents/DocumentController.coffee

@@ -15,7 +15,6 @@ module.exports =
 			res.send JSON.stringify {
 				lines: lines
 			}
-			req.session.destroy()
 
 	setDocument: (req, res, next = (error) ->) ->
 		project_id = req.params.Project_id
@@ -27,8 +26,7 @@ module.exports =
 				logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
 				return next(error)
 			logger.log doc_id:doc_id, project_id:project_id, "finished receiving set document request from api (docupdater)"
-			res.send 200
-			req.session.destroy()
+			res.sendStatus 200
 
 		
 		

+ 7 - 7
services/web/app/coffee/Features/Editor/EditorHttpController.coffee

@@ -49,7 +49,7 @@ module.exports = EditorHttpController =
 		name = req.body.name
 
 		if !name?
-			return res.send 400 # Malformed request
+			return res.sendStatus 400 # Malformed request
 
 		logger.log project_id: project_id, doc_id: doc_id, "restoring doc"
 		ProjectEntityHandler.restoreDoc project_id, doc_id, name, (err, doc, folder_id) =>
@@ -68,7 +68,7 @@ module.exports = EditorHttpController =
 		name = req.body.name
 		parent_folder_id = req.body.parent_folder_id
 		if !EditorHttpController._nameIsAcceptableLength(name)
-			return res.send 400
+			return res.sendStatus 400
 		EditorController.addDoc project_id, parent_folder_id, name, [], "editor", (error, doc) ->
 			return next(error) if error?
 			res.json doc
@@ -78,7 +78,7 @@ module.exports = EditorHttpController =
 		name = req.body.name
 		parent_folder_id = req.body.parent_folder_id
 		if !EditorHttpController._nameIsAcceptableLength(name)
-			return res.send 400
+			return res.sendStatus 400
 		EditorController.addFolder project_id, parent_folder_id, name, "editor", (error, doc) ->
 			return next(error) if error?
 			res.json doc
@@ -89,10 +89,10 @@ module.exports = EditorHttpController =
 		entity_type = req.params.entity_type
 		name = req.body.name
 		if !EditorHttpController._nameIsAcceptableLength(name)
-			return res.send 400
+			return res.sendStatus 400
 		EditorController.renameEntity project_id, entity_id, entity_type, name, (error) ->
 			return next(error) if error?
-			res.send 204
+			res.sendStatus 204
 
 	moveEntity: (req, res, next) ->
 		project_id  = req.params.Project_id
@@ -101,7 +101,7 @@ module.exports = EditorHttpController =
 		folder_id = req.body.folder_id
 		EditorController.moveEntity project_id, entity_id, folder_id, entity_type, (error) ->
 			return next(error) if error?
-			res.send 204
+			res.sendStatus 204
 
 	deleteDoc: (req, res, next)->
 		req.params.entity_type  = "doc"
@@ -121,6 +121,6 @@ module.exports = EditorHttpController =
 		entity_type = req.params.entity_type
 		EditorController.deleteEntity project_id, entity_id, entity_type, "editor", (error) ->
 			return next(error) if error?
-			res.send 204
+			res.sendStatus 204
 
 

+ 10 - 11
services/web/app/coffee/Features/Editor/EditorRouter.coffee

@@ -3,21 +3,20 @@ SecurityManager = require('../../managers/SecurityManager')
 AuthenticationController = require "../Authentication/AuthenticationController"
 
 module.exports =
-	apply: (app) ->
-		app.post   '/project/:Project_id/doc', SecurityManager.requestCanModifyProject, EditorHttpController.addDoc
-		app.post   '/project/:Project_id/folder', SecurityManager.requestCanModifyProject, EditorHttpController.addFolder
+	apply: (webRouter, apiRouter) ->
+		webRouter.post   '/project/:Project_id/doc', SecurityManager.requestCanModifyProject, EditorHttpController.addDoc
+		webRouter.post   '/project/:Project_id/folder', SecurityManager.requestCanModifyProject, EditorHttpController.addFolder
 
-		app.post   '/project/:Project_id/:entity_type/:entity_id/rename', SecurityManager.requestCanModifyProject, EditorHttpController.renameEntity
-		app.post   '/project/:Project_id/:entity_type/:entity_id/move', SecurityManager.requestCanModifyProject, EditorHttpController.moveEntity
+		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
 
-		app.delete '/project/:Project_id/file/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteFile
-		app.delete '/project/:Project_id/doc/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteDoc
-		app.delete '/project/:Project_id/folder/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteFolder
+		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
 
-		app.post   '/project/:Project_id/doc/:doc_id/restore', SecurityManager.requestCanModifyProject, EditorHttpController.restoreDoc
+		webRouter.post   '/project/:Project_id/doc/:doc_id/restore', SecurityManager.requestCanModifyProject, 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
 		# whenever a user joins a project, like updating the deleted status.
-		app.post   '/project/:Project_id/join', AuthenticationController.httpAuth, EditorHttpController.joinProject
-		app.ignoreCsrf('post', '/project/:Project_id/join')
+		apiRouter.post '/project/:Project_id/join', AuthenticationController.httpAuth, EditorHttpController.joinProject

+ 22 - 4
services/web/app/coffee/Features/FileStore/FileStoreController.coffee

@@ -1,21 +1,39 @@
 logger = require('logger-sharelatex')
 FileStoreHandler = require("./FileStoreHandler")
 ProjectLocator = require("../Project/ProjectLocator")
- 
+_ = require('underscore')
+
+is_mobile_safari = (user_agent) ->
+	user_agent and (user_agent.indexOf('iPhone') >= 0 or
+									user_agent.indexOf('iPad') >= 0)
+
+is_html = (file) ->
+	ends_with = (ext) ->
+		file.name? and
+		file.name.length > ext.length and
+		(file.name.lastIndexOf(ext) == file.name.length - ext.length)
+
+	ends_with('.html') or ends_with('.htm') or ends_with('.xhtml')
+
 module.exports =
 
 	getFile : (req, res)->
 		project_id = req.params.Project_id
 		file_id = req.params.File_id
 		queryString = req.query
+		user_agent = req.get('User-Agent')
 		logger.log project_id: project_id, file_id: file_id, queryString:queryString, "file download"
 		ProjectLocator.findElement {project_id: project_id, element_id: file_id, type: "file"}, (err, file)->
 			if err?
 				logger.err err:err, project_id: project_id, file_id: file_id, queryString:queryString, "error finding element for downloading file"
-				return res.send 500
+				return res.sendStatus 500
 			FileStoreHandler.getFileStream project_id, file_id, queryString, (err, stream)->
 				if err?
 					logger.err err:err, project_id: project_id, file_id: file_id, queryString:queryString, "error getting file stream for downloading file"
-					return res.send 500
+					return res.sendStatus 500
+				# mobile safari will try to render html files, prevent this
+				if (is_mobile_safari(user_agent) and is_html(file))
+					logger.log filename: file.name, user_agent: user_agent, "sending html file to mobile-safari as plain text"
+					res.setHeader('Content-Type', 'text/plain')
 				res.setHeader("Content-Disposition", "attachment; filename=#{file.name}")
-				stream.pipe res
+				stream.pipe res

+ 2 - 2
services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee

@@ -32,9 +32,9 @@ module.exports = HealthCheckController =
 
 	checkRedis: (req, res, next)->
 		if redisCheck.isAlive()
-			res.send 200
+			res.sendStatus 200
 		else
-			res.send 500
+			res.sendStatus 500
 		
 Reporter = (res) ->
 	(runner) ->

+ 25 - 0
services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee

@@ -0,0 +1,25 @@
+InactiveProjectManager = require("./InactiveProjectManager")
+logger = require("logger-sharelatex")
+
+
+module.exports = 
+
+	deactivateOldProjects: (req, res)->
+		logger.log "recived request to deactivate old projects"
+		numberOfProjectsToArchive = req.body.numberOfProjectsToArchive
+		ageOfProjects = req.body.ageOfProjects
+		InactiveProjectManager.deactivateOldProjects numberOfProjectsToArchive, ageOfProjects, (err, projectsDeactivated)->
+			if err?
+				res.sendStatus(500)
+			else
+				res.send(projectsDeactivated)
+
+
+	deactivateProject: (req, res)->
+		project_id = req.params.project_id
+		logger.log project_id:project_id, "recived request to deactivating project"
+		InactiveProjectManager.deactivateProject project_id, (err)->
+			if err?
+				res.sendStatus 500
+			else
+				res.sendStatus 200

+ 56 - 0
services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee

@@ -0,0 +1,56 @@
+async = require("async")
+_ = require("underscore")
+logger = require("logger-sharelatex")
+DocstoreManager = require("../Docstore/DocstoreManager")
+ProjectGetter = require("../Project/ProjectGetter")
+ProjectUpdateHandler = require("../Project/ProjectUpdateHandler")
+Project = require("../../models/Project").Project
+
+MILISECONDS_IN_DAY = 86400000
+module.exports = InactiveProjectManager =
+
+	reactivateProjectIfRequired: (project_id, callback)->
+		ProjectGetter.getProject project_id, {active:true}, (err, project)->
+			if err?
+				logger.err err:err, project_id:project_id, "error getting project"
+				return callback(err)
+			logger.log project_id:project_id, active:project.active, "seeing if need to reactivate project"
+
+			if project.active
+				return callback()
+
+			DocstoreManager.unarchiveProject project_id, (err)->
+				if err?
+					logger.err err:err, project_id:project_id, "error reactivating project in docstore"
+					return callback(err)
+				ProjectUpdateHandler.markAsActive project_id, callback
+
+	deactivateOldProjects: (limit = 10, daysOld = 360, callback)->
+		oldProjectDate = new Date() - (MILISECONDS_IN_DAY * daysOld)
+		logger.log oldProjectDate:oldProjectDate, limit:limit, daysOld:daysOld, "starting process of deactivating old projects"
+		Project.find()
+			.where("lastOpened").lt(oldProjectDate)
+			.where("active").equals(true)
+			.select("_id")
+			.limit(limit)
+			.exec (err, projects)->
+				if err?
+					logger.err err:err, "could not get projects for deactivating"
+				jobs = _.map projects, (project)->
+					return (cb)->
+						InactiveProjectManager.deactivateProject project._id, cb
+				logger.log numberOfProjects:projects?.length, "deactivating projects"
+				async.series jobs, (err)->
+					if err?
+						logger.err err:err, "error deactivating projects"
+					callback err, projects
+
+
+	deactivateProject: (project_id, callback)->
+		logger.log project_id:project_id, "deactivating inactive project"
+		DocstoreManager.archiveProject project_id, (err)->
+			if err?
+				logger.err err:err, project_id:project_id, "error deactivating project in docstore"
+				return callback(err)
+			ProjectUpdateHandler.markAsInactive project_id, callback
+

+ 14 - 8
services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee

@@ -6,12 +6,12 @@ module.exports =
 
 	renderRequestResetForm: (req, res)->
 		logger.log "rendering request reset form"
-		res.render "user/passwordReset", 
+		res.render "user/passwordReset",
 			title:"reset_password"
 
 	requestReset: (req, res)->
 		email = req.body.email.trim().toLowerCase()
-		opts = 
+		opts =
 			endpointName: "password_reset_rate_limit"
 			timeInterval: 60
 			subjectName: req.ip
@@ -23,22 +23,28 @@ module.exports =
 				if err?
 					res.send 500, {message:err?.message}
 				else if exists
-					res.send 200
+					res.sendStatus 200
 				else
 					res.send 404, {message: req.i18n.translate("cant_find_email")}
 
 	renderSetPasswordForm: (req, res)->
-		res.render "user/setPassword", 
+		if req.query.passwordResetToken?
+			req.session.resetToken = req.query.passwordResetToken
+			return res.redirect('/user/password/set')
+		if !req.session.resetToken?
+			return res.redirect('/user/password/reset')
+		res.render "user/setPassword",
 			title:"set_password"
-			passwordResetToken:req.query.passwordResetToken
+			passwordResetToken: req.session.resetToken
 
 	setNewUserPassword: (req, res)->
 		{passwordResetToken, password} = req.body
 		if !password? or password.length == 0 or !passwordResetToken? or passwordResetToken.length == 0
-			return res.send 400
+			return res.sendStatus 400
+		delete req.session.resetToken
 		PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found) ->
 			return next(err) if err?
 			if found
-				res.send 200
+				res.sendStatus 200
 			else
-				res.send 404, {message: req.i18n.translate("password_reset_token_expired")}
+				res.send 404, {message: req.i18n.translate("password_reset_token_expired")}

+ 5 - 5
services/web/app/coffee/Features/PasswordReset/PasswordResetRouter.coffee

@@ -2,13 +2,13 @@ PasswordResetController = require("./PasswordResetController")
 AuthenticationController = require('../Authentication/AuthenticationController')
 
 module.exports =
-	apply: (app) ->
+	apply: (webRouter, apiRouter) ->
 
-		app.get  '/user/password/reset', PasswordResetController.renderRequestResetForm
-		app.post  '/user/password/reset', PasswordResetController.requestReset
+		webRouter.get  '/user/password/reset', PasswordResetController.renderRequestResetForm
+		webRouter.post  '/user/password/reset', PasswordResetController.requestReset
 		AuthenticationController.addEndpointToLoginWhitelist '/user/password/reset'
 		
-		app.get '/user/password/set', PasswordResetController.renderSetPasswordForm
-		app.post '/user/password/set', PasswordResetController.setNewUserPassword
+		webRouter.get '/user/password/set', PasswordResetController.renderSetPasswordForm
+		webRouter.post '/user/password/set', PasswordResetController.setNewUserPassword
 		AuthenticationController.addEndpointToLoginWhitelist '/user/password/set'
 

+ 1 - 2
services/web/app/coffee/Features/Project/ProjectApiController.coffee

@@ -9,7 +9,6 @@ module.exports =
 		ProjectDetailsHandler.getDetails project_id, (err, projDetails)->
 			if err?
 				logger.log err:err, project_id:project_id, "something went wrong getting project details"
-				return res.send 500
-			req.session.destroy()
+				return res.sendStatus 500
 			res.json(projDetails)
 

+ 26 - 12
services/web/app/coffee/Features/Project/ProjectController.coffee

@@ -14,6 +14,8 @@ _ = require("underscore")
 Settings = require("settings-sharelatex")
 SecurityManager = require("../../managers/SecurityManager")
 fs = require "fs"
+InactiveProjectManager = require("../InactiveData/InactiveProjectManager")
+ProjectUpdateHandler = require("./ProjectUpdateHandler")
 
 module.exports = ProjectController =
 
@@ -44,7 +46,7 @@ module.exports = ProjectController =
 
 		async.series jobs, (error) ->
 			return next(error) if error?
-			res.send(204)
+			res.sendStatus(204)
 
 	deleteProject: (req, res) ->
 		project_id = req.params.Project_id
@@ -58,18 +60,18 @@ module.exports = ProjectController =
 
 		doDelete project_id, (err)->
 			if err?
-				res.send 500
+				res.sendStatus 500
 			else
-				res.send 200
+				res.sendStatus 200
 
 	restoreProject: (req, res) ->
 		project_id = req.params.Project_id
 		logger.log project_id:project_id, "received request to restore project"
 		projectDeleter.restoreProject project_id, (err)->
 			if err?
-				res.send 500
+				res.sendStatus 500
 			else
-				res.send 200
+				res.sendStatus 200
 
 	cloneProject: (req, res, next)->
 		metrics.inc "cloned-project"
@@ -99,7 +101,7 @@ module.exports = ProjectController =
 		], (err, project)->
 			if err?
 				logger.error err: err, project: project, user: user, name: projectName, templateType: template, "error creating project"
-				res.send 500
+				res.sendStatus 500
 			else
 				logger.log project: project, user: user, name: projectName, templateType: template, "created project"
 				res.send {project_id:project._id}
@@ -109,13 +111,13 @@ module.exports = ProjectController =
 		project_id = req.params.Project_id
 		newName = req.body.newProjectName
 		if newName.length > 150
-			return res.send 400
+			return res.sendStatus 400
 		editorController.renameProject project_id, newName, (err)->
 			if err?
 				logger.err err:err, project_id:project_id, newName:newName, "problem renaming project"
-				res.send 500
+				res.sendStatus 500
 			else
-				res.send 200
+				res.sendStatus 200
 
 	projectListPage: (req, res, next)->
 		timer = new metrics.Timer("project-list")
@@ -173,6 +175,7 @@ module.exports = ProjectController =
 			user_id = 'openUser'
 		
 		project_id = req.params.Project_id
+		logger.log project_id:project_id, "loading editor"
 	
 		async.parallel {
 			project: (cb)->
@@ -181,11 +184,19 @@ module.exports = ProjectController =
 				if user_id == 'openUser'
 					cb null, defaultSettingsForAnonymousUser(user_id)
 				else
-					User.findById user_id, cb
+					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'
 					return cb()
 				SubscriptionLocator.getUsersSubscription user_id, cb
+			activate: (cb)->
+				InactiveProjectManager.reactivateProjectIfRequired project_id, cb
+			markAsOpened: (cb)-> 
+				#don't need to wait for this to complete
+				ProjectUpdateHandler.markAsOpened project_id, ->
+				cb()
 		}, (err, results)->
 			if err?
 				logger.err err:err, "error getting details for project page"
@@ -194,13 +205,16 @@ module.exports = ProjectController =
 			user = results.user
 			subscription = results.subscription
 
+			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
-					return res.send 401
+					return res.sendStatus 401
 
 				if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt?
 					allowedFreeTrial = !!subscription.freeTrial.allowed || true
-
+				logger.log project_id:project_id, "rendering editor page"
 				res.render 'project/editor',
 					title:  project.name
 					priority_title: true

+ 0 - 1
services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee

@@ -19,7 +19,6 @@ module.exports =
 		project = new Project
 			 owner_ref  : new ObjectId(owner_id)
 			 name       : projectName
-			 useClsi2   : true
 		project.rootFolder[0] = rootFolder
 		User.findById owner_id, "ace.spellCheckLanguage", (err, user)->
 			project.spellCheckLanguage = user.ace.spellCheckLanguage

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

@@ -54,7 +54,10 @@ module.exports =
 
 	findRootDoc : (opts, callback)->
 		getRootDoc = (project)=>
-			@findElement {project:project, element_id:project.rootDoc_id, type:"docs"}, callback
+			if project.rootDoc_id?
+				@findElement {project:project, element_id:project.rootDoc_id, type:"docs"}, callback
+			else
+				callback null, null
 		{project, project_id} = opts
 		if project?
 			getRootDoc project

+ 6 - 2
services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee

@@ -15,8 +15,12 @@ module.exports = ProjectRootDocManager =
 				return (cb)->
 					rootDocId = null
 					for line in doc.lines || []
-						match = line.match /(.*)\\documentclass/ # no lookbehind in js regexp :(
-						isRootDoc = Path.extname(path).match(/\.R?tex$/) and match and !match[1].match /%/
+						# We've had problems with this regex locking up CPU.
+						# Previously /.*\\documentclass/ would totally lock up on lines of 500kb (data text files :()
+						# This regex will only look from the start of the line, including whitespace so will return quickly
+						# regardless of line length.
+						match = line.match /^\s*\\documentclass/
+						isRootDoc = Path.extname(path).match(/\.R?tex$/) and match
 						if isRootDoc
 							rootDocId = doc?._id
 					cb(rootDocId)

+ 22 - 0
services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee

@@ -1,5 +1,6 @@
 Project = require('../../models/Project').Project
 logger = require('logger-sharelatex')
+Project = require("../../models/Project").Project
 
 module.exports = 
 	markAsUpdated : (project_id, callback)->
@@ -8,3 +9,24 @@ module.exports =
 		Project.update conditions, update, {}, (err)->
 			if callback?
 				callback()
+
+	markAsOpened : (project_id, callback)->
+		conditions = {_id:project_id}
+		update = {lastOpened:Date.now()}
+		Project.update conditions, update, {}, (err)->
+			if callback?
+				callback()
+
+	markAsInactive: (project_id, callback)->
+		conditions = {_id:project_id}
+		update = {active:false}
+		Project.update conditions, update, {}, (err)->
+			if callback?
+				callback()
+
+	markAsActive: (project_id, callback)->
+		conditions = {_id:project_id}
+		update = {active:true}
+		Project.update conditions, update, {}, (err)->
+			if callback?
+				callback()

+ 2 - 2
services/web/app/coffee/Features/RealTimeProxy/RealTimeProxyRouter.coffee

@@ -10,8 +10,8 @@ wsProxy = httpProxy.createProxyServer({
 })
 
 module.exports =
-	apply: (app) ->
-		app.all /\/socket\.io\/.*/, (req, res, next) ->
+	apply: (webRouter, apiRouter) ->
+		webRouter.all /\/socket\.io\/.*/, (req, res, next) ->
 			proxy.web req, res, next
 			
 		setTimeout () ->

+ 6 - 6
services/web/app/coffee/Features/ServerAdmin/AdminController.coffee

@@ -50,12 +50,12 @@ module.exports = AdminController =
 	dissconectAllUsers: (req, res)=>
 		logger.warn "disconecting everyone"
 		EditorRealTimeController.emitToAll 'forceDisconnect', "Sorry, we are performing a quick update to the editor and need to close it down. Please refresh the page to continue."
-		res.send(200)
+		res.sendStatus(200)
 
 	closeEditor : (req, res)->
 		logger.warn "closing editor"
 		Settings.editorIsOpen = req.body.isOpen
-		res.send(200)
+		res.sendStatus(200)
 
 	writeAllToMongo : (req, res)->
 		logger.log "writing all docs to mongo"
@@ -74,19 +74,19 @@ module.exports = AdminController =
 
 	flushProjectToTpds: (req, res)->
 		projectEntityHandler.flushProjectToThirdPartyDataStore req.body.project_id, (err)->
-			res.send 200
+			res.sendStatus 200
 
 	pollDropboxForUser: (req, res)->
 		user_id = req.body.user_id
 		TpdsUpdateSender.pollDropboxForUser user_id, () ->
-			res.send 200
+			res.sendStatus 200
 			
 	createMessage: (req, res, next) ->
 		SystemMessageManager.createMessage req.body.content, (error) ->
 			return next(error) if error?
-			res.send 200
+			res.sendStatus 200
 			
 	clearMessages: (req, res, next) ->
 		SystemMessageManager.clearMessages (error) ->
 			return next(error) if error?
-			res.send 200
+			res.sendStatus 200

+ 1 - 1
services/web/app/coffee/Features/Spelling/SpellingController.coffee

@@ -11,4 +11,4 @@ module.exports = SpellingController =
 		getReq.pipe(res)
 		getReq.on "error", (error) ->
 			logger.error err: error, "Spelling API error"
-			res.send 500
+			res.sendStatus 500

+ 12 - 12
services/web/app/coffee/Features/StaticPages/StaticPagesRouter.coffee

@@ -3,18 +3,18 @@ UniversityController = require("./UniversityController")
 
 
 module.exports =
-	apply: (app) ->
-		app.get  '/', HomeController.index
-		app.get  '/home', HomeController.home
+	apply: (webRouter, apiRouter) ->
+		webRouter.get  '/', HomeController.index
+		webRouter.get  '/home', HomeController.home
 
-		app.get '/tos', HomeController.externalPage("tos", "Terms of Service")
-		app.get '/about', HomeController.externalPage("about", "About Us")
-		app.get '/security', HomeController.externalPage("security", "Security")
-		app.get '/privacy_policy', HomeController.externalPage("privacy", "Privacy Policy")
-		app.get '/planned_maintenance', HomeController.externalPage("planned_maintenance", "Planned Maintenance")
-		app.get '/style', HomeController.externalPage("style_guide", "Style Guide")
+		webRouter.get '/tos', HomeController.externalPage("tos", "Terms of Service")
+		webRouter.get '/about', HomeController.externalPage("about", "About Us")
+		webRouter.get '/security', HomeController.externalPage("security", "Security")
+		webRouter.get '/privacy_policy', HomeController.externalPage("privacy", "Privacy Policy")
+		webRouter.get '/planned_maintenance', HomeController.externalPage("planned_maintenance", "Planned Maintenance")
+		webRouter.get '/style', HomeController.externalPage("style_guide", "Style Guide")
 
-		app.get '/dropbox', HomeController.externalPage("dropbox", "Dropbox and ShareLaTeX")
+		webRouter.get '/dropbox', HomeController.externalPage("dropbox", "Dropbox and ShareLaTeX")
 
-		app.get '/university', UniversityController.getIndexPage
-		app.get '/university/*', UniversityController.getPage
+		webRouter.get '/university', UniversityController.getIndexPage
+		webRouter.get '/university/*', UniversityController.getPage

+ 2 - 0
services/web/app/coffee/Features/StaticPages/UniversityController.coffee

@@ -18,6 +18,8 @@ module.exports = UniversityController =
 		request.get universityUrl, (err, r, data)->
 			if r?.statusCode == 404
 				return ErrorController.notFound(req, res, next)
+			if err?
+				return res.send 500
 			data = data.trim()
 			try
 				data = JSON.parse(data)

+ 6 - 6
services/web/app/coffee/Features/Subscription/SubscriptionController.coffee

@@ -147,8 +147,8 @@ module.exports = SubscriptionController =
 			SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)->
 				if err?
 					logger.err err:err, user_id:user._id, "something went wrong creating subscription"
-					return res.send 500
-				res.send 201
+					return res.sendStatus 500
+				res.sendStatus 201
 
 	successful_subscription: (req, res)->
 		SecurityManager.getCurrentUser req, (error, user) =>
@@ -191,9 +191,9 @@ module.exports = SubscriptionController =
 		if req.body? and req.body["expired_subscription_notification"]?
 			recurlySubscription = req.body["expired_subscription_notification"].subscription
 			SubscriptionHandler.recurlyCallback recurlySubscription, ->
-				res.send 200
+				res.sendStatus 200
 		else
-			res.send 200
+			res.sendStatus 200
 
 	renderUpgradeToAnnualPlanPage: (req, res)->
 		SecurityManager.getCurrentUser req, (error, user) ->
@@ -221,9 +221,9 @@ module.exports = SubscriptionController =
 			SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)->
 				if err?
 					logger.err err:err, user_id:user._id, "error updating subscription"
-					res.send 500
+					res.sendStatus 500
 				else
-					res.send 200
+					res.sendStatus 200
 
 
 	recurlyNotificationParser: (req, res, next) ->

+ 3 - 3
services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee

@@ -62,9 +62,9 @@ module.exports =
 			return ErrorsController.notFound(req, res)
 		SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, req.session.user.email, (err)->
 			if err?
-				res.send 500
+				res.sendStatus 500
 			else
-				res.send 200
+				res.sendStatus 200
 
 	completeJoin: (req, res)->
 		subscription_id = req.params.subscription_id
@@ -74,7 +74,7 @@ module.exports =
 			if err? and err == "token_not_found"
 				res.redirect "/user/subscription/#{subscription_id}/group/invited?expired=true"
 			else if err?
-				res.send 500
+				res.sendStatus 500
 			else
 				res.redirect "/user/subscription/#{subscription_id}/group/successful-join"
 

+ 3 - 1
services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee

@@ -10,7 +10,9 @@ module.exports =
 		else if user_or_id?
 			user_id = user_or_id
 		logger.log user_id:user_id, "getting users subscription"
-		Subscription.findOne admin_id:user_id, callback
+		Subscription.findOne admin_id:user_id, (err, subscription)->
+			logger.log user_id:user_id, "got users subscription"
+			callback(err, subscription)
 
 	getMemberSubscriptions: (user_id, callback) ->
 		logger.log user_id: user_id, "getting users group subscriptions"

+ 22 - 23
services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee

@@ -4,44 +4,43 @@ SubscriptionGroupController = require './SubscriptionGroupController'
 Settings = require "settings-sharelatex"
 
 module.exports =
-	apply: (app) ->
+	apply: (webRouter, apiRouter) ->
 		return unless Settings.enableSubscriptions
 
-		app.get  '/user/subscription/plans',      SubscriptionController.plansPage
+		webRouter.get  '/user/subscription/plans',      SubscriptionController.plansPage
 
-		app.get  '/user/subscription',            AuthenticationController.requireLogin(), SubscriptionController.userSubscriptionPage
+		webRouter.get  '/user/subscription',            AuthenticationController.requireLogin(), SubscriptionController.userSubscriptionPage
 
-		app.get  '/user/subscription/custom_account', AuthenticationController.requireLogin(), SubscriptionController.userCustomSubscriptionPage
+		webRouter.get  '/user/subscription/custom_account', AuthenticationController.requireLogin(), SubscriptionController.userCustomSubscriptionPage
 
 		
-		app.get  '/user/subscription/new',        AuthenticationController.requireLogin(), SubscriptionController.paymentPage 
-		app.get  '/user/subscription/billing-details/edit', AuthenticationController.requireLogin(), SubscriptionController.editBillingDetailsPage
+		webRouter.get  '/user/subscription/new',        AuthenticationController.requireLogin(), SubscriptionController.paymentPage 
+		webRouter.get  '/user/subscription/billing-details/edit', AuthenticationController.requireLogin(), SubscriptionController.editBillingDetailsPage
 
-		app.get  '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription
+		webRouter.get  '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription
 
 
-		app.get '/subscription/group',  AuthenticationController.requireLogin(), SubscriptionGroupController.renderSubscriptionGroupAdminPage
-		app.post '/subscription/group/user', AuthenticationController.requireLogin(),  SubscriptionGroupController.addUserToGroup
-		app.get '/subscription/group/export',  AuthenticationController.requireLogin(), SubscriptionGroupController.exportGroupCsv
-		app.del '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup
+		webRouter.get '/subscription/group',  AuthenticationController.requireLogin(), SubscriptionGroupController.renderSubscriptionGroupAdminPage
+		webRouter.post '/subscription/group/user', AuthenticationController.requireLogin(),  SubscriptionGroupController.addUserToGroup
+		webRouter.get '/subscription/group/export',  AuthenticationController.requireLogin(), SubscriptionGroupController.exportGroupCsv
+		webRouter.delete '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup
 
-		app.get '/user/subscription/:subscription_id/group/invited', AuthenticationController.requireLogin(), SubscriptionGroupController.renderGroupInvitePage
-		app.post '/user/subscription/:subscription_id/group/begin-join', AuthenticationController.requireLogin(), SubscriptionGroupController.beginJoinGroup
-		app.get '/user/subscription/:subscription_id/group/complete-join', AuthenticationController.requireLogin(), SubscriptionGroupController.completeJoin
-		app.get '/user/subscription/:subscription_id/group/successful-join', AuthenticationController.requireLogin(), SubscriptionGroupController.renderSuccessfulJoinPage
+		webRouter.get '/user/subscription/:subscription_id/group/invited', AuthenticationController.requireLogin(), SubscriptionGroupController.renderGroupInvitePage
+		webRouter.post '/user/subscription/:subscription_id/group/begin-join', AuthenticationController.requireLogin(), SubscriptionGroupController.beginJoinGroup
+		webRouter.get '/user/subscription/:subscription_id/group/complete-join', AuthenticationController.requireLogin(), SubscriptionGroupController.completeJoin
+		webRouter.get '/user/subscription/:subscription_id/group/successful-join', AuthenticationController.requireLogin(), SubscriptionGroupController.renderSuccessfulJoinPage
 
 		#recurly callback
-		app.post '/user/subscription/callback',   SubscriptionController.recurlyNotificationParser, SubscriptionController.recurlyCallback
-		app.ignoreCsrf("post", '/user/subscription/callback')
+		apiRouter.post '/user/subscription/callback',   SubscriptionController.recurlyNotificationParser, SubscriptionController.recurlyCallback
 
 		#user changes their account state
-		app.post '/user/subscription/create',     AuthenticationController.requireLogin(), SubscriptionController.createSubscription
-		app.post '/user/subscription/update',     AuthenticationController.requireLogin(), SubscriptionController.updateSubscription
-		app.post '/user/subscription/cancel',     AuthenticationController.requireLogin(), SubscriptionController.cancelSubscription
-		app.post '/user/subscription/reactivate', AuthenticationController.requireLogin(), SubscriptionController.reactivateSubscription
+		webRouter.post '/user/subscription/create',     AuthenticationController.requireLogin(), SubscriptionController.createSubscription
+		webRouter.post '/user/subscription/update',     AuthenticationController.requireLogin(), SubscriptionController.updateSubscription
+		webRouter.post '/user/subscription/cancel',     AuthenticationController.requireLogin(), SubscriptionController.cancelSubscription
+		webRouter.post '/user/subscription/reactivate', AuthenticationController.requireLogin(), SubscriptionController.reactivateSubscription
 
 
-		app.get "/user/subscription/upgrade-annual",  AuthenticationController.requireLogin(), SubscriptionController.renderUpgradeToAnnualPlanPage
-		app.post "/user/subscription/upgrade-annual",  AuthenticationController.requireLogin(), SubscriptionController.processUpgradeToAnnualPlan
+		webRouter.get "/user/subscription/upgrade-annual",  AuthenticationController.requireLogin(), SubscriptionController.renderUpgradeToAnnualPlanPage
+		webRouter.post "/user/subscription/upgrade-annual",  AuthenticationController.requireLogin(), SubscriptionController.processUpgradeToAnnualPlan
 
 

+ 6 - 10
services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee

@@ -17,11 +17,10 @@ module.exports =
 			logger.log user_id:user_id, filePath:filePath, fullPath:req.params[0], "sending response that tpdsUpdate has been completed"
 			if err?
 				logger.err err:err, user_id:user_id, filePath:filePath, "error reciving update from tpds"
-				res.send(500)
+				res.sendStatus(500)
 			else
 				logger.log user_id:user_id, filePath:filePath, projectName:projectName, "telling tpds update has been processed"
-				res.send 200
-			req.session.destroy()
+				res.sendStatus 200
 
 
 	deleteUpdate: (req, res)->
@@ -32,11 +31,10 @@ module.exports =
 		tpdsUpdateHandler.deleteUpdate user_id, projectName, filePath, source, (err)->
 			if err?
 				logger.err err:err, user_id:user_id, filePath:filePath, "error reciving update from tpds"
-				res.send(500)
+				res.sendStatus(500)
 			else
 				logger.log user_id:user_id, filePath:filePath, projectName:projectName, "telling tpds delete has been processed"
-				res.send 200
-			req.session.destroy()
+				res.sendStatus 200
 	
 	# updateProjectContents and deleteProjectContents are used by GitHub. The project_id is known so we 
 	# can skip right ahead to creating/updating/deleting the file. These methods will not ignore noisy
@@ -49,8 +47,7 @@ module.exports =
 		logger.log project_id: project_id, path: path, source: source, "received project contents update"
 		UpdateMerger.mergeUpdate project_id, path, req, source, (error) ->
 			return next(error) if error?
-			res.send(200)
-			req.session.destroy()
+			res.sendStatus(200)
 			
 	deleteProjectContents: (req, res, next = (error) ->) ->
 		{project_id} = req.params
@@ -59,8 +56,7 @@ module.exports =
 		logger.log project_id: project_id, path: path, source: source, "received project contents delete request"
 		UpdateMerger.deleteUpdate project_id, path, source, (error) ->
 			return next(error) if error?
-			res.send(200)
-			req.session.destroy()
+			res.sendStatus(200)
 
 	parseParams: parseParams = (req)->
 		path = req.params[0]

+ 32 - 7
services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee

@@ -4,6 +4,7 @@ path = require('path')
 Project = require('../../models/Project').Project
 keys = require('../../infrastructure/Keys')
 metrics = require("../../infrastructure/Metrics")
+request = require("request")
 
 buildPath = (user_id, project_name, filePath)->
 	projectPath = path.join(project_name, "/", filePath)
@@ -11,9 +12,33 @@ buildPath = (user_id, project_name, filePath)->
 	fullPath = path.join("/user/", "#{user_id}", "/entity/",projectPath)
 	return fullPath
 
-queue = require('fairy').connect(settings.redis.fairy).queue(keys.queue.web_to_tpds_http_requests)
 
-module.exports =
+
+
+tpdsworkerEnabled = -> settings.apis.tpdsworker?.url?
+if !tpdsworkerEnabled()
+	logger.log "tpdsworker is not enabled, request will not be sent to it"
+
+module.exports = TpdsUpdateSender =
+
+	_enqueue: (group, method, job, callback)->
+		if !tpdsworkerEnabled()
+			return callback()
+		opts = 
+			uri:"#{settings.apis.tpdsworker.url}/enqueue/web_to_tpds_http_requests"
+			json :
+				group:group
+				method:method
+				job:job
+			method:"post"
+			timeout: (5 * 1000)
+		request opts, (err)->
+			if err?
+				logger.err err:err, "error queuing something in the tpdsworker"
+				callback(err)
+			else
+				logger.log group:group, "successfully queued up job for tpdsworker"
+				callback()
 
 	_addEntity: (options, callback = (err)->)->
 		getProjectsUsersIds options.project_id, (err, user_id, allUserIds)->
@@ -27,9 +52,9 @@ module.exports =
 				uri : "#{settings.apis.thirdPartyDataStore.url}#{buildPath(user_id, options.project_name, options.path)}" 
 				title: "addFile"
 				streamOrigin : options.streamOrigin
-			queue.enqueue options.project_id, "pipeStreamFrom", postOptions, ->
+			TpdsUpdateSender._enqueue options.project_id, "pipeStreamFrom", postOptions, (err)->
 				logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, rev:options.rev, "sending file to third party data store queued up for processing"
-				callback()
+				callback(err)
 	
 	addFile : (options, callback = (err)->)->
 		metrics.inc("tpds.add-file")
@@ -64,7 +89,7 @@ module.exports =
 					user_id : user_id
 					endPath: endPath 
 					startPath: startPath 
-			queue.enqueue options.project_id, "standardHttpRequest", moveOptions, callback
+			TpdsUpdateSender._enqueue options.project_id, "standardHttpRequest", moveOptions, callback
 
 	deleteEntity : (options, callback = (err)->)->
 		metrics.inc("tpds.delete-entity")
@@ -78,7 +103,7 @@ module.exports =
 				uri : "#{settings.apis.thirdPartyDataStore.url}#{buildPath(user_id, options.project_name, options.path)}"
 				title:"deleteEntity"
 				sl_all_user_ids:JSON.stringify(allUserIds)
-			queue.enqueue options.project_id, "standardHttpRequest", deleteOptions, callback
+			TpdsUpdateSender._enqueue options.project_id, "standardHttpRequest", deleteOptions, callback
 			
 	pollDropboxForUser: (user_id, callback = (err) ->) ->
 		metrics.inc("tpds.poll-dropbox")
@@ -88,7 +113,7 @@ module.exports =
 			uri:"#{settings.apis.thirdPartyDataStore.url}/user/poll"
 			json:
 				user_ids: [user_id]
-		queue.enqueue "poll-dropbox:#{user_id}", "standardHttpRequest", options, callback
+		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)->

+ 4 - 3
services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee

@@ -9,8 +9,8 @@ module.exports = ProjectUploadController =
 	uploadProject: (req, res, next) ->
 		timer = new metrics.Timer("project-upload")
 		user_id = req.session.user._id
-		{name, path} = req.files.qqfile
-		name = Path.basename(name, ".zip")
+		{originalname, path} = req.files.qqfile
+		name = Path.basename(originalname, ".zip")
 		ProjectUploadManager.createProjectFromZipArchive user_id, name, path, (error, project) ->
 			fs.unlink path, ->
 			timer.done()
@@ -27,7 +27,8 @@ module.exports = ProjectUploadController =
 	
 	uploadFile: (req, res, next) ->
 		timer = new metrics.Timer("file-upload")
-		{name, path} = req.files.qqfile
+		name = req.files.qqfile.originalname
+		path = req.files.qqfile.path
 		project_id   = req.params.Project_id
 		folder_id    = req.query.folder_id
 		if !name? or name.length == 0 or name.length > 150

+ 3 - 3
services/web/app/coffee/Features/Uploads/UploadsRouter.coffee

@@ -3,11 +3,11 @@ AuthenticationController = require('../Authentication/AuthenticationController')
 ProjectUploadController = require "./ProjectUploadController"
 
 module.exports =
-	apply: (app) ->
-		app.post '/project/new/upload',
+	apply: (webRouter, apiRouter) ->
+		webRouter.post '/project/new/upload',
 			AuthenticationController.requireLogin(),
 			ProjectUploadController.uploadProject
-		app.post '/Project/:Project_id/upload',
+		webRouter.post '/Project/:Project_id/upload',
 			SecurityManager.requestCanModifyProject,
 			ProjectUploadController.uploadFile
 

+ 7 - 7
services/web/app/coffee/Features/User/UserController.coffee

@@ -20,8 +20,8 @@ module.exports =
 		user_id = req.session.user._id
 		UserDeleter.deleteUser user_id, (err)->
 			if !err?
-				req.session.destroy()
-			res.send(200)
+				req.session?.destroy()
+			res.sendStatus(200)
 
 	unsubscribe: (req, res)->
 		UserLocator.findById req.session.user._id, (err, user)->
@@ -34,7 +34,7 @@ module.exports =
 		User.findById user_id, (err, user)->
 			if err? or !user?
 				logger.err err:err, user_id:user_id, "problem updaing user settings"
-				return res.send 500
+				return res.sendStatus 500
 
 			if req.body.first_name?
 				user.first_name = req.body.first_name.trim()
@@ -59,9 +59,9 @@ module.exports =
 			user.save (err)->
 				newEmail = req.body.email?.trim().toLowerCase()
 				if !newEmail? or newEmail == user.email
-					return res.send 200
+					return res.sendStatus 200
 				else if newEmail.indexOf("@") == -1
-					return res.send(400)
+					return res.sendStatus(400)
 				else
 					UserUpdater.changeEmailAddress user_id, newEmail, (err)->
 						if err?
@@ -71,7 +71,7 @@ module.exports =
 							else
 								message = req.i18n.translate("problem_changing_email_address")
 							return res.send 500, {message:message}
-						res.send(200)
+						res.sendStatus(200)
 
 	logout : (req, res)->
 		metrics.inc "user.logout"
@@ -84,7 +84,7 @@ module.exports =
 	register : (req, res, next = (error) ->)->
 		email = req.body.email
 		if !email? or email == ""
-			res.send 422 # Unprocessable Entity
+			res.sendStatus 422 # Unprocessable Entity
 			return
 		logger.log {email}, "registering new user"
 		UserRegistrationHandler.registerNewUser {

+ 1 - 2
services/web/app/coffee/Features/User/UserInfoController.coffee

@@ -9,7 +9,7 @@ module.exports = UserController =
 		# 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() 
+			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, {
@@ -26,7 +26,6 @@ module.exports = UserController =
 			return next(error) if error?
 			return res.send(404) if !user?
 			UserController.sendFormattedPersonalInfo(user, res, next)
-			req.session.destroy()
 
 	sendFormattedPersonalInfo: (user, res, next = (error) ->) ->
 		UserController._formatPersonalInfo user, (error, info) ->

+ 21 - 21
services/web/app/coffee/infrastructure/ExpressLocals.coffee

@@ -39,38 +39,38 @@ for path in [
 		logger.log filePath:filePath, "file does not exist for fingerprints"
 	
 
-module.exports = (app)->
-	app.use (req, res, next)->
+module.exports = (app, webRouter, apiRouter)->
+	webRouter.use (req, res, next)->
 		res.locals.session = req.session
 		next()
 
-	app.use (req, res, next)-> 
+	webRouter.use (req, res, next)-> 
 		res.locals.jsPath = jsPath
 		next()
 
-	app.use (req, res, next)-> 
+	webRouter.use (req, res, next)-> 
 		res.locals.settings = Settings
 		next()
 
-	app.use (req, res, next)->
+	webRouter.use (req, res, next)->
 		res.locals.translate = (key, vars = {}) ->
 			vars.appName = Settings.appName
 			req.i18n.translate(key, vars)
 		res.locals.currentUrl = req.originalUrl
 		next()
 
-	app.use (req, res, next)->
+	webRouter.use (req, res, next)->
 		res.locals.getSiteHost = ->
 			Settings.siteUrl.substring(Settings.siteUrl.indexOf("//")+2)
 		next()
 
-	app.use (req, res, next)->
+	webRouter.use (req, res, next)->
 		res.locals.formatProjectPublicAccessLevel = (privilegeLevel)->
 			formatedPrivileges = private:"Private", readOnly:"Public: Read Only", readAndWrite:"Public: Read and Write"
 			return formatedPrivileges[privilegeLevel] || "Private"
 		next()
 
-	app.use (req, res, next)-> 
+	webRouter.use (req, res, next)-> 
 		res.locals.buildReferalUrl = (referal_medium) ->
 			url = Settings.siteUrl
 			if req.session? and req.session.user? and req.session.user.referal_id?
@@ -94,16 +94,16 @@ module.exports = (app)->
 			return ""
 		next()
 
-	app.use (req, res, next) ->
-		res.locals.csrfToken = req.session._csrf
+	webRouter.use (req, res, next) ->
+		res.locals.csrfToken = req?.csrfToken()
 		next()
 
-	app.use (req, res, next) ->
+	webRouter.use (req, res, next) ->
 		res.locals.getReqQueryParam = (field)->
 			return req.query?[field]
 		next()
 
-	app.use (req, res, next)-> 
+	webRouter.use (req, res, next)-> 
 		res.locals.fingerprint = (path) ->
 			if fingerprints[path]?
 				return fingerprints[path]
@@ -112,16 +112,16 @@ module.exports = (app)->
 				return ""
 		next()
 
-	app.use (req, res, next)-> 
+	webRouter.use (req, res, next)-> 
 		res.locals.formatPrice = SubscriptionFormatters.formatPrice
 		next()
 
-	app.use (req, res, next)->
+	webRouter.use (req, res, next)->
 		res.locals.externalAuthenticationSystemUsed = ->
 			Settings.ldap?
 		next()
 
-	app.use (req, res, next)->
+	webRouter.use (req, res, next)->
 		if req.session.user?
 			res.locals.user =
 				email: req.session.user.email
@@ -139,34 +139,34 @@ module.exports = (app)->
 		res.locals.sentryPublicDSN = Settings.sentry?.publicDSN
 		next()
 
-	app.use (req, res, next) ->
+	webRouter.use (req, res, next) ->
 		if req.query? and req.query.scribtex_path?
 			res.locals.lookingForScribtex = true
 			res.locals.scribtexPath = req.query.scribtex_path
 		next()
 
-	app.use (req, res, next) ->
+	webRouter.use (req, res, next) ->
 		res.locals.nav = Settings.nav
 		res.locals.templates = Settings.templateLinks
 		next()
 		
-	app.use (req, res, next) ->
+	webRouter.use (req, res, next) ->
 		SystemMessageManager.getMessages (error, messages = []) ->
 			res.locals.systemMessages = messages
 			next()
 
-	app.use (req, res, next)->
+	webRouter.use (req, res, next)->
 		res.locals.query = req.query
 		next()
 
-	app.use (req, res, next)->
+	webRouter.use (req, res, next)->
 		subdomain = _.find Settings.i18n.subdomainLang, (subdomain)->
 			subdomain.lngCode == req.showUserOtherLng and !subdomain.hide
 		res.locals.recomendSubdomain = subdomain
 		res.locals.currentLngCode = req.lng
 		next()
 
-	app.use (req, res, next) ->
+	webRouter.use (req, res, next) ->
 		if Settings.reloadModuleViewsOnEachRequest
 			Modules.loadViewIncludes()
 		res.locals.moduleIncludes = Modules.moduleIncludes

+ 2 - 2
services/web/app/coffee/infrastructure/Modules.coffee

@@ -13,9 +13,9 @@ module.exports = Modules =
 				loadedModule.name = moduleName
 				@modules.push loadedModule
 
-	applyRouter: (app) ->
+	applyRouter: (webRouter, apiRouter) ->
 		for module in @modules
-			module.router?.apply(app)
+			module.router?.apply(webRouter, apiRouter)
 			
 	viewIncludes: {}
 	loadViewIncludes: (app) ->

+ 16 - 0
services/web/app/coffee/infrastructure/Mongoose.coffee

@@ -0,0 +1,16 @@
+mongoose = require('mongoose')
+Settings = require 'settings-sharelatex'
+logger = require('logger-sharelatex')
+
+mongoose.connect(Settings.mongo.url, server: poolSize: 10)
+
+mongoose.connection.on 'connected', () ->
+	logger.log {url:Settings.mongo.url}, 'mongoose default connection open'
+
+mongoose.connection.on 'error', (err) ->
+	logger.err err:err, 'mongoose error on default connection';
+
+mongoose.connection.on 'disconnected', () ->
+	logger.log 'mongoose default connection disconnected'
+
+module.exports = mongoose

+ 66 - 49
services/web/app/coffee/infrastructure/Server.coffee

@@ -7,14 +7,22 @@ crawlerLogger = require('./CrawlerLogger')
 expressLocals = require('./ExpressLocals')
 Router = require('../router')
 metrics.inc("startup")
-
 redis = require("redis-sharelatex")
 rclient = redis.createClient(Settings.redis.web)
 
-RedisStore = require('connect-redis')(express)
+session = require("express-session")
+RedisStore = require('connect-redis')(session)
+bodyParser = require('body-parser')
+multer  = require('multer')
+methodOverride = require('method-override')
+csrf = require('csurf')
+csrfProtection = csrf()
+cookieParser = require('cookie-parser')
+
 sessionStore = new RedisStore(client:rclient)
 
-cookieParser = express.cookieParser(Settings.security.sessionSecret)
+Mongoose = require("./Mongoose")
+
 oneDayInMilliseconds = 86400000
 ReferalConnect = require('../Features/Referal/ReferalConnect')
 RedirectManager = require("./RedirectManager")
@@ -25,6 +33,8 @@ Modules = require "./Modules"
 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)
 
+metrics.event_loop?.monitor(logger)
+
 Settings.editorIsOpen ||= true
 
 if Settings.cacheStaticAssets
@@ -34,56 +44,59 @@ else
 
 app = express()
 
-csrf = express.csrf()
-ignoreCsrfRoutes = []
-app.ignoreCsrf = (method, route) ->
-	ignoreCsrfRoutes.push new express.Route(method, route)
-
-
-app.configure () ->
-	if Settings.behindProxy
-		app.enable('trust proxy')
-	app.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge })
-	app.set 'views', __dirname + '/../../views'
-	app.set 'view engine', 'jade'
-	Modules.loadViewIncludes app
-	app.use express.bodyParser(uploadDir: Settings.path.uploadFolder)
-	app.use translations.expressMiddlewear
-	app.use translations.setLangBasedOnDomainMiddlewear
-	app.use cookieParser
-	app.use express.session
-		proxy: Settings.behindProxy
-		cookie:
-			domain: Settings.cookieDomain
-			maxAge: Settings.cookieSessionLength
-			secure: Settings.secureCookie
-		store: sessionStore
-		key: Settings.cookieName
-	
-	# Measure expiry from last request, not last login
-	app.use (req, res, next) ->
-		req.session.touch()
-		next()
-	
-	app.use (req, res, next) ->
-		for route in ignoreCsrfRoutes
-			if route.method == req.method?.toLowerCase() and route.match(req.path)
-				return next()
-		csrf(req, res, next)
+webRouter = express.Router()
+apiRouter = express.Router()
 
-	app.use ReferalConnect.use
-	app.use express.methodOverride()
+if Settings.behindProxy
+	app.enable('trust proxy')
 
-expressLocals(app)
+webRouter.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge })
+app.set 'views', __dirname + '/../../views'
+app.set 'view engine', 'jade'
+Modules.loadViewIncludes app
 
-app.configure 'production', ->
-	logger.info "Production Enviroment"
-	app.enable('view cache')
+
+
+app.use bodyParser.urlencoded({ extended: true, limit: "2mb"})
+app.use bodyParser.json({limit: "2mb"})
+app.use multer(dest: Settings.path.uploadFolder)
+app.use methodOverride()
 
 app.use metrics.http.monitor(logger)
 app.use RedirectManager
 app.use OldAssetProxy
 
+
+webRouter.use cookieParser(Settings.security.sessionSecret)
+webRouter.use session
+	resave: false
+	saveUninitialized:false
+	secret:Settings.security.sessionSecret
+	proxy: Settings.behindProxy
+	cookie:
+		domain: Settings.cookieDomain
+		maxAge: Settings.cookieSessionLength
+		secure: Settings.secureCookie
+	store: sessionStore
+	key: Settings.cookieName
+webRouter.use csrfProtection
+webRouter.use translations.expressMiddlewear
+webRouter.use translations.setLangBasedOnDomainMiddlewear
+
+# Measure expiry from last request, not last login
+webRouter.use (req, res, next) ->
+	req.session.touch()
+	next()
+
+webRouter.use ReferalConnect.use
+expressLocals(app, webRouter, apiRouter)
+
+if app.get('env') == 'production'
+	logger.info "Production Enviroment"
+	app.enable('view cache')
+
+
+
 app.use (req, res, next)->
 	metrics.inc "http-request"
 	crawlerLogger.log(req)
@@ -96,12 +109,11 @@ app.use (req, res, next) ->
 	else
 		next()
 
-app.get "/status", (req, res)->
+apiRouter.get "/status", (req, res)->
 	res.send("web sharelatex is alive")
-	req.session.destroy()
 	
 profiler = require "v8-profiler"
-app.get "/profile", (req, res) ->
+apiRouter.get "/profile", (req, res) ->
 	time = parseInt(req.query.time || "1000")
 	profiler.startProfiling("test")
 	setTimeout () ->
@@ -112,7 +124,12 @@ app.get "/profile", (req, res) ->
 logger.info ("creating HTTP server").yellow
 server = require('http').createServer(app)
 
-router = new Router(app)
+# process api routes first, if nothing matched fall though and use
+# web middlewear + routes
+app.use(apiRouter)
+app.use(webRouter)
+
+router = new Router(webRouter, apiRouter)
 
 module.exports =
 	app: app

+ 4 - 1
services/web/app/coffee/models/Project.coffee

@@ -17,6 +17,8 @@ DeletedDocSchema = new Schema
 ProjectSchema = new Schema
 	name              :   {type:String, default:'new project'}
 	lastUpdated       :   {type:Date, default: () -> new Date()}
+	lastOpened		  :   {type:Date}
+	active			  :   { type: Boolean,  default: true }
 	owner_ref         :   {type:ObjectId, ref:'User'}
 	collaberator_refs :   [ type:ObjectId, ref:'User' ]
 	readOnly_refs     :   [ type:ObjectId, ref:'User' ]
@@ -26,7 +28,6 @@ ProjectSchema = new Schema
 	compiler		  :   {type:String, default:'pdflatex'}
 	spellCheckLanguage :   {type:String, default:'en'}
 	deletedByExternalDataSource : {type: Boolean, default: false}
-	useClsi2          :   {type:Boolean, default: true}
 	description : {type:String, default:''}
 	archived          : { type: Boolean }
 	deletedDocs       : [DeletedDocSchema]
@@ -42,6 +43,7 @@ ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
 		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')
@@ -54,6 +56,7 @@ ProjectSchema.statics.findPopulatedById = (project_id, callback)->
 					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)->

+ 105 - 97
services/web/app/coffee/router.coffee

@@ -35,71 +35,72 @@ WikiController = require("./Features/Wiki/WikiController")
 Modules = require "./infrastructure/Modules"
 RateLimiterMiddlewear = require('./Features/Security/RateLimiterMiddlewear')
 RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter')
+InactiveProjectController = require("./Features/InactiveData/InactiveProjectController")
 
 logger = require("logger-sharelatex")
 _ = require("underscore")
 
 module.exports = class Router
-	constructor: (app)->
+	constructor: (webRouter, apiRouter)->
 		if !Settings.allowPublicAccess
-			app.all '*', AuthenticationController.requireGlobalLogin
+			webRouter.all '*', AuthenticationController.requireGlobalLogin
 
-		app.use(app.router)
 		
-		app.get  '/login', UserPagesController.loginPage
+		webRouter.get  '/login', UserPagesController.loginPage
 		AuthenticationController.addEndpointToLoginWhitelist '/login'
 
-		app.post '/login', AuthenticationController.login
-		app.get  '/logout', UserController.logout
-		app.get  '/restricted', SecurityManager.restricted
+		webRouter.post '/login', AuthenticationController.login
+		webRouter.get  '/logout', UserController.logout
+		webRouter.get  '/restricted', SecurityManager.restricted
 
 		# Left as a placeholder for implementing a public register page
-		app.get  '/register', UserPagesController.registerPage
+		webRouter.get  '/register', UserPagesController.registerPage
 		AuthenticationController.addEndpointToLoginWhitelist '/register'
 
-		EditorRouter.apply(app)
-		CollaboratorsRouter.apply(app)
-		SubscriptionRouter.apply(app)
-		UploadsRouter.apply(app)
-		PasswordResetRouter.apply(app)
-		StaticPagesRouter.apply(app)
-		RealTimeProxyRouter.apply(app)
+
+		EditorRouter.apply(webRouter, apiRouter)
+		CollaboratorsRouter.apply(webRouter, apiRouter)
+		SubscriptionRouter.apply(webRouter, apiRouter)
+		UploadsRouter.apply(webRouter, apiRouter)
+		PasswordResetRouter.apply(webRouter, apiRouter)
+		StaticPagesRouter.apply(webRouter, apiRouter)
+		RealTimeProxyRouter.apply(webRouter, apiRouter)
 		
-		Modules.applyRouter(app)
+		Modules.applyRouter(webRouter, apiRouter)
 
-		app.get '/blog', BlogController.getIndexPage
-		app.get '/blog/*', BlogController.getPage
 
 		if Settings.enableSubscriptions
-			app.get  '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus
-
-		app.get  '/user/settings', AuthenticationController.requireLogin(), UserPagesController.settingsPage
-		app.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings
-		app.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword
+			webRouter.get  '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus
+		
+		webRouter.get '/blog', BlogController.getIndexPage
+		webRouter.get '/blog/*', BlogController.getPage
+		
+		webRouter.get  '/user/settings', AuthenticationController.requireLogin(), UserPagesController.settingsPage
+		webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings
+		webRouter.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword
 
-		app.del  '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe
-		app.del  '/user', AuthenticationController.requireLogin(), UserController.deleteUser
+		webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe
+		webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser
 
-		app.get  '/user/auth_token', AuthenticationController.requireLogin(), AuthenticationController.getAuthToken
-		app.get  '/user/personal_info', AuthenticationController.requireLogin(allow_auth_token: true), UserInfoController.getLoggedInUsersPersonalInfo
-		app.get  '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo
+		webRouter.get  '/user/auth_token', AuthenticationController.requireLogin(), AuthenticationController.getAuthToken
+		webRouter.get  '/user/personal_info', AuthenticationController.requireLogin(allow_auth_token: true), UserInfoController.getLoggedInUsersPersonalInfo
+		apiRouter.get  '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo
 
-		app.get  '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage
-		app.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject
+		webRouter.get  '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage
+		webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject
 
-		app.get  '/Project/:Project_id', RateLimiterMiddlewear.rateLimit({
+		webRouter.get  '/Project/:Project_id', RateLimiterMiddlewear.rateLimit({
 			endpointName: "open-project"
 			params: ["Project_id"]
 			maxRequests: 10
 			timeInterval: 60
 		}), SecurityManager.requestCanAccessProject, ProjectController.loadEditor
-		app.get  '/Project/:Project_id/file/:File_id', SecurityManager.requestCanAccessProject, FileStoreController.getFile
+		webRouter.get  '/Project/:Project_id/file/:File_id', SecurityManager.requestCanAccessProject, FileStoreController.getFile
+		webRouter.post '/project/:Project_id/settings', SecurityManager.requestCanModifyProject, ProjectController.updateProjectSettings
 
-		app.post '/project/:Project_id/settings', SecurityManager.requestCanModifyProject, ProjectController.updateProjectSettings
-
-		app.post '/project/:Project_id/compile', SecurityManager.requestCanAccessProject, CompileController.compile
-		app.get  '/Project/:Project_id/output/output.pdf', SecurityManager.requestCanAccessProject, CompileController.downloadPdf
-		app.get  /^\/project\/([^\/]*)\/output\/(.*)$/,
+		webRouter.post '/project/:Project_id/compile', SecurityManager.requestCanAccessProject, CompileController.compile
+		webRouter.get  '/Project/:Project_id/output/output.pdf', SecurityManager.requestCanAccessProject, CompileController.downloadPdf
+		webRouter.get  /^\/project\/([^\/]*)\/output\/(.*)$/,
 			((req, res, next) ->
 				params =
 					"Project_id": req.params[0]
@@ -107,78 +108,86 @@ module.exports = class Router
 				req.params = params
 				next()
 			), SecurityManager.requestCanAccessProject, CompileController.getFileFromClsi
-		app.del "/project/:Project_id/output", SecurityManager.requestCanAccessProject, CompileController.deleteAuxFiles
-		app.get "/project/:Project_id/sync/code", SecurityManager.requestCanAccessProject, CompileController.proxySync
-		app.get "/project/:Project_id/sync/pdf", SecurityManager.requestCanAccessProject, CompileController.proxySync
+		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
 
-		app.del  '/Project/:Project_id', SecurityManager.requestIsOwner, ProjectController.deleteProject
-		app.post '/Project/:Project_id/restore', SecurityManager.requestIsOwner, ProjectController.restoreProject
-		app.post '/Project/:Project_id/clone', SecurityManager.requestCanAccessProject, ProjectController.cloneProject
+		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
 
-		app.post '/project/:Project_id/rename', SecurityManager.requestIsOwner, ProjectController.renameProject
+		webRouter.post '/project/:Project_id/rename', SecurityManager.requestIsOwner, ProjectController.renameProject
 
-		app.get  "/project/:Project_id/updates", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
-		app.get  "/project/:Project_id/doc/:doc_id/diff", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
-		app.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
+		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
 
-		app.get  '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject
-		app.get  '/project/download/zip', SecurityManager.requestCanAccessMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
+		webRouter.get  '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject
+		webRouter.get  '/project/download/zip', SecurityManager.requestCanAccessMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
 
-		app.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags
-		app.post '/project/:project_id/tag', AuthenticationController.requireLogin(), TagsController.processTagsUpdate
+		webRouter.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags
+		webRouter.post '/project/:project_id/tag', AuthenticationController.requireLogin(), TagsController.processTagsUpdate
 
-		app.get  '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails
+		# Deprecated in favour of /internal/project/:project_id but still used by versioning
+		apiRouter.get  '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails
 
-		app.get '/internal/project/:Project_id/zip', AuthenticationController.httpAuth, ProjectDownloadsController.downloadProject
-		app.get '/internal/project/:project_id/compile/pdf', AuthenticationController.httpAuth, CompileController.compileAndDownloadPdf
+		# New 'stable' /internal API end points
+		apiRouter.get  '/internal/project/:project_id',     AuthenticationController.httpAuth, ProjectApiController.getProjectDetails
+		apiRouter.get  '/internal/project/:Project_id/zip', AuthenticationController.httpAuth, ProjectDownloadsController.downloadProject
+		apiRouter.get  '/internal/project/:project_id/compile/pdf', AuthenticationController.httpAuth, CompileController.compileAndDownloadPdf
 
+		apiRouter.post '/internal/deactivateOldProjects', AuthenticationController.httpAuth, InactiveProjectController.deactivateOldProjects
+		apiRouter.post '/internal/project/:project_id/deactivate', AuthenticationController.httpAuth, InactiveProjectController.deactivateProject
 
-		app.get  '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.getDocument
-		app.post '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.setDocument
-		app.ignoreCsrf('post', '/project/:Project_id/doc/:doc_id')
+		webRouter.get  /^\/internal\/project\/([^\/]*)\/output\/(.*)$/,
+			((req, res, next) ->
+				params =
+					"Project_id": req.params[0]
+					"file":       req.params[1]
+				req.params = params
+				next()
+			), AuthenticationController.httpAuth, CompileController.getFileFromClsi
+
+		apiRouter.get  '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.getDocument
+		apiRouter.post '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.setDocument
 
-		app.post '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate
-		app.del  '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate
-		app.ignoreCsrf('post', '/user/:user_id/update/*')
-		app.ignoreCsrf('delete', '/user/:user_id/update/*')
+		apiRouter.post '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate
+		apiRouter.delete '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate
 		
-		app.post '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.updateProjectContents
-		app.del  '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.deleteProjectContents
-		app.ignoreCsrf('post', '/project/:project_id/contents/*')
-		app.ignoreCsrf('delete', '/project/:project_id/contents/*')
+		apiRouter.post '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.updateProjectContents
+		apiRouter.delete '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.deleteProjectContents
 
-		app.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
-		app.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
+		webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
+		webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
 
-		app.get  "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.getMessages
-		app.post "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.sendMessage
+		webRouter.get  "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.getMessages
+		webRouter.post "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.sendMessage
 		
-		app.get  /learn(\/.*)?/, WikiController.getPage
+		webRouter.get  /learn(\/.*)?/, WikiController.getPage
 
 		#Admin Stuff
-		app.get  '/admin', SecurityManager.requestIsAdmin, AdminController.index
-		app.get  '/admin/register', SecurityManager.requestIsAdmin, AdminController.registerNewUser
-		app.post '/admin/register', SecurityManager.requestIsAdmin, UserController.register
-		app.post '/admin/closeEditor', SecurityManager.requestIsAdmin, AdminController.closeEditor
-		app.post '/admin/dissconectAllUsers', SecurityManager.requestIsAdmin, AdminController.dissconectAllUsers
-		app.post '/admin/syncUserToSubscription', SecurityManager.requestIsAdmin, AdminController.syncUserToSubscription
-		app.post '/admin/flushProjectToTpds', SecurityManager.requestIsAdmin, AdminController.flushProjectToTpds
-		app.post '/admin/pollDropboxForUser', SecurityManager.requestIsAdmin, AdminController.pollDropboxForUser
-		app.post '/admin/messages', SecurityManager.requestIsAdmin, AdminController.createMessage
-		app.post '/admin/messages/clear', SecurityManager.requestIsAdmin, AdminController.clearMessages
-
-		app.get '/perfTest', (req,res)->
+		webRouter.get  '/admin', SecurityManager.requestIsAdmin, AdminController.index
+		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
+
+		apiRouter.get '/perfTest', (req,res)->
 			res.send("hello")
-			req.session.destroy()
 
-		app.get '/status', (req,res)->
+		apiRouter.get '/status', (req,res)->
 			res.send("websharelatex is up")
-			req.session.destroy()
+		
 
-		app.get '/health_check', HealthCheckController.check
-		app.get '/health_check/redis', HealthCheckController.checkRedis
+		webRouter.get '/health_check', HealthCheckController.check
+		webRouter.get '/health_check/redis', HealthCheckController.checkRedis
 
-		app.get "/status/compiler/:Project_id", SecurityManager.requestCanAccessProject, (req, res) ->
+		apiRouter.get "/status/compiler/:Project_id", SecurityManager.requestCanAccessProject, (req, res) ->
 			sendRes = _.once (statusCode, message)->
 				res.writeHead statusCode
 				res.end message
@@ -187,27 +196,26 @@ module.exports = class Router
 			setTimeout (() ->
 				sendRes 500, "Compiler timed out"
 			), 10000
-			req.session.destroy()
 
-		app.get "/ip", (req, res, next) ->
+		apiRouter.get "/ip", (req, res, next) ->
 			res.send({
 				ip: req.ip
 				ips: req.ips
 				headers: req.headers
 			})
 
-		app.get '/oops-express', (req, res, next) -> next(new Error("Test error"))
-		app.get '/oops-internal', (req, res, next) -> throw new Error("Test error")
-		app.get '/oops-mongo', (req, res, next) ->
+		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) ->
 			require("./models/Project").Project.findOne {}, () ->
 				throw new Error("Test error")
 
-		app.get '/opps-small', (req, res, next)->
+		apiRouter.get '/opps-small', (req, res, next)->
 			logger.err "test error occured"
 			res.send()
 
-		app.post '/error/client', (req, res, next) ->
+		webRouter.post '/error/client', (req, res, next) ->
 			logger.error err: req.body.error, meta: req.body.meta, "client side error"
-			res.send(204)
+			res.sendStatus(204)
 
-		app.get '*', ErrorController.notFound
+		webRouter.get '*', ErrorController.notFound

+ 28 - 9
services/web/app/views/project/editor/header.jade

@@ -57,15 +57,34 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
 			ng-show="onlineUsersArray.length > 0"
 			ng-controller="OnlineUsersController"
 		)
-			span.online-user(
-				ng-repeat="user in onlineUsersArray",
-				ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }",
-				popover="{{ user.name }}"
-				popover-placement="bottom"
-				popover-append-to-body="true"
-				popover-trigger="mouseenter"
-				ng-click="gotoUser(user)"
-			) {{ user.name.slice(0,1) }}
+			span(ng-if="onlineUsersArray.length < 4")
+				span.online-user(
+					ng-repeat="user in onlineUsersArray",
+					ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }",
+					popover="{{ user.name }}"
+					popover-placement="bottom"
+					popover-append-to-body="true"
+					popover-trigger="mouseenter"
+					ng-click="gotoUser(user)"
+				) {{ user.name.slice(0,1) }}
+			
+			span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
+				span.online-user.online-user-multi(
+					dropdown-toggle,
+					tooltip="#{translate('connected_users')}",
+					tooltip-placement="left"
+				)
+					strong {{ onlineUsersArray.length }}
+					i.fa.fa-fw.fa-user
+				ul.dropdown-menu.pull-right
+					li.dropdown-header #{translate('connected_users')}
+					li(ng-repeat="user in onlineUsersArray")
+						a(href, ng-click="gotoUser(user)")
+							span.online-user(
+								ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }"
+							) {{ user.name.slice(0,1) }}
+							| {{ user.name }}
+
 	
 		a.btn.btn-full-height(
 			href,

+ 5 - 0
services/web/app/views/project/editor/left-menu.jade

@@ -48,6 +48,11 @@ aside#left-menu.full-size(
 			h4() #{translate("sync")}
 			!= moduleIncludes("editorLeftMenu:sync", locals)
 
+	span(ng-show="!anonymous")
+		h4 #{translate("services")}
+		!= moduleIncludes("editorLeftMenu:editing_services", locals)
+
+
 	h4(ng-show="!anonymous") #{translate("settings")}
 	form.settings(ng-controller="SettingsController", ng-show="!anonymous")
 		.containter-fluid

+ 3 - 3
services/web/app/views/project/editor/share.jade

@@ -48,8 +48,8 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 					.small #{translate("share_with_your_collabs")}
 					.form-group
 						input.form-control(
-							type="email"
-							placeholder="Enter email address..."
+							type="text"
+							placeholder="joe@example.com, sue@example.com, ..."
 							ng-model="inputs.email"
 							focus-on="open"
 						)
@@ -64,7 +64,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 							| &nbsp;&nbsp;
 							button.btn.btn-info(
 								type="submit"
-								ng-click="addMember()"
+								ng-click="addMembers()"
 							) #{translate("share")}
 				div.text-center(ng-hide="canAddCollaborators")
 					p #{translate("need_to_upgrade_for_more_collabs")}.

+ 26 - 12
services/web/app/views/project/list/side-bar.jade

@@ -71,19 +71,33 @@
 			strong #{translate("create_your_first_project")}
 
 - if (showUserDetailsArea)
-	.row-spaced#userProfileInformation(ng-if="projects.length > 0", ng-cloak)
-		div(ng-controller="UserProfileController")
-			hr(ng-show="percentComplete < 100")
-			.text-centered.user-profile(ng-show="percentComplete < 100")
-				.progress
-					.progress-bar.progress-bar-info(ng-style="{'width' : (percentComplete+'%')}")
-
-				p.small #{translate("profile_complete_percentage", {percentval:"{{percentComplete}}"})}
+	- if (Math.random() < 0.5)
+		.row-spaced
+			hr
+			.card.card-thin
+				p.text-center.small
+					| <strong>Python</strong> or <strong>R</strong> user?
+				
+				p.text-center.small
+					a(href="https://www.getdatajoy.com/", target="_blank").btn.btn-info.btn-small Try DataJoy
 				
-				button#completeUserProfileInformation.btn.btn-info(
-					ng-hide="formVisable",
-					ng-click="openUserProfileModal()"
-				) #{translate("complete")}
+				p.text-center.small(style="font-size: 0.8em")
+					a(href="https://www.getdatajoy.com/", target="_blank") DataJoy
+					|  is a new online Python and R editor from ShareLaTeX.
+	- else
+		.row-spaced#userProfileInformation(ng-if="projects.length > 0", ng-cloak)
+			div(ng-controller="UserProfileController")
+				hr(ng-show="percentComplete < 100")
+				.text-centered.user-profile(ng-show="percentComplete < 100")
+					.progress
+						.progress-bar.progress-bar-info(ng-style="{'width' : (percentComplete+'%')}")
+
+					p.small #{translate("profile_complete_percentage", {percentval:"{{percentComplete}}"})}
+					
+					button#completeUserProfileInformation.btn.btn-info(
+						ng-hide="formVisable",
+						ng-click="openUserProfileModal()"
+					) #{translate("complete")}
 
 -if (settings.enableSubscriptions && !hasSubscription)
 	.row-spaced(ng-if="projects.length > 0", ng-cloak).text-centered

+ 13 - 5
services/web/package.json

@@ -12,22 +12,30 @@
   "dependencies": {
     "archiver": "0.9.0",
     "async": "0.6.2",
+    "base64-stream": "^0.1.2",
+    "basic-auth-connect": "^1.0.0",
     "bcrypt": "0.8.3",
+    "body-parser": "^1.13.1",
     "bufferedstream": "1.6.0",
-    "connect-redis": "1.4.5",
+    "connect-redis": "2.3.0",
+    "cookie-parser": "1.3.5",
+    "csurf": "^1.8.3",
     "dateformat": "1.0.4-1.2.3",
-    "express": "3.3.4",
-    "fairy": "0.0.2",
+    "express": "4.13.0",
+    "express-session": "1.11.3",
     "http-proxy": "^1.8.1",
     "jade": "~1.3.1",
     "ldapjs": "^0.7.1",
     "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.0.0",
     "lynx": "0.1.1",
-    "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.0.0",
+    "marked": "^0.3.3",
+    "method-override": "^2.3.3",
+    "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.2.0",
     "mimelib": "0.2.14",
     "mocha": "1.17.1",
     "mongojs": "0.18.2",
-    "mongoose": "3.8.28",
+    "mongoose": "4.1.0",
+    "multer": "^0.1.8",
     "node-uuid": "1.4.1",
     "nodemailer": "0.6.1",
     "optimist": "0.6.1",

+ 5 - 1
services/web/public/coffee/ide/online-users/OnlineUsersManager.coffee

@@ -56,7 +56,11 @@ define [
 					user.doc = @ide.fileTreeManager.findEntityById(user.doc_id)
 
 				if user.name?.trim().length == 0
-					user.name = user.email
+					user.name = user.email.trim()
+				
+				user.initial = user.name?[0]
+				if !user.initial or user.initial == " "
+					user.initial = "?"
 
 				@$scope.onlineUsersArray.push user
 

+ 24 - 9
services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee

@@ -22,19 +22,34 @@ define [
 			allowedNoOfMembers = $scope.project.features.collaborators
 			$scope.canAddCollaborators = noOfMembers < allowedNoOfMembers or allowedNoOfMembers == INFINITE_COLLABORATORS
 
-		$scope.addMember = () ->
+		$scope.addMembers = () ->
 			return if !$scope.inputs.email? or $scope.inputs.email == ""
+
+			emails = $scope.inputs.email.split(/,\s*/)
+			$scope.inputs.email = ""
 			$scope.state.error = null
 			$scope.state.inflight = true
-			projectMembers
-				.addMember($scope.inputs.email, $scope.inputs.privileges)
-				.success (data) ->
-					$scope.state.inflight = false
-					$scope.inputs.email = ""
-					$scope.project.members.push data?.user
-				.error () ->
+
+			do addNextMember = () ->
+				if emails.length == 0 or !$scope.canAddCollaborators
 					$scope.state.inflight = false
-					$scope.state.error = "Sorry, something went wrong :("
+					$scope.$apply()
+					return
+				
+				email = emails.shift()
+				projectMembers
+					.addMember(email, $scope.inputs.privileges)
+					.success (data) ->
+						if data?.user # data.user is false if collaborator limit is hit.
+							$scope.project.members.push data.user
+							setTimeout () ->
+								# Give $scope a chance to update $scope.canAddCollaborators
+								# with new collaborator information.
+								addNextMember()
+							, 0
+					.error () ->
+						$scope.state.inflight = false
+						$scope.state.error = "Sorry, something went wrong :("
 
 
 		$scope.removeMember = (member) ->

+ 0 - 1
services/web/public/coffee/main.coffee

@@ -3,7 +3,6 @@ define [
 	"main/user-details"
 	"main/account-settings"
 	"main/account-upgrade"
-	"main/templates"
 	"main/plans"
 	"main/group-members"
 	"main/scribtex-popup"

+ 0 - 77
services/web/public/coffee/main/templates.coffee

@@ -1,77 +0,0 @@
-define [
-	"base"
-], (App) ->
-
-	App.controller "openInSlController", ($scope) ->
-
-		$scope.openInSlText = "Open in ShareLaTeX"
-		$scope.isDisabled = false
-
-		$scope.open = ->
-			$scope.openInSlText = "Creating..."
-			$scope.isDisabled = true
-			ga('send', 'event', 'template-site', 'open-in-sl', $('.page-header h1').text())
-
-		$scope.downloadZip = ->
-			ga('send', 'event', 'template-site', 'download-zip', $('.page-header h1').text())
-
-
-	App.factory "algolia", ->
-		if window?.sharelatex?.algolia?.app_id?
-			client = new AlgoliaSearch(window.sharelatex.algolia?.app_id, window.sharelatex.algolia?.api_key)
-			index = client.initIndex(window.sharelatex.algolia?.indexes?.templates)
-			return index
-
-
-
-	App.controller "SearchController", ($scope, algolia, _) ->
-		$scope.hits = []
-
-		$scope.clearSearchText = ->
-			$scope.searchQueryText = ""
-			updateHits []
-
-		$scope.safeApply = (fn)->
-			phase = $scope.$root.$$phase
-			if(phase == '$apply' || phase == '$digest')
-				$scope.$eval(fn)
-			else
-				$scope.$apply(fn)
-
-		buildHitViewModel = (hit)->
-			result = 
-				name : hit._highlightResult.name.value
-				description: hit._highlightResult.description.value
-				url :"/templates/#{hit._id}"
-				image_url: "#{window.sharelatex?.templates?.cdnDomain}/#{hit._id}/v/#{hit.version}/pdf-converted-cache/style-thumbnail"
-
-		updateHits = (hits)->
-			$scope.safeApply ->
-				$scope.hits = hits
-
-		$scope.search = ->
-			query = $scope.searchQueryText
-			if !query? or query.length == 0
-				updateHits []
-				return
-
-			query = "#{window.sharelatex?.templates?.user_id} #{query}"
-			algolia.search query, (err, response)->
-				if response.hits.length == 0
-					updateHits []
-				else
-					hits = _.map response.hits, buildHitViewModel
-					updateHits hits
-
-
-	App.controller "MissingTemplateController", ($scope, $modal)->
-		$scope.showMissingTemplateModal = ->
-			$modal.open {
-				templateUrl: "missingTemplateModal"
-				controller:"MissingTemplateModalController"
-			}
-
-	App.controller "MissingTemplateModalController", ($scope, $modalInstance) ->
-		$scope.cancel = () ->
-			$modalInstance.dismiss()
-

BIN
services/web/public/img/about/geri.jpg


BIN
services/web/public/img/about/kiri_channon.jpg


BIN
services/web/public/img/about/shane_kilkelly.jpg


BIN
services/web/public/img/enago.png


+ 1 - 0
services/web/public/stylesheets/app/editor.less

@@ -3,6 +3,7 @@
 @import "./editor/toolbar.less";
 @import "./editor/left-menu.less";
 @import "./editor/pdf.less";
+@import "./editor/enago.less";
 @import "./editor/share.less";
 @import "./editor/chat.less";
 @import "./editor/binary-file.less";

+ 13 - 0
services/web/public/stylesheets/app/editor/enago.less

@@ -0,0 +1,13 @@
+.services {
+	h1, h2, h3, p {
+		text-shadow: 0 -1px 1px white;
+	}
+	h1, h2, h3, h4 {
+		color: @red;
+	}
+
+
+	hr.small {
+		margin:0px;
+	}
+}

+ 26 - 1
services/web/public/stylesheets/app/editor/online-users.less

@@ -1,6 +1,8 @@
+@online-user-color: rgb(0, 170, 255);
+
 .online-users {
 	.online-user {
-		background-color: rgb(0, 170, 255);
+		background-color: @online-user-color;
 		width: 24px;
 		display: inline-block;
 		height: 24px;
@@ -11,4 +13,27 @@
 		border-radius: 3px;
 		cursor: pointer;
 	}
+	
+	.online-user-multi {
+		width: auto;
+		min-width: 24px;
+		padding-left: 8px;
+		padding-right: 5px;
+	}
+	
+	.dropdown-menu {
+		a {
+			// Override toolbar link styles
+			display: block;
+			padding: 4px 10px 5px;
+			margin: 1px 2px;
+			color: @text-color;
+			&:hover, &:active {
+				color: @text-color;
+				background-color: @gray-lightest;
+				text-shadow: none;
+				.box-shadow(none);
+			}
+		}
+	}
 }

+ 3 - 0
services/web/public/stylesheets/app/templates.less

@@ -49,6 +49,9 @@
 
 .template-details-section {
 	padding-bottom: 20px;
+  .btn {
+		margin-left: 6px;
+  }
 }
 
 .searchResult {

+ 4 - 0
services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee

@@ -423,6 +423,7 @@ describe "AuthenticationController", ->
 		beforeEach ->
 			@req.session =
 				save: sinon.stub().callsArg(0)
+				destroy : sinon.stub()
 			@req.sessionStore =
 				generate: sinon.stub()
 			@AuthenticationController.establishUserSession @req, @user, @callback
@@ -435,6 +436,9 @@ describe "AuthenticationController", ->
 			@req.session.user.referal_id.should.equal @user.referal_id
 			@req.session.user.isAdmin.should.equal @user.isAdmin
 			
+		it "should destroy the session", ->
+			@req.session.destroy.called.should.equal true
+			
 		it "should regenerate the session to protect against session fixation", ->
 			@req.sessionStore.generate.calledWith(@req).should.equal true
 

+ 2 - 2
services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee

@@ -91,7 +91,7 @@ describe "CollaboratorsController", ->
 			@req.params =
 				Project_id: @project_id = "project-id-123"
 				user_id: @user_id = "user-id-123"
-			@res.send = sinon.stub()
+			@res.sendStatus = sinon.stub()
 			@EditorController.removeUserFromProject = sinon.stub().callsArg(2)
 			@CollaboratorsController.removeUserFromProject @req, @res
 			
@@ -101,7 +101,7 @@ describe "CollaboratorsController", ->
 				.should.equal true
 				
 		it "should send the back a success response", ->
-			@res.send.calledWith(204).should.equal true
+			@res.sendStatus.calledWith(204).should.equal true
 
 
 	describe "_formatCollaborators", ->

+ 2 - 3
services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee

@@ -98,7 +98,6 @@ describe "CompileController", ->
 
 		describe "when downloading for embedding", ->
 			beforeEach ->
-				@project.useClsi2 = true
 				@CompileController.proxyToClsi = sinon.stub()
 				@CompileController.downloadPdf(@req, @res, @next)
 
@@ -321,7 +320,7 @@ describe "CompileController", ->
 			@CompileManager.deleteAuxFiles = sinon.stub().callsArg(1)
 			@req.params =
 				Project_id: @project_id
-			@res.send = sinon.stub()
+			@res.sendStatus = sinon.stub()
 			@CompileController.deleteAuxFiles @req, @res, @next
 
 		it "should proxy to the CLSI", ->
@@ -330,7 +329,7 @@ describe "CompileController", ->
 				.should.equal true
 
 		it "should return a 200", ->
-			@res.send
+			@res.sendStatus
 				.calledWith(200)
 				.should.equal true
 

+ 40 - 1
services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee

@@ -13,7 +13,7 @@ describe "DocstoreManager", ->
 				apis:
 					docstore:
 						url: "docstore.sharelatex.com"
-			"logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub()}
+			"logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub(), err:->}
 
 		@requestDefaults.calledWith(jar: false).should.equal true
 
@@ -179,3 +179,42 @@ describe "DocstoreManager", ->
 						project_id: @project_id
 					}, "error getting all docs from docstore")
 					.should.equal true
+
+
+	describe "archiveProject", ->
+		describe "with a successful response code", ->
+			beforeEach ->
+				@request.post = sinon.stub().callsArgWith(1, null, statusCode: 204)
+				@DocstoreManager.archiveProject @project_id, @callback
+
+			it "should call the callback", ->
+				@callback.called.should.equal true
+
+		describe "with a failed response code", ->
+			beforeEach ->
+				@request.post = sinon.stub().callsArgWith(1, null, statusCode: 500)
+				@DocstoreManager.archiveProject @project_id, @callback
+
+			it "should call the callback with an error", ->
+				@callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true
+
+
+
+	describe "unarchiveProject", ->
+		describe "with a successful response code", ->
+			beforeEach ->
+				@request.post = sinon.stub().callsArgWith(1, null, statusCode: 204)
+				@DocstoreManager.unarchiveProject @project_id, @callback
+
+			it "should call the callback", ->
+				@callback.called.should.equal true
+
+		describe "with a failed response code", ->
+			beforeEach ->
+				@request.post = sinon.stub().callsArgWith(1, null, statusCode: 500)
+				@DocstoreManager.unarchiveProject @project_id, @callback
+
+			it "should call the callback with an error", ->
+				@callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true
+
+

+ 4 - 1
services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee

@@ -11,7 +11,10 @@ Errors = require "../../../../app/js/errors"
 
 describe "DocumentController", ->
 	beforeEach ->
-		@DocumentController = SandboxedModule.require modulePath, requires:  
+		@DocumentController = SandboxedModule.require modulePath, requires:
+			"logger-sharelatex":
+				log:->
+				err:->
 			"../Project/ProjectEntityHandler": @ProjectEntityHandler = {}
 		@res = new MockResponse()
 		@req = new MockRequest()

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

@@ -24,6 +24,7 @@ describe "EditorHttpController", ->
 		@req = {}
 		@res =
 			send: sinon.stub()
+			sendStatus: sinon.stub()
 			json: sinon.stub()
 		@callback = sinon.stub()
 			
@@ -190,7 +191,7 @@ describe "EditorHttpController", ->
 				@EditorHttpController.addDoc @req, @res
 
 			it "should send back a bad request status code", ->
-				@res.send.calledWith(400).should.equal true
+				@res.sendStatus.calledWith(400).should.equal true
 
 	describe "addFolder", ->
 		beforeEach ->
@@ -223,7 +224,7 @@ describe "EditorHttpController", ->
 				@EditorHttpController.addFolder @req, @res
 
 			it "should send back a bad request status code", ->
-				@res.send.calledWith(400).should.equal true
+				@res.sendStatus.calledWith(400).should.equal true
 
 
 	describe "renameEntity", ->
@@ -243,7 +244,7 @@ describe "EditorHttpController", ->
 				.should.equal true
 
 		it "should send back a success response", ->
-			@res.send.calledWith(204).should.equal true
+			@res.sendStatus.calledWith(204).should.equal true
 
 	describe "renameEntity with long name", ->
 		beforeEach ->
@@ -257,7 +258,7 @@ describe "EditorHttpController", ->
 			@EditorHttpController.renameEntity @req, @res
 
 		it "should send back a bad request status code", ->
-			@res.send.calledWith(400).should.equal true
+			@res.sendStatus.calledWith(400).should.equal true
 
 	describe "rename entity with 0 length name", ->
 
@@ -272,7 +273,7 @@ describe "EditorHttpController", ->
 			@EditorHttpController.renameEntity @req, @res
 
 		it "should send back a bad request status code", ->
-			@res.send.calledWith(400).should.equal true
+			@res.sendStatus.calledWith(400).should.equal true
 
 
 	describe "moveEntity", ->
@@ -292,7 +293,7 @@ describe "EditorHttpController", ->
 				.should.equal true
 
 		it "should send back a success response", ->
-			@res.send.calledWith(204).should.equal true
+			@res.sendStatus.calledWith(204).should.equal true
 
 	describe "deleteEntity", ->
 		beforeEach ->
@@ -309,4 +310,4 @@ describe "EditorHttpController", ->
 				.should.equal true
 
 		it "should send back a success response", ->
-			@res.send.calledWith(204).should.equal true
+			@res.sendStatus.calledWith(204).should.equal true

+ 67 - 5
services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee

@@ -21,14 +21,15 @@ describe "FileStoreController", ->
 		@stream = {}
 		@project_id = "2k3j1lk3j21lk3j"
 		@file_id = "12321kklj1lk3jk12"
-		@req = 
+		@req =
 			params:
 				Project_id: @project_id
 				File_id: @file_id
 			query: "query string here"
-		@res = 
+			get: (key) -> undefined
+		@res =
 			setHeader: sinon.stub()
-		@file = 
+		@file =
 			name: "myfile.png"
 
 	describe "getFile", ->
@@ -49,7 +50,7 @@ describe "FileStoreController", ->
 				done()
 			@controller.getFile @req, @res
 
-		it "should get the file from the db", (done)->	
+		it "should get the file from the db", (done)->
 			@stream.pipe = (des)=>
 				opts =
 					project_id: @project_id
@@ -64,5 +65,66 @@ describe "FileStoreController", ->
 				@res.setHeader.calledWith("Content-Disposition", "attachment; filename=#{@file.name}").should.equal true
 				done()
 			@controller.getFile @req, @res
-			
 
+		# Test behaviour around handling html files
+		['.html', '.htm', '.xhtml'].forEach (extension) ->
+			describe "with a '#{extension}' file extension", ->
+
+				beforeEach ->
+					@user_agent = 'A generic browser'
+					@file.name = "bad#{extension}"
+					@req.get = (key) =>
+						if key == 'User-Agent'
+							@user_agent
+
+				describe "from a non-ios browser", ->
+
+					it "should not set Content-Type", (done) ->
+						@stream.pipe = (des) =>
+							@res.setHeader.calledWith("Content-Type", "text/plain").should.equal false
+							done()
+						@controller.getFile @req, @res
+
+				describe "from an iPhone", ->
+
+					beforeEach ->
+						@user_agent = "An iPhone browser"
+
+					it "should set Content-Type to 'text/plain'", (done) ->
+						@stream.pipe = (des) =>
+							@res.setHeader.calledWith("Content-Type", "text/plain").should.equal true
+							done()
+						@controller.getFile @req, @res
+
+				describe "from an iPad", ->
+
+					beforeEach ->
+						@user_agent = "An iPad browser"
+
+					it "should set Content-Type to 'text/plain'", (done) ->
+						@stream.pipe = (des) =>
+							@res.setHeader.calledWith("Content-Type", "text/plain").should.equal true
+							done()
+						@controller.getFile @req, @res
+
+		# None of these should trigger the iOS/html logic
+		['x.html-is-rad', 'html.pdf', '.html-is-good-for-hidden-files', 'somefile'].forEach (filename) ->
+			describe "with filename as '#{filename}'", ->
+
+				beforeEach ->
+					@user_agent = 'A generic browser'
+					@file.name = filename
+					@req.get = (key) =>
+						if key == 'User-Agent'
+							@user_agent
+
+				['iPhone', 'iPad', 'Firefox', 'Chrome'].forEach (browser) ->
+					describe "downloaded from #{browser}", ->
+						beforeEach ->
+							@user_agent = "Some #{browser} thing"
+
+						it 'Should not set the Content-type', (done) ->
+							@stream.pipe = (des) =>
+								@res.setHeader.calledWith("Content-Type", "text/plain").should.equal false
+								done()
+							@controller.getFile @req, @res

+ 86 - 0
services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee

@@ -0,0 +1,86 @@
+should = require('chai').should()
+SandboxedModule = require('sandboxed-module')
+assert = require('assert')
+path = require('path')
+sinon = require('sinon')
+modulePath = path.join __dirname, "../../../../app/js/Features/InactiveData/InactiveProjectManager"
+expect = require("chai").expect
+
+describe "InactiveProjectManager", ->
+
+	beforeEach ->
+
+		@settings = {}
+		@DocstoreManager =
+			unarchiveProject:sinon.stub()
+			archiveProject:sinon.stub()
+		@ProjectUpdateHandler = 
+			markAsActive:sinon.stub()
+			markAsInactive:sinon.stub()
+		@ProjectGetter = 
+			getProject:sinon.stub()
+		@InactiveProjectManager = SandboxedModule.require modulePath, requires:
+			"settings-sharelatex":@settings
+			"logger-sharelatex": 
+				log:->
+				err:->
+			"../Docstore/DocstoreManager":@DocstoreManager
+			"../Project/ProjectUpdateHandler":@ProjectUpdateHandler
+			"../Project/ProjectGetter":@ProjectGetter
+
+		@project_id = "1234"
+
+	describe "reactivateProjectIfRequired", ->
+
+		beforeEach ->
+			@project = {active:false}
+			@ProjectGetter.getProject.callsArgWith(2, null, @project)
+			@ProjectUpdateHandler.markAsActive.callsArgWith(1)
+
+		it "should call unarchiveProject", (done)->
+			@DocstoreManager.unarchiveProject.callsArgWith(1)
+			@InactiveProjectManager.reactivateProjectIfRequired @project_id, (err)=>
+				@DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal true
+				@ProjectUpdateHandler.markAsActive.calledWith(@project_id).should.equal true
+				done()
+
+		it "should not mark project as active if error with unarchinging", (done)->
+			@DocstoreManager.unarchiveProject.callsArgWith(1, "error")
+			@InactiveProjectManager.reactivateProjectIfRequired @project_id, (err)=>
+				err.should.equal "error"
+				@DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal true
+				@ProjectUpdateHandler.markAsActive.calledWith(@project_id).should.equal false
+				done()
+
+
+		it "should not call unarchiveProject if it is active", (done)->
+			@project.active = true
+			@DocstoreManager.unarchiveProject.callsArgWith(1)
+			@InactiveProjectManager.reactivateProjectIfRequired @project_id, (err)=>
+				@DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal false
+				@ProjectUpdateHandler.markAsActive.calledWith(@project_id).should.equal false
+				done()
+
+
+	describe "deactivateProject", ->
+
+		beforeEach ->
+
+		it "should call unarchiveProject and markAsInactive", (done)->
+			@DocstoreManager.archiveProject.callsArgWith(1)
+			@ProjectUpdateHandler.markAsInactive.callsArgWith(1)
+
+			@InactiveProjectManager.deactivateProject @project_id, (err)=>
+				@DocstoreManager.archiveProject.calledWith(@project_id).should.equal true
+				@ProjectUpdateHandler.markAsInactive.calledWith(@project_id).should.equal true
+				done()
+
+		it "should not call markAsInactive if there was a problem unarchiving", (done)->
+			@DocstoreManager.archiveProject.callsArgWith(1, "errorrr")
+			@ProjectUpdateHandler.markAsInactive.callsArgWith(1)
+
+			@InactiveProjectManager.deactivateProject @project_id, (err)=>
+				err.should.equal "errorrr"
+				@DocstoreManager.archiveProject.calledWith(@project_id).should.equal true
+				@ProjectUpdateHandler.markAsInactive.calledWith(@project_id).should.equal false
+				done()

+ 51 - 6
services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee

@@ -14,7 +14,7 @@ describe "PasswordResetController", ->
 		@PasswordResetHandler =
 			generateAndEmailResetToken:sinon.stub()
 			setNewUserPassword:sinon.stub()
-		@RateLimiter = 
+		@RateLimiter =
 			addCount: sinon.stub()
 		@PasswordResetController = SandboxedModule.require modulePath, requires:
 			"settings-sharelatex":@settings
@@ -32,6 +32,8 @@ describe "PasswordResetController", ->
 				password:@password
 			i18n:
 				translate:->
+			session: {}
+			query: {}
 
 		@res = {}
 
@@ -51,7 +53,7 @@ describe "PasswordResetController", ->
 		it "should tell the handler to process that email", (done)->
 			@RateLimiter.addCount.callsArgWith(1, null, true)
 			@PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, true)
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				code.should.equal 200
 				@PasswordResetHandler.generateAndEmailResetToken.calledWith(@email.trim()).should.equal true
 				done()
@@ -78,7 +80,7 @@ describe "PasswordResetController", ->
 			@req.body.email = @email
 			@RateLimiter.addCount.callsArgWith(1, null, true)
 			@PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, true)
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				code.should.equal 200
 				@PasswordResetHandler.generateAndEmailResetToken.calledWith(@email.toLowerCase()).should.equal true
 				done()
@@ -86,9 +88,12 @@ describe "PasswordResetController", ->
 
 	describe "setNewUserPassword", ->
 
+		beforeEach ->
+			@req.session.resetToken = @token
+
 		it "should tell the user handler to reset the password", (done)->
 			@PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true)
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				code.should.equal 200
 				@PasswordResetHandler.setNewUserPassword.calledWith(@token, @password).should.equal true
 				done()
@@ -104,7 +109,7 @@ describe "PasswordResetController", ->
 		it "should return 400 (Bad Request) if there is no password", (done)->
 			@req.body.password = ""
 			@PasswordResetHandler.setNewUserPassword.callsArgWith(2)
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				code.should.equal 400
 				@PasswordResetHandler.setNewUserPassword.called.should.equal false
 				done()
@@ -113,11 +118,51 @@ describe "PasswordResetController", ->
 		it "should return 400 (Bad Request) if there is no passwordResetToken", (done)->
 			@req.body.passwordResetToken = ""
 			@PasswordResetHandler.setNewUserPassword.callsArgWith(2)
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				code.should.equal 400
 				@PasswordResetHandler.setNewUserPassword.called.should.equal false
 				done()
 			@PasswordResetController.setNewUserPassword @req, @res
 
+		it "should clear the session.resetToken", (done) ->
+			@PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true)
+			@res.sendStatus = (code)=>
+				code.should.equal 200
+				@req.session.should.not.have.property 'resetToken'
+				done()
+			@PasswordResetController.setNewUserPassword @req, @res
+
+	describe "renderSetPasswordForm", ->
+
+		describe "with token in query-string", ->
+			beforeEach ->
+				@req.query.passwordResetToken = @token
+
+			it "should set session.resetToken and redirect", (done) ->
+				@req.session.should.not.have.property 'resetToken'
+				@res.redirect = (path) =>
+					path.should.equal '/user/password/set'
+					@req.session.resetToken.should.equal @token
+					done()
+				@PasswordResetController.renderSetPasswordForm(@req, @res)
+
+		describe "without a token in query-string", ->
+
+			describe "with token in session", ->
+				beforeEach ->
+					@req.session.resetToken = @token
+
+				it "should render the page, passing the reset token", (done) ->
+					@res.render = (template_path, options) =>
+						options.passwordResetToken.should.equal @req.session.resetToken
+						done()
+					@PasswordResetController.renderSetPasswordForm(@req, @res)
 
+			describe "without a token in session", ->
 
+				it "should redirect to the reset request page", (done) ->
+					@res.redirect = (path) =>
+						path.should.equal "/user/password/reset"
+						@req.session.should.not.have.property 'resetToken'
+						done()
+					@PasswordResetController.renderSetPasswordForm(@req, @res)

+ 1 - 8
services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee

@@ -36,14 +36,7 @@ describe 'Project api controller', ->
 
 		it "should send a 500 if there is an error", (done)->
 			@ProjectDetailsHandler.getDetails.callsArgWith(1, "error")
-			@res.send = (resCode)=>
+			@res.sendStatus = (resCode)=>
 				resCode.should.equal 500
 				done()
 			@controller.getProjectDetails @req, @res
-
-		it "should destroy the session", (done)->
-			@ProjectDetailsHandler.getDetails.callsArgWith(1, null, @projDetails)
-			@res.json = (data)=>
-				@req.session.destroy.called.should.equal true
-				done()
-			@controller.getProjectDetails @req, @res

+ 34 - 12
services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee

@@ -42,6 +42,10 @@ describe "ProjectController", ->
 			userCanAccessProject:sinon.stub()
 		@EditorController = 
 			renameProject:sinon.stub()
+		@InactiveProjectManager =
+			reactivateProjectIfRequired:sinon.stub()
+		@ProjectUpdateHandler =
+			markAsOpened: sinon.stub()
 		@ProjectController = SandboxedModule.require modulePath, requires:
 			"settings-sharelatex":@settings
 			"logger-sharelatex": 
@@ -57,6 +61,8 @@ describe "ProjectController", ->
 			'../../models/Project': Project:@ProjectModel
 			"../../models/User":User:@UserModel
 			"../../managers/SecurityManager":@SecurityManager
+			"../InactiveData/InactiveProjectManager":@InactiveProjectManager
+			"./ProjectUpdateHandler":@ProjectUpdateHandler
 
 		@user = 
 			_id:"!£123213kjljkl"
@@ -78,7 +84,7 @@ describe "ProjectController", ->
 			@EditorController.renameProject = sinon.stub().callsArg(2)
 			@req.body =
 				name: @name = "New name"
-			@res.send = (code) =>
+			@res.sendStatus = (code) =>
 				@EditorController.renameProject
 					.calledWith(@project_id, @name)
 					.should.equal true
@@ -90,7 +96,7 @@ describe "ProjectController", ->
 			@EditorController.setCompiler = sinon.stub().callsArg(2)
 			@req.body =
 				compiler: @compiler = "pdflatex"
-			@res.send = (code) =>
+			@res.sendStatus = (code) =>
 				@EditorController.setCompiler
 					.calledWith(@project_id, @compiler)
 					.should.equal true
@@ -102,7 +108,7 @@ describe "ProjectController", ->
 			@EditorController.setSpellCheckLanguage = sinon.stub().callsArg(2)
 			@req.body =
 				spellCheckLanguage: @languageCode = "fr"
-			@res.send = (code) =>
+			@res.sendStatus = (code) =>
 				@EditorController.setSpellCheckLanguage
 					.calledWith(@project_id, @languageCode)
 					.should.equal true
@@ -114,7 +120,7 @@ describe "ProjectController", ->
 			@EditorController.setPublicAccessLevel = sinon.stub().callsArg(2)
 			@req.body =
 				publicAccessLevel: @publicAccessLevel = "readonly"
-			@res.send = (code) =>
+			@res.sendStatus = (code) =>
 				@EditorController.setPublicAccessLevel
 					.calledWith(@project_id, @publicAccessLevel)
 					.should.equal true
@@ -126,7 +132,7 @@ describe "ProjectController", ->
 			@EditorController.setRootDoc = sinon.stub().callsArg(2)
 			@req.body =
 				rootDocId: @rootDocId = "root-doc-id"
-			@res.send = (code) =>
+			@res.sendStatus = (code) =>
 				@EditorController.setRootDoc
 					.calledWith(@project_id, @rootDocId)
 					.should.equal true
@@ -136,7 +142,7 @@ describe "ProjectController", ->
 
 	describe "deleteProject", ->
 		it "should tell the project deleter to archive when forever=false", (done)->
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				@ProjectDeleter.archiveProject.calledWith(@project_id).should.equal true
 				code.should.equal 200
 				done()
@@ -144,7 +150,7 @@ describe "ProjectController", ->
 
 		it "should tell the project deleter to delete when forever=true", (done)->
 			@req.query = forever: "true"
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				@ProjectDeleter.deleteProject.calledWith(@project_id).should.equal true
 				code.should.equal 200
 				done()
@@ -152,7 +158,7 @@ describe "ProjectController", ->
 
 	describe "restoreProject", ->
 		it "should tell the project deleter", (done)->
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				@ProjectDeleter.restoreProject.calledWith(@project_id).should.equal true
 				code.should.equal 200
 				done()
@@ -244,7 +250,7 @@ describe "ProjectController", ->
 
 		it "should call the editor controller", (done)->
 			@EditorController.renameProject.callsArgWith(2)
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				code.should.equal 200
 				@EditorController.renameProject.calledWith(@project_id, @newProjectName).should.equal true
 				done()
@@ -252,7 +258,7 @@ describe "ProjectController", ->
 
 		it "should send a 500 if there is a problem", (done)->
 			@EditorController.renameProject.callsArgWith(2, "problem")
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				code.should.equal 500
 				@EditorController.renameProject.calledWith(@project_id, @newProjectName).should.equal true
 				done()
@@ -260,7 +266,7 @@ describe "ProjectController", ->
 
 		it "should return an error if the name is over 150 chars", (done)->
 			@req.body.newProjectName = "EDMUBEEBKBXUUUZERMNSXFFWIBHGSDAWGMRIQWJBXGWSBVWSIKLFPRBYSJEKMFHTRZBHVKJSRGKTBHMJRXPHORFHAKRNPZGGYIOTEDMUBEEBKBXUUUZERMNSXFFWIBHGSDAWGMRIQWJBXGWSBVWSIKLFPRBYSJEKMFHTRZBHVKJSRGKTBHMJRXPHORFHAKRNPZGGYIOT"
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				code.should.equal 400
 				done()
 			@ProjectController.renameProject @req, @res
@@ -282,6 +288,9 @@ describe "ProjectController", ->
 			@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {})
 			@SecurityManager.userCanAccessProject.callsArgWith 2, true, "owner"
 			@ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub()
+			@InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1)
+			@ProjectUpdateHandler.markAsOpened.callsArgWith(1)
+
 
 		it "should render the project/editor page", (done)->
 			@res.render = (pageName, opts)=>
@@ -317,7 +326,20 @@ describe "ProjectController", ->
 
 		it "should not render the page if the project can not be accessed", (done)->
 			@SecurityManager.userCanAccessProject = sinon.stub().callsArgWith 2, false
-			@res.send = (resCode, opts)=>
+			@res.sendStatus = (resCode, opts)=>
 				resCode.should.equal 401
 				done()
 			@ProjectController.loadEditor @req, @res
+
+		it "should reactivateProjectIfRequired", (done)->
+			@res.render = (pageName, opts)=>
+				@InactiveProjectManager.reactivateProjectIfRequired.calledWith(@project_id).should.equal true
+				done()
+			@ProjectController.loadEditor @req, @res
+
+		it "should mark project as opened", (done)->
+			@res.render = (pageName, opts)=>
+				@ProjectUpdateHandler.markAsOpened.calledWith(@project_id).should.equal true
+				done()
+			@ProjectController.loadEditor @req, @res
+

+ 7 - 0
services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee

@@ -159,6 +159,13 @@ describe 'project model', ->
 				assert !err?
 				doc._id.should.equal rootDoc._id
 				done()
+		
+		it 'should return null when the project has no rootDoc', (done) ->
+			project.rootDoc_id = null
+			@locator.findRootDoc project, (err, doc)->
+				assert !err?
+				expect(doc).to.equal null
+				done()
 
 	describe 'findElementByPath', ->
 

+ 34 - 1
services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee

@@ -3,7 +3,7 @@ chai = require('chai').should()
 modulePath = "../../../../app/js/Features/Project/ProjectUpdateHandler.js"
 SandboxedModule = require('sandboxed-module')
 
-describe 'updating a project', ->
+describe 'ProjectUpdateHandler', ->
 
 
 	beforeEach ->
@@ -22,3 +22,36 @@ describe 'updating a project', ->
 				now = Date.now()+""
 				date.substring(0,5).should.equal now.substring(0,5)
 				done()
+
+	describe "markAsOpened", ->
+
+		it 'should send an update to mongo', (done)->
+			project_id = "project_id"
+			@handler.markAsOpened project_id, (err)=>
+				args = @ProjectModel.update.args[0]
+				args[0]._id.should.equal project_id
+				date = args[1].lastOpened+""
+				now = Date.now()+""
+				date.substring(0,5).should.equal now.substring(0,5)
+				done()
+
+	describe "markAsInactive", ->
+
+		it 'should send an update to mongo', (done)->
+			project_id = "project_id"
+			@handler.markAsInactive project_id, (err)=>
+				args = @ProjectModel.update.args[0]
+				args[0]._id.should.equal project_id
+				args[1].active.should.equal false
+				done()
+
+	describe "markAsActive", ->
+		it 'should send an update to mongo', (done)->
+			project_id = "project_id"
+			@handler.markAsActive project_id, (err)=>
+				args = @ProjectModel.update.args[0]
+				args[0]._id.should.equal project_id
+				args[1].active.should.equal true
+				done()
+
+

+ 11 - 11
services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee

@@ -285,9 +285,9 @@ describe "SubscriptionController sanboxed", ->
 	describe "createSubscription", ->
 		beforeEach (done)->
 			@res =
-				send:->
+				sendStatus:->
 					done()
-			sinon.spy @res, "send"
+			sinon.spy @res, "sendStatus"
 			@subscriptionDetails =
 				card:"1234"
 				cvv:"123"
@@ -300,7 +300,7 @@ describe "SubscriptionController sanboxed", ->
 			done()
 
 		it "should redurect to the subscription page", (done)->
-			@res.send.calledWith(201).should.equal true
+			@res.sendStatus.calledWith(201).should.equal true
 			done()
 
 
@@ -363,9 +363,9 @@ describe "SubscriptionController sanboxed", ->
 						expired_subscription_notification:
 							subscription:
 								uuid: @activeRecurlySubscription.uuid
-				@res = send:->
+				@res = sendStatus:->
 					done()
-				sinon.spy @res, "send"
+				sinon.spy @res, "sendStatus"
 				@SubscriptionController.recurlyCallback @req, @res
 
 			it "should tell the SubscriptionHandler to process the recurly callback", (done)->
@@ -374,7 +374,7 @@ describe "SubscriptionController sanboxed", ->
 
 
 			it "should send a 200", (done)->
-				@res.send.calledWith(200)
+				@res.sendStatus.calledWith(200)
 				done()
 
 		describe "with a non-actionable request", ->
@@ -385,16 +385,16 @@ describe "SubscriptionController sanboxed", ->
 						new_subscription_notification:
 							subscription:
 								uuid: @activeRecurlySubscription.uuid
-				@res = send:->
+				@res = sendStatus:->
 					done()
-				sinon.spy @res, "send"
+				sinon.spy @res, "sendStatus"
 				@SubscriptionController.recurlyCallback @req, @res
 
 			it "should not call the subscriptionshandler", ->
 				@SubscriptionHandler.recurlyCallback.called.should.equal false
 
 			it "should respond with a 200 status", ->
-				@res.send.calledWith(200)
+				@res.sendStatus.calledWith(200)
 
 
 	describe "renderUpgradeToAnnualPlanPage", ->
@@ -442,7 +442,7 @@ describe "SubscriptionController sanboxed", ->
 			@req.body =
 				planName:"student"
 
-			@res.send = ()=>
+			@res.sendStatus = ()=>
 				@SubscriptionHandler.updateSubscription.calledWith(@user, "student-annual", "STUDENTCODEHERE").should.equal true
 				done()
 
@@ -453,7 +453,7 @@ describe "SubscriptionController sanboxed", ->
 			@req.body =
 				planName:"collaborator"
 
-			@res.send = (url)=>
+			@res.sendStatus = (url)=>
 				@SubscriptionHandler.updateSubscription.calledWith(@user, "collaborator-annual", "COLLABORATORCODEHERE").should.equal true
 				done()
 

+ 1 - 1
services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupControllerTests.coffee

@@ -127,7 +127,7 @@ describe "SubscriptionGroupController", ->
 
 			it "should ask the SubscriptionGroupHandler to send the verification email", (done)->
 				res =
-					send : (statusCode)=>
+					sendStatus : (statusCode)=>
 						statusCode.should.equal 200
 						@GroupHandler.sendVerificationEmail.calledWith(@subscription_id, @licenceName, @user_email).should.equal true
 						done()

+ 6 - 11
services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee

@@ -28,7 +28,7 @@ describe 'TpdsController', ->
 				headers:
 					"x-sl-update-source": @source = "dropbox"
 			@TpdsUpdateHandler.newUpdate = sinon.stub().callsArg(5)
-			res =  send: => 
+			res =  sendStatus: => 
 				@TpdsUpdateHandler.newUpdate.calledWith(@user_id, "projectName","/here.txt", req, @source).should.equal true
 				done()
 			@TpdsController.mergeUpdate req, res
@@ -43,7 +43,7 @@ describe 'TpdsController', ->
 				headers:
 					"x-sl-update-source": @source = "dropbox"
 			@TpdsUpdateHandler.deleteUpdate = sinon.stub().callsArg(4)
-			res = send: => 
+			res = sendStatus: => 
 				@TpdsUpdateHandler.deleteUpdate.calledWith(@user_id, "projectName", "/here.txt", @source).should.equal true
 				done()
 			@TpdsController.deleteUpdate req, res
@@ -86,7 +86,7 @@ describe 'TpdsController', ->
 				headers:
 					"x-sl-update-source": @source = "github"
 			@res =
-				send: sinon.stub()
+				sendStatus: sinon.stub()
 			
 			@TpdsController.updateProjectContents @req, @res
 			
@@ -96,10 +96,8 @@ describe 'TpdsController', ->
 				.should.equal true
 				
 		it "should return a success", ->
-			@res.send.calledWith(200).should.equal true
+			@res.sendStatus.calledWith(200).should.equal true
 
-		it "should clear the session", ->
-			@req.session.destroy.called.should.equal true
 			
 	describe 'deleteProjectContents', ->
 		beforeEach ->
@@ -113,7 +111,7 @@ describe 'TpdsController', ->
 				headers:
 					"x-sl-update-source": @source = "github"
 			@res =
-				send: sinon.stub()
+				sendStatus: sinon.stub()
 			
 			@TpdsController.deleteProjectContents @req, @res
 			
@@ -123,8 +121,5 @@ describe 'TpdsController', ->
 				.should.equal true
 				
 		it "should return a success", ->
-			@res.send.calledWith(200).should.equal true
-
-		it "should clear the session", ->
-			@req.session.destroy.called.should.equal true
+			@res.sendStatus.calledWith(200).should.equal true
 

+ 44 - 21
services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee

@@ -19,32 +19,55 @@ filestoreUrl = "filestore.sharelatex.com"
 
 describe 'TpdsUpdateSender', ->
 	beforeEach ->
-		@requestQueuer = regist:(queue, meth, opts, callback)->
+		@requestQueuer = (queue, meth, opts, callback)->
 		project = {owner_ref:user_id,readOnly_refs:[read_only_ref_1], collaberator_refs:[collaberator_ref_1]}
 		@Project = findById:sinon.stub().callsArgWith(2, null, project)
 		@docstoreUrl = "docstore.sharelatex.env"
+		@request = sinon.stub().returns(pipe:->)
+		@settings = 
+			siteUrl:siteUrl
+			httpAuthSiteUrl:httpAuthSiteUrl,
+			apis: 
+				thirdPartyDataStore: {url: thirdPartyDataStoreApiUrl}
+				filestore:
+					url: filestoreUrl
+				docstore:
+					pubUrl: @docstoreUrl
 		@updateSender = SandboxedModule.require modulePath, requires:
-			'fairy':{connect:=>{queue:=>@requestQueuer}}
-			"settings-sharelatex": 
-				siteUrl:siteUrl
-				httpAuthSiteUrl:httpAuthSiteUrl,
-				apis: 
-					thirdPartyDataStore: {url: thirdPartyDataStoreApiUrl}
-					filestore:
-						url: filestoreUrl
-					docstore:
-						pubUrl: @docstoreUrl
-				redis:fairy:{}
+			"settings-sharelatex": @settings
 			"logger-sharelatex":{log:->}
 			'../../models/Project': Project:@Project
-			'request':->{pipe:->}
+			'request':@request
+
+	describe "_enqueue", ->
+
+		it "should not call request if there is no tpdsworker url", (done)->
+			@updateSender._enqueue null, null, null, (err)=>
+				@request.called.should.equal false
+				done()
+
+		it "should post the message to the tpdsworker", (done)->
+			@settings.apis.tpdsworker = url:"www.tpdsworker.env"
+			group = "myproject"
+			method = "somemethod"
+			job = "do something"
+			@request.callsArgWith(1)
+			@updateSender._enqueue group, method, job, (err)=>
+				args = @request.args[0][0]
+				args.json.group.should.equal group
+				args.json.job.should.equal job
+				args.json.method.should.equal method
+				args.uri.should.equal "www.tpdsworker.env/enqueue/web_to_tpds_http_requests"
+				done()
+
+
 
 	describe 'sending updates', ->
 
-		it 'ques a post the file with user and file id', (done)->
+		it 'queues a post the file with user and file id', (done)->
 			file_id = '4545345'
 			path = '/some/path/here.jpg'
-			@requestQueuer.enqueue = (uid, method, job, callback)->
+			@updateSender._enqueue = (uid, method, job, callback)->
 				uid.should.equal project_id
 				job.method.should.equal "post"
 				job.streamOrigin.should.equal "#{filestoreUrl}/project/#{project_id}/file/#{file_id}"
@@ -59,7 +82,7 @@ describe 'TpdsUpdateSender', ->
 			path = "/some/path/here.tex"
 			lines = ["line1", "line2", "line3"]
 
-			@requestQueuer.enqueue = (uid, method, job, callback)=>
+			@updateSender._enqueue = (uid, method, job, callback)=>
 				uid.should.equal project_id
 				job.method.should.equal "post"
 				expectedUrl = "#{thirdPartyDataStoreApiUrl}/user/#{user_id}/entity/#{encodeURIComponent(project_name)}#{encodeURIComponent(path)}"
@@ -71,7 +94,7 @@ describe 'TpdsUpdateSender', ->
 
 		it 'deleting entity', (done)->
 			path = "/path/here/t.tex"
-			@requestQueuer.enqueue = (uid, method, job, callback)->
+			@updateSender._enqueue = (uid, method, job, callback)->
 				uid.should.equal project_id
 				job.method.should.equal "DELETE"
 				expectedUrl = "#{thirdPartyDataStoreApiUrl}/user/#{user_id}/entity/#{encodeURIComponent(project_name)}#{encodeURIComponent(path)}"
@@ -83,7 +106,7 @@ describe 'TpdsUpdateSender', ->
 		it 'moving entity', (done)->
 			startPath = "staring/here/file.tex"
 			endPath = "ending/here/file.tex"
-			@requestQueuer.enqueue = (uid, method, job, callback)->
+			@updateSender._enqueue = (uid, method, job, callback)->
 				uid.should.equal project_id
 				job.method.should.equal "put"
 				job.uri.should.equal "#{thirdPartyDataStoreApiUrl}/user/#{user_id}/entity"
@@ -96,7 +119,7 @@ describe 'TpdsUpdateSender', ->
 		it 'should be able to rename a project using the move entity func', (done)->
 			oldProjectName = "/oldProjectName/"
 			newProjectName = "/newProjectName/"
-			@requestQueuer.enqueue = (uid, method, job, callback)->
+			@updateSender._enqueue = (uid, method, job, callback)->
 				uid.should.equal project_id
 				job.method.should.equal "put"
 				job.uri.should.equal "#{thirdPartyDataStoreApiUrl}/user/#{user_id}/entity"
@@ -107,9 +130,9 @@ describe 'TpdsUpdateSender', ->
 			@updateSender.moveEntity {project_id:project_id, project_name:oldProjectName, newProjectName:newProjectName}
 			
 		it "pollDropboxForUser", (done) ->
-			@requestQueuer.enqueue = sinon.stub().callsArg(3)
+			@updateSender._enqueue = sinon.stub().callsArg(3)
 			@updateSender.pollDropboxForUser user_id, (error) =>
-				@requestQueuer.enqueue
+				@updateSender._enqueue
 					.calledWith(
 						"poll-dropbox:#{user_id}",
 						"standardHttpRequest",

+ 3 - 3
services/web/test/UnitTests/coffee/Uploads/ProjectUploadControllerTests.coffee

@@ -29,7 +29,7 @@ describe "ProjectUploadController", ->
 			@req.files =
 				qqfile:
 					path: @path
-					name: @name
+					originalname: @name
 			@req.session =
 				user:
 					_id: @user_id
@@ -102,7 +102,7 @@ describe "ProjectUploadController", ->
 			@req.files =
 				qqfile:
 					path: @path
-					name: @name
+					originalname: @name
 			@req.params =
 				Project_id: @project_id
 			@req.query =
@@ -173,7 +173,7 @@ describe "ProjectUploadController", ->
 		describe "with a bad request", ->
 
 			beforeEach ->
-				@req.files.qqfile.name = ""
+				@req.files.qqfile.originalname = ""
 				@ProjectUploadController.uploadFile @req, @res
 
 			it "should return a a non success response", ->

+ 9 - 9
services/web/test/UnitTests/coffee/User/UserControllerTests.coffee

@@ -78,7 +78,7 @@ describe "UserController", ->
 
 		it "should delete the user", (done)->
 
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				@UserDeleter.deleteUser.calledWith(@user_id)
 				code.should.equal 200
 				done()
@@ -98,7 +98,7 @@ describe "UserController", ->
 
 		it "should call save", (done)->
 			@req.body = {}
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				@user.save.called.should.equal true
 				done()
 			@UserController.updateUserSettings @req, @res
@@ -106,7 +106,7 @@ describe "UserController", ->
 		it "should set the first name", (done)->
 			@req.body =
 				first_name: "bobby  "
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				@user.first_name.should.equal "bobby"
 				done()
 			@UserController.updateUserSettings @req, @res
@@ -114,7 +114,7 @@ describe "UserController", ->
 		it "should set the role", (done)->
 			@req.body =
 				role: "student"
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				@user.role.should.equal "student"
 				done()
 			@UserController.updateUserSettings @req, @res
@@ -122,7 +122,7 @@ describe "UserController", ->
 		it "should set the institution", (done)->
 			@req.body =
 				institution: "MIT"
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				@user.institution.should.equal "MIT"
 				done()
 			@UserController.updateUserSettings @req, @res
@@ -130,21 +130,21 @@ describe "UserController", ->
 		it "should set some props on ace", (done)->
 			@req.body =
 				theme: "something"
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				@user.ace.theme.should.equal "something"
 				done()
 			@UserController.updateUserSettings @req, @res
 
 		it "should send an error if the email is 0 len", (done)->
 			@req.body.email = ""
-			@res.send = (code)->
+			@res.sendStatus = (code)->
 				code.should.equal 400
 				done()
 			@UserController.updateUserSettings @req, @res
 
 		it "should send an error if the email does not contain an @", (done)->
 			@req.body.email = "bob at something dot com"
-			@res.send = (code)->
+			@res.sendStatus = (code)->
 				code.should.equal 400
 				done()
 			@UserController.updateUserSettings @req, @res
@@ -152,7 +152,7 @@ describe "UserController", ->
 		it "should call the user updater with the new email and user _id", (done)->
 			@req.body.email = @newEmail.toUpperCase()
 			@UserUpdater.changeEmailAddress.callsArgWith(2)
-			@res.send = (code)=>
+			@res.sendStatus = (code)=>
 				code.should.equal 200
 				@UserUpdater.changeEmailAddress.calledWith(@user_id, @newEmail).should.equal true
 				done()

+ 13 - 0
services/web/test/UnitTests/coffee/helpers/MockResponse.coffee

@@ -22,6 +22,19 @@ class MockResponse
 		@redirectedTo = url
 		@callback() if @callback?
 	
+	sendStatus: (status) ->
+		if arguments.length < 2
+			if typeof status != "number"
+				body = status
+				status = 200
+		@statusCode = status
+		@returned = true
+		if 200 <= status < 300
+			@success = true
+		else
+			@success = false
+		@callback() if @callback?
+
 	send: (status, body) ->
 		if arguments.length < 2
 			if typeof status != "number"