Explorar o código

Merge branch 'master' into i18n

Conflicts:
	package.json
Henry Oswald %!s(int64=12) %!d(string=hai) anos
pai
achega
abbbdf463f
Modificáronse 79 ficheiros con 931 adicións e 807 borrados
  1. 2 0
      services/web/.gitignore
  2. 1 4
      services/web/README.md
  3. 38 14
      services/web/TpdsWorker.coffee
  4. 1 1
      services/web/app.coffee
  5. 5 1
      services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
  6. 5 4
      services/web/app/coffee/Features/Compile/CompileController.coffee
  7. 42 4
      services/web/app/coffee/Features/Editor/EditorController.coffee
  8. 0 0
      services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee
  9. 10 6
      services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee
  10. 3 0
      services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee
  11. 2 0
      services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
  12. 1 1
      services/web/app/coffee/Features/ThirdPartyDataStore/TpdsPollingBackgroundTasks.coffee
  13. 2 2
      services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee
  14. 1 1
      services/web/app/coffee/Features/User/UserDeleter.coffee
  15. 2 2
      services/web/app/coffee/Features/User/UserRegistrationHandler.coffee
  16. 20 36
      services/web/app/coffee/controllers/HomeController.coffee
  17. 6 7
      services/web/app/coffee/controllers/ProjectController.coffee
  18. 16 16
      services/web/app/coffee/controllers/UserController.coffee
  19. 0 62
      services/web/app/coffee/managers/CollaberationManager.coffee
  20. 0 6
      services/web/app/coffee/managers/GuidManager.coffee
  21. 3 1
      services/web/app/coffee/managers/SecurityManager.coffee
  22. 2 2
      services/web/app/coffee/models/Project.coffee
  23. 1 1
      services/web/app/coffee/models/User.coffee
  24. 46 55
      services/web/app/coffee/router.coffee
  25. 0 41
      services/web/app/views/about/about.jade
  26. 0 31
      services/web/app/views/about/attribution.jade
  27. 0 16
      services/web/app/views/about/planned_maintenance.jade
  28. 0 26
      services/web/app/views/about/privacy.jade
  29. 0 94
      services/web/app/views/about/security.jade
  30. 0 34
      services/web/app/views/about/tos.jade
  31. 0 46
      services/web/app/views/homepage/comments.jade
  32. 0 15
      services/web/app/views/homepage/header.jade
  33. 0 64
      services/web/app/views/homepage/home.jade
  34. 0 7
      services/web/app/views/homepage/socialMedia.jade
  35. 1 1
      services/web/app/views/layout.jade
  36. 3 2
      services/web/app/views/menubar.jade
  37. 6 1
      services/web/app/views/referal/bonus.jade
  38. 1 1
      services/web/app/views/referal/facebookWallPost.jade
  39. 0 59
      services/web/app/views/resources.jade
  40. 2 1
      services/web/app/views/subscriptions/group_admin.jade
  41. 42 27
      services/web/app/views/templates.jade
  42. 11 0
      services/web/cleanup.js
  43. 15 19
      services/web/package.json
  44. 22 12
      services/web/public/coffee/SubscriptionGroupsManager.coffee
  45. 13 2
      services/web/public/coffee/account/AccountManager.coffee
  46. 18 1
      services/web/public/coffee/analytics/AnalyticsManager.coffee
  47. 17 1
      services/web/public/coffee/auto-complete/AutoCompleteManager.coffee
  48. 32 0
      services/web/public/coffee/debug/DebugManager.coffee
  49. 44 4
      services/web/public/coffee/editor/Document.coffee
  50. 1 1
      services/web/public/coffee/editor/Editor.coffee
  51. 8 3
      services/web/public/coffee/editor/ShareJsDoc.coffee
  52. 13 0
      services/web/public/coffee/file-tree/EntityView.coffee
  53. 5 4
      services/web/public/coffee/file-tree/FileTreeManager.coffee
  54. 3 0
      services/web/public/coffee/file-tree/FileTreeView.coffee
  55. 16 0
      services/web/public/coffee/file-tree/FolderView.coffee
  56. 14 7
      services/web/public/coffee/ide.coffee
  57. 9 4
      services/web/public/coffee/ide/ConnectionManager.coffee
  58. 1 1
      services/web/public/coffee/list.coffee
  59. 2 1
      services/web/public/coffee/pdf/CompiledView.coffee
  60. 23 0
      services/web/public/coffee/pdf/PdfManager.coffee
  61. 7 5
      services/web/public/coffee/project-members/ProjectMembersManager.coffee
  62. 2 2
      services/web/public/coffee/settings/DropboxSettingsManager.coffee
  63. 4 1
      services/web/public/coffee/settings/SettingsManager.coffee
  64. 14 0
      services/web/public/coffee/track-changes/ChangeListView.coffee
  65. 46 10
      services/web/public/coffee/track-changes/TrackChangesManager.coffee
  66. 39 2
      services/web/public/js/libs/pdfListView/PdfListView.js
  67. 3 0
      services/web/public/stylesheets/less/bonus.less
  68. 30 0
      services/web/public/stylesheets/less/editor.less
  69. 43 0
      services/web/public/stylesheets/less/trackchanges.less
  70. 31 0
      services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
  71. 23 4
      services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee
  72. 111 0
      services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee
  73. 17 12
      services/web/test/UnitTests/coffee/Project/ProjectDetailsHandlerTests.coffee
  74. 4 2
      services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee
  75. 12 0
      services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupHandlerTests.coffee
  76. 1 1
      services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsPollingBackgroundTasksTests.coffee
  77. 1 1
      services/web/test/UnitTests/coffee/User/UserDeleterTests.coffee
  78. 3 8
      services/web/test/UnitTests/coffee/Versioning/AutomaticSnapshotManagerTests.coffee
  79. 9 7
      services/web/test/smoke/coffee/SmokeTests.coffee

+ 2 - 0
services/web/.gitignore

@@ -63,6 +63,8 @@ public/minjs/
 public/js/main.js
 Gemfile.lock
 
+app/views/external
+
 *.swp
 .DS_Store
 

+ 1 - 4
services/web/README.md

@@ -17,10 +17,7 @@ Unit test status
 License and Credits
 -------------------
 
-### Code
-
-All coffeescript files (files ended in *.coffee) are licensed under the
-[AGPLv3 license](http://www.gnu.org/licenses/agpl-3.0.html).
+This project is licensed under the [AGPLv3 license](http://www.gnu.org/licenses/agpl-3.0.html)
 
 ### Stylesheets
 

+ 38 - 14
services/web/TpdsWorker.coffee

@@ -33,26 +33,50 @@ processingFuncs =
 			else
 				callback()
 
-	pipeStreamFrom: (options, callback)->
+	pipeStreamFrom: (options, _callback)->
+		callback = (args...) ->
+			_callback(args...)
+			_callback = () ->
+
 		if options.filePath == "/droppy/main.tex"
 			request options.streamOrigin, (err,res, body)->
 				logger.log options:options, body:body
+
 		origin = request(options.streamOrigin)
+
+		cancelled = false
+		gotResponse = false
+		origin.on 'response', (res) ->
+			return if cancelled
+			gotResponse = true
+			if 200 <= res.statusCode < 300
+				dest = request(options)
+				origin.pipe(dest)
+
+				dest.on "error", (err)->
+					logger.error err:err, options:options, "something went wrong in pipeStreamFrom dest"
+					callback(err)
+					
+				dest.on 'end', callback
+			else
+				error = new Error("received non-success status code: #{res.statusCode}")
+				logger.error err: error, options: options, "something went wrong connecting to origin"
+				callback(error)
+
 		origin.on 'error', (err)->
+			return if cancelled
+			gotResponse = true
 			logger.error err:err, options:options, "something went wrong in pipeStreamFrom origin"
-			if err?
-				callback(err)
-			else
-				callback()
-		dest = request(options)
-		origin.pipe(dest)
-		dest.on "error", (err)->
-			logger.error err:err, options:options, "something went wrong in pipeStreamFrom dest"
-			if err?
-				callback(err)
-			else
-				callback()
-		dest.on 'end', callback
+			callback(err)
+
+		setTimeout () ->
+			return if gotResponse
+			cancelled = true
+			error = new Error("timeout")
+			logger.error err: error, options: options, "timeout"
+			callback(error)
+		, 2000
+
 
 
 workerRegistration = (groupKey, method, options, callback)->

+ 1 - 1
services/web/app.coffee

@@ -15,7 +15,7 @@ argv = require("optimist")
 	.argv
 
 Server.app.use (error, req, res, next) ->
-	logger.error err: error
+	logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear"
 	res.statusCode = error.status or 500
 	if res.statusCode == 500
 		res.end("Oops, something went wrong with your request, sorry. If this continues, please contact us at team@sharelatex.com")

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

@@ -44,7 +44,11 @@ module.exports = AuthenticationController =
 				res.send(auth_token)
 
 	getLoggedInUserId: (req, callback = (error, user_id) ->) ->
-		callback null, req.session.user._id.toString()
+		if req?.session?.user?._id?
+			callback null, req.session.user._id.toString()
+		else
+			e = new Error("user is not on req session")
+			callback e
 
 	getLoggedInUser: (req, options = {allow_auth_token: false}, callback = (error, user) ->) ->
 		if req.session?.user?._id?

+ 5 - 4
services/web/app/coffee/Features/Compile/CompileController.coffee

@@ -19,6 +19,9 @@ module.exports = CompileController =
 				res.header('Content-Disposition', "filename=#{project.getSafeProjectName()}.pdf")
 			CompileController.proxyToClsi("/project/#{project_id}/output/output.pdf", req, res, next)
 
+	deleteAuxFiles: (req, res, next) ->
+		project_id = req.params.Project_id
+		CompileController.proxyToClsi("/project/#{project_id}", req, res, next)
 
 	compileAndDownloadPdf: (req, res, next)->
 		project_id = req.params.project_id
@@ -29,8 +32,6 @@ module.exports = CompileController =
 			url = "/project/#{project_id}/output/output.pdf"
 			CompileController.proxyToClsi url, req, res, next
 
-
-
 	getFileFromClsi: (req, res, next = (error) ->) ->
 		CompileController.proxyToClsi("/project/#{req.params.Project_id}/output/#{req.params.file}", req, res, next)
 
@@ -38,8 +39,8 @@ module.exports = CompileController =
 		logger.log url: url, "proxying to CLSI"
 		url = "#{Settings.apis.clsi.url}#{url}"
 		oneMinute = 60 * 1000
-		proxy = request.get(url: url, timeout: oneMinute)
+		proxy = request(url: url, method: req.method, timeout: oneMinute)
 		proxy.pipe(res)
 		proxy.on "error", (error) ->
-			logger.error err: error, url: url, "CLSI proxy error"
+			logger.warn err: error, url: url, "CLSI proxy error"
 	

+ 42 - 4
services/web/app/coffee/Features/Editor/EditorController.coffee

@@ -1,6 +1,6 @@
 logger = require('logger-sharelatex')
 Metrics = require('../../infrastructure/Metrics')
-sanitize = require('validator').sanitize
+sanitize = require('sanitizer')
 ProjectEditorHandler = require('../Project/ProjectEditorHandler')
 ProjectEntityHandler = require('../Project/ProjectEntityHandler')
 ProjectOptionsHandler = require('../Project/ProjectOptionsHandler')
@@ -163,7 +163,7 @@ module.exports = EditorController =
 
 	addDoc: (project_id, folder_id, docName, docLines, sl_req_id, callback = (error, doc)->)->
 		{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
-		docName = sanitize(docName).xss()
+		docName = sanitize.escape(docName)
 		logger.log sl_req_id:sl_req_id, "sending new doc to project #{project_id}"
 		Metrics.inc "editor.add-doc"
 		ProjectEntityHandler.addDoc project_id, folder_id, docName, docLines, sl_req_id, (err, doc, folder_id)=>
@@ -172,7 +172,7 @@ module.exports = EditorController =
 
 	addFile: (project_id, folder_id, fileName, path, sl_req_id, callback = (error, file)->)->
 		{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
-		fileName = sanitize(fileName).xss()
+		fileName = sanitize.escape(fileName)
 		logger.log  sl_req_id:sl_req_id, "sending new file to project #{project_id} with folderid: #{folder_id}"
 		Metrics.inc "editor.add-file"
 		ProjectEntityHandler.addFile project_id, folder_id, fileName, path, (err, fileRef, folder_id)=>
@@ -185,7 +185,7 @@ module.exports = EditorController =
 
 	addFolder: (project_id, folder_id, folderName, sl_req_id, callback = (error, folder)->)->
 		{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
-		folderName = sanitize(folderName).xss()
+		folderName = sanitize.escape(folderName)
 		logger.log "sending new folder to project #{project_id}"
 		Metrics.inc "editor.add-folder"
 		ProjectEntityHandler.addFolder project_id, folder_id, folderName, (err, folder, folder_id)=>
@@ -237,6 +237,44 @@ module.exports = EditorController =
 			EditorRealTimeController.emitToRoom(project_id, 'projectDescriptionUpdated', description)
 			callback()
 
+	deleteProject: (project_id, callback)->
+		Metrics.inc "editor.delete-project"
+		logger.log project_id:project_id, "recived message to delete project"
+		ProjectHandler.deleteProject project_id, callback
+
+	renameEntity: (project_id, entity_id, entityType, newName, callback)->
+		newName = sanitize.escape(newName)
+		Metrics.inc "editor.rename-entity"
+		logger.log entity_id:entity_id, entity_id:entity_id, entity_id:entity_id, "reciving new name for entity for project"
+		ProjectHandler.renameEntity project_id, entity_id, entityType, newName, =>
+			if newName.length > 0
+				EditorRealTimeController.emitToRoom project_id, 'reciveEntityRename', entity_id, newName
+				callback?()
+
+	moveEntity: (project_id, entity_id, folder_id, entityType, callback)->
+		Metrics.inc "editor.move-entity"
+		ProjectEntityHandler.moveEntity project_id, entity_id, folder_id, entityType, =>
+			EditorRealTimeController.emitToRoom project_id, 'reciveEntityMove', entity_id, folder_id
+			callback?()
+
+	renameProject: (project_id, window_id, newName, callback)->
+		newName = sanitize.escape(newName)
+		ProjectHandler.renameProject project_id, window_id, newName, =>
+			newName = sanitize.escape(newName)
+			EditorRealTimeController.emitToRoom project_id, 'projectNameUpdated', window_id, newName
+			callback?()
+
+	setPublicAccessLevel : (project_id, newAccessLevel, callback)->
+		ProjectHandler.setPublicAccessLevel project_id, newAccessLevel, =>
+			EditorRealTimeController.emitToRoom project_id, 'publicAccessLevelUpdated', newAccessLevel
+			callback?()
+
+	setRootDoc: (project_id, newRootDocID, callback)->
+		ProjectEntityHandler.setRootDoc project_id, newRootDocID, () =>
+			EditorRealTimeController.emitToRoom project_id, 'rootDocUpdated', newRootDocID
+			callback?()
+
+			
 	p:
 		notifyProjectUsersOfNewFolder: (project_id, folder_id, folder, callback = (error)->)->
 			logger.log project_id:project_id, folder:folder, parentFolder_id:folder_id, "sending newly created folder out to users"

+ 0 - 0
services/web/app/coffee/managers/NewsletterManager.coffee → services/web/app/coffee/Features/Newsletter/NewsletterManager.coffee


+ 10 - 6
services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee

@@ -1,4 +1,5 @@
 ProjectGetter = require("./ProjectGetter")
+UserGetter = require("../User/UserGetter")
 Project = require('../../models/Project').Project
 logger = require("logger-sharelatex")
 
@@ -9,12 +10,15 @@ module.exports =
 			if err?
 				logger.err err:err, project_id:project_id, "error getting project"
 				return callback(err)
-			details =
-				name : project.name
-				description: project.description
-				compiler: project.compiler
-			logger.log project_id:project_id, details:details, "getting project details"
-			callback(err, details)
+			UserGetter.getUser project.owner_ref, (err, user) ->
+				return callback(err) if err?
+				details =
+					name : project.name
+					description: project.description
+					compiler: project.compiler
+					features: user.features
+				logger.log project_id:project_id, details:details, "getting project details"
+				callback(err, details)
 
 	setProjectDescription: (project_id, description, callback)->
 		conditions = _id:project_id

+ 3 - 0
services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee

@@ -28,6 +28,9 @@ module.exports =
 			jobs = _.map subscription.member_ids, (user_id)->
 				return (cb)->
 					UserLocator.findById user_id, (err, user)->
+						if err? or !user?
+							users.push _id:user_id
+							return cb()
 						userViewModel = buildUserViewModel(user)
 						users.push(userViewModel)
 						cb()

+ 2 - 0
services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee

@@ -38,6 +38,8 @@ module.exports =
 	buildViewModel : ->
 		plans = Settings.plans
 
+		console.log plans
+		console.log(typeof(plans))
 		allPlans = {}
 		plans.forEach (plan)->
 			allPlans[plan.planCode] = plan

+ 1 - 1
services/web/app/coffee/Features/ThirdPartyDataStore/TpdsPollingBackgroundTasks.coffee

@@ -23,7 +23,7 @@ self = module.exports =
 			callback()
 
 	_getUserIdsWithDropbox: (callback)->
-		User.find {"dropbox.access_token.oauth_token_secret":{"$exists":true}}, "_id", (err, users)->
+		User.find {"dropbox.access_token.oauth_token_secret":{"$exists":true}, "features.dropbox":true}, "_id", (err, users)->
 			ids = users.map (user)->
 				return user._id+""
 			callback err, ids

+ 2 - 2
services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee

@@ -5,7 +5,7 @@ logger = require('logger-sharelatex')
 Settings = require('settings-sharelatex')
 slReqIdHelper = require('soa-req-id')
 FileTypeManager = require('../Uploads/FileTypeManager')
-GuidManager = require '../../managers/GuidManager'
+uuid = require('node-uuid')
 fs = require('fs')
 
 module.exports =
@@ -75,7 +75,7 @@ module.exports =
 
 		writeStreamToDisk: (project_id, file_id, stream, callback = (err, fsPath)->)->
 			if !file_id?
-				file_id = GuidManager.newGuid()
+				file_id = uuid.v4()
 			dumpPath = "#{Settings.path.dumpFolder}/#{project_id}_#{file_id}"
 
 			writeStream = fs.createWriteStream(dumpPath)

+ 1 - 1
services/web/app/coffee/Features/User/UserDeleter.coffee

@@ -1,5 +1,5 @@
 User = require("../../models/User").User
-NewsletterManager = require "../../managers/NewsletterManager"
+NewsletterManager = require "../Newsletter/NewsletterManager"
 ProjectDeleter = require("../Project/ProjectDeleter")
 logger = require("logger-sharelatex")
 SubscriptionHandler = require("../Subscription/SubscriptionHandler")

+ 2 - 2
services/web/app/coffee/Features/User/UserRegistrationHandler.coffee

@@ -1,4 +1,4 @@
-sanitize = require('validator').sanitize
+sanitize = require('sanitizer')
 
 module.exports =
 	validateEmail : (email) ->
@@ -13,7 +13,7 @@ module.exports =
 		return hasZeroLength
 
 	validateRegisterRequest : (req, callback)->
-		email = sanitize(req.body.email).xss().trim().toLowerCase()
+		email = sanitize.escape(req.body.email).trim().toLowerCase()
 		password = req.body.password
 		username = email.match(/^[^@]*/)
 		if username?

+ 20 - 36
services/web/app/coffee/controllers/HomeController.coffee

@@ -3,8 +3,11 @@ _ = require('underscore')
 User = require('./UserController')
 Quotes = require('../models/Quote').Quote
 
+Path = require "path"
+fs = require "fs"
+homepageExists = fs.existsSync Path.resolve(__dirname + "/../../views/external/home.jade")
 
-module.exports =
+module.exports = HomeController =
 	index : (req,res)->
 		if req.session.user
 			if req.query.scribtex_path?
@@ -12,42 +15,23 @@ module.exports =
 			else
 				res.redirect '/project'
 		else
-			res.render 'homepage/home',
-				title: 'ShareLaTeX.com'
-
-	comments : (req, res)->
-		res.render 'homepage/comments.jade',
-			title: 'User Comments'
-
-	resources : (req, res)->
-		res.render 'resources.jade',
-			title: 'LaTeX Resources'
-
-	tos : (req, res) ->
-		res.render 'about/tos',
-			title: "Terms of Service"
-
-	privacy : (req, res) ->
-		res.render 'about/privacy',
-			title: "Privacy Policy"
-
-	about : (req, res) ->
-		res.render 'about/about',
-			title: "About us"
+			if homepageExists
+				res.render 'external/home',
+					title: 'ShareLaTeX.com'
+			else
+				res.redirect "/login"
+
+	externalPage: (page, title) ->
+		return (req, res, next = (error) ->) ->
+			path = Path.resolve(__dirname + "/../../views/external/#{page}.jade")
+			fs.exists path, (exists) -> # No error in this callback - old method in Node.js!
+				if exists
+					res.render "external/#{page}.jade",
+						title: title
+				else
+					HomeController.notFound(req, res, next)
 
 	notFound: (req, res)->
 		res.statusCode = 404
 		res.render 'general/404',
-			title: "Page Not Found"
-
-	security : (req, res) ->
-		res.render 'about/security',
-			title: "Security"
-
-	attribution: (req, res) ->
-		res.render 'about/attribution',
-			title: "Attribution"
-
-	planned_maintenance: (req, res) ->
-		res.render 'about/planned_maintenance',
-			title: "Planned Maintenance"
+			title: "Page Not Found"

+ 6 - 7
services/web/app/coffee/controllers/ProjectController.coffee

@@ -1,13 +1,12 @@
 User = require('../models/User').User
 Project = require('../models/Project').Project
-sanitize = require('validator').sanitize
+sanitize = require('sanitizer')
 path = require "path"
 logger = require('logger-sharelatex')
 _ = require('underscore')
 fs = require('fs')
 ProjectHandler = require '../handlers/ProjectHandler'
 SecurityManager = require '../managers/SecurityManager'
-GuidManager = require '../managers/GuidManager'
 Settings = require('settings-sharelatex')
 projectCreationHandler = require '../Features/Project/ProjectCreationHandler'
 projectDuplicator = require('../Features/Project/ProjectDuplicator')
@@ -19,7 +18,7 @@ SubscriptionFormatters = require("../Features/Subscription/SubscriptionFormatter
 FileStoreHandler = require("../Features/FileStore/FileStoreHandler")
 
 module.exports = class ProjectController
-	constructor: (@collaberationManager)->
+	constructor: ()->
 		ProjectHandler = new ProjectHandler()
 
 	list: (req, res, next)->
@@ -72,8 +71,8 @@ module.exports = class ProjectController
 
 	apiNewProject: (req, res)->
 		user = req.session.user
-		projectName = sanitize(req.body.projectName).xss()
-		template = sanitize(req.body.template).xss()
+		projectName = sanitize.escape(req.body.projectName)
+		template = sanitize.escape(req.body.template)
 		logger.log user: user, type: template, name: projectName, "creating project"
 		if template == 'example'
 			projectCreationHandler.createExampleProject user._id, projectName, (err, project)->
@@ -123,7 +122,7 @@ module.exports = class ProjectController
 								trackChanges: false
 					else
 						anonymous = false
-					SubscriptionLocator.getUsersSubscription user._id, (err, subscription)->
+					SubscriptionLocator.getUsersSubscription user?._id, (err, subscription)->
 						SecurityManager.userCanAccessProject user, project, (canAccess, privlageLevel)->
 							allowedFreeTrial = true
 							if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt?
@@ -154,7 +153,7 @@ module.exports = class ProjectController
 										spellCheckLanguage: user.ace.spellCheckLanguage
 										pdfViewer : user.ace.pdfViewer
 										docPositions: {}
-										trackChanges: user.featureSwitches.trackChanges
+										oldHistory: !!user.featureSwitches?.oldHistory
 									})
 									sharelatexObject : JSON.stringify({
 										siteUrl: Settings.siteUrl,

+ 16 - 16
services/web/app/coffee/controllers/UserController.coffee

@@ -1,11 +1,11 @@
 User = require('../models/User').User
-sanitize = require('validator').sanitize
+sanitize = require('sanitizer')
 fs = require('fs')
 _ = require('underscore')
 logger = require('logger-sharelatex')
 Security = require('../managers/SecurityManager')
 Settings = require('settings-sharelatex')
-newsLetterManager = require('../managers/NewsletterManager')
+newsLetterManager = require('../Features/Newsletter/NewsletterManager')
 dropboxHandler = require('../Features/Dropbox/DropboxHandler')
 userRegistrationHandler = require('../Features/User/UserRegistrationHandler')
 metrics = require('../infrastructure/Metrics')
@@ -95,8 +95,8 @@ module.exports =
 			title: 'Password Reset',
 
 	doRequestPasswordReset : (req, res, next = (error) ->)->
-		email = sanitize(req.body.email).xss()
-		email = sanitize(email).trim()
+		email = sanitize.escape(req.body.email)
+		email = sanitize.escape(email).trim()
 		email = email.toLowerCase()
 		logger.log email: email, "password reset requested"
 		User.findOne {'email':email}, (err, user)->
@@ -156,11 +156,11 @@ module.exports =
 		metrics.inc "user.settings-update"
 		User.findById req.session.user._id, (err, user)->
 			if(user)
-				user.first_name   = sanitize(req.body.first_name).xss().trim()
-				user.last_name    = sanitize(req.body.last_name).xss().trim()
-				user.ace.mode     = sanitize(req.body.mode).xss().trim()
-				user.ace.theme    = sanitize(req.body.theme).xss().trim()
-				user.ace.fontSize = sanitize(req.body.fontSize).xss().trim()
+				user.first_name   = sanitize.escape(req.body.first_name).trim()
+				user.last_name    = sanitize.escape(req.body.last_name).trim()
+				user.ace.mode     = sanitize.escape(req.body.mode).trim()
+				user.ace.theme    = sanitize.escape(req.body.theme).trim()
+				user.ace.fontSize = sanitize.escape(req.body.fontSize).trim()
 				user.ace.autoComplete = req.body.autoComplete == "true"
 				user.ace.spellCheckLanguage = req.body.spellCheckLanguage
 				user.ace.pdfViewer = req.body.pdfViewer
@@ -171,7 +171,7 @@ module.exports =
 		metrics.inc "user.password-change"
 		oldPass = req.body.currentPassword
 		AuthenticationManager.authenticate _id: req.session.user._id, oldPass, (err, user)->
-			return callback(err) if err?
+			return next(err) if err?
 			if(user)
 				logger.log user: req.session.user, "changing password"
 				newPassword1 = req.body.newPassword1
@@ -197,23 +197,23 @@ module.exports =
 					  type:'error'
 					  text:'Your old password is wrong'
 
-	redirectUserToDropboxAuth: (req, res)->
+	redirectUserToDropboxAuth: (req, res, next)->
 		user_id = req.session.user._id
 		dropboxHandler.getDropboxRegisterUrl user_id, (err, url)->
-			return callback(err) if err?
+			return next(err) if err?
 			logger.log url:url, "redirecting user for dropbox auth"
 			res.redirect url
 
-	completeDropboxRegistration: (req, res)->
+	completeDropboxRegistration: (req, res, next)->
 		user_id = req.session.user._id
 		dropboxHandler.completeRegistration user_id, (err, success)->
-			return callback(err) if err?
+			return next(err) if err?
 			res.redirect('/user/settings#dropboxSettings')
 
-	unlinkDropbox: (req, res)->
+	unlinkDropbox: (req, res, next)->
 		user_id = req.session.user._id
 		dropboxHandler.unlinkAccount user_id, (err, success)->
-			return callback(err) if err?
+			return next(err) if err?
 			res.redirect('/user/settings#dropboxSettings')
 
 	deleteUser: (req, res)->

+ 0 - 62
services/web/app/coffee/managers/CollaberationManager.coffee

@@ -1,62 +0,0 @@
-#this file is being slowly refactored out
-
-logger = require('logger-sharelatex')
-sanitize = require('validator').sanitize
-projectHandler = require('../handlers/ProjectHandler')
-projectHandler = new projectHandler()
-SecurityManager = require('./SecurityManager')
-_ = require('underscore')
-projectEditorHandler = require('../Features/Project/ProjectEditorHandler')
-projectEntityHandler = require('../Features/Project/ProjectEntityHandler')
-versioningApiHandler = require('../Features/Versioning/VersioningApiHandler')
-metrics = require('../infrastructure/Metrics')
-EditorRealTimeController = require('../Features/Editor/EditorRealTimeController')
-
-module.exports = class CollaberationManager
-	constructor: (@io)->
-
-	deleteProject: (project_id, callback)->
-		metrics.inc "editor.delete-project"
-		logger.log project_id:project_id, "recived message to delete project"
-		projectHandler.deleteProject project_id, callback
-
-	renameEntity: (project_id, entity_id, entityType, newName, callback)->
-		newName = sanitize(newName).xss()
-		metrics.inc "editor.rename-entity"
-		logger.log entity_id:entity_id, entity_id:entity_id, entity_id:entity_id, "reciving new name for entity for project"
-		projectHandler.renameEntity project_id, entity_id, entityType, newName, =>
-			if newName.length > 0
-				EditorRealTimeController.emitToRoom project_id, 'reciveEntityRename', entity_id, newName
-				callback?()
-
-	moveEntity: (project_id, entity_id, folder_id, entityType, callback)->
-		metrics.inc "editor.move-entity"
-		projectEntityHandler.moveEntity project_id, entity_id, folder_id, entityType, =>
-			EditorRealTimeController.emitToRoom project_id, 'reciveEntityMove', entity_id, folder_id
-			callback?()
-
-	renameProject: (project_id, window_id, newName, callback)->
-		newName = sanitize(newName).xss()
-		projectHandler.renameProject project_id, window_id, newName, =>
-			newName = sanitize(newName).xss()
-			EditorRealTimeController.emitToRoom project_id, 'projectNameUpdated', window_id, newName
-			callback?()
-
-	setPublicAccessLevel : (project_id, newAccessLevel, callback)->
-		projectHandler.setPublicAccessLevel project_id, newAccessLevel, =>
-			EditorRealTimeController.emitToRoom project_id, 'publicAccessLevelUpdated', newAccessLevel
-			callback?()
-
-	distributMessage: (project_id, client, message)->
-		message = sanitize(message).xss()
-		metrics.inc "editor.instant-message"
-		client.get "first_name", (err, first_name)=>
-			EditorRealTimeController.emitToRoom project_id, 'reciveNewMessage', first_name, message
-
-	setRootDoc: (project_id, newRootDocID, callback)->
-		projectEntityHandler.setRootDoc project_id, newRootDocID, () =>
-			EditorRealTimeController.emitToRoom project_id, 'rootDocUpdated', newRootDocID
-			callback?()
-
-	takeVersionSnapShot : (project_id, message, callback)->
-		versioningApiHandler.takeVersionSnapshot project_id, message, callback

+ 0 - 6
services/web/app/coffee/managers/GuidManager.coffee

@@ -1,6 +0,0 @@
-module.exports =
-	newGuid : ()->
-		S4 = ()->
-			return (((1+Math.random())*0x10000)|0).toString(16).substring(1)
-		return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4())
-

+ 3 - 1
services/web/app/coffee/managers/SecurityManager.coffee

@@ -166,7 +166,9 @@ getRequestUserAndProject = (req, res, options, callback)->
 			callback err, user, project
 
 getProjectIdFromRef = (ref)->
-	if ref._id?
+	if !ref?
+		return null
+	else if ref._id?
 		return ref._id+''
 	else
 		return ref+''

+ 2 - 2
services/web/app/coffee/models/Project.coffee

@@ -3,7 +3,7 @@ Settings = require 'settings-sharelatex'
 _ = require('underscore')
 FolderSchema = require('./Folder.js').FolderSchema
 logger = require('logger-sharelatex')
-sanitize = require('validator').sanitize
+sanitize = require('sanitizer')
 concreteObjectId = require('mongoose').Types.ObjectId
 Errors  = require "../errors"
 
@@ -112,7 +112,7 @@ applyToAllFilesRecursivly = ProjectSchema.statics.applyToAllFilesRecursivly = (f
 
 ProjectSchema.methods.getSafeProjectName = ->
 	safeProjectName = this.name.replace(new RegExp("\\W", "g"), '_')
-	return sanitize(safeProjectName).xss()
+	return sanitize.escape(safeProjectName)
 
 conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10)
 

+ 1 - 1
services/web/app/coffee/models/User.coffee

@@ -32,7 +32,7 @@ UserSchema = new Schema
 						}
 	featureSwitches	  : {
 							dropbox: {type:Boolean, default:true},
-							trackChanges: {type:Boolean, default:false}
+							oldHistory: {type:Boolean}
 						}
 	referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
 	refered_users: [ type:ObjectId, ref:'User' ]

+ 46 - 55
services/web/app/coffee/router.coffee

@@ -5,8 +5,7 @@ ProjectController = require("./controllers/ProjectController")
 ProjectApiController = require("./Features/Project/ProjectApiController")
 InfoController = require('./controllers/InfoController')
 SpellingController = require('./Features/Spelling/SpellingController')
-CollaberationManager = require('./managers/CollaberationManager')
-SecutiryManager = require('./managers/SecurityManager')
+SecurityManager = require('./managers/SecurityManager')
 AuthorizationManager = require('./Features/Security/AuthorizationManager')
 versioningController =  require("./Features/Versioning/VersioningApiController")
 EditorController = require("./Features/Editor/EditorController")
@@ -46,9 +45,7 @@ module.exports = class Router
 	constructor: (app, io, socketSessions)->
 		app.use(app.router)
 
-		collaberationManager = new CollaberationManager(io)
-
-		Project = new ProjectController(collaberationManager)
+		Project = new ProjectController()
 		projectHandler = new ProjectHandler()
 
 		app.get  '/', HomeController.index
@@ -56,16 +53,16 @@ module.exports = class Router
 		app.get  '/login', UserController.loginForm
 		app.post '/login', AuthenticationController.login
 		app.get  '/logout', UserController.logout
-		app.get  '/restricted', SecutiryManager.restricted
-
-		app.get '/resources', HomeController.resources
-		app.get '/comments', HomeController.comments
-		app.get '/tos', HomeController.tos
-		app.get '/about', HomeController.about
-		app.get '/attribution', HomeController.attribution
-		app.get '/security', HomeController.security
-		app.get '/privacy_policy', HomeController.privacy
-		app.get '/planned_maintenance', HomeController.planned_maintenance
+		app.get  '/restricted', SecurityManager.restricted
+
+		app.get '/resources', HomeController.externalPage("resources", "LaTeX Resources")
+		app.get '/tos', HomeController.externalPage("tos", "Terms of Service")
+		app.get '/about', HomeController.externalPage("about", "About Us")
+		app.get '/attribution', HomeController.externalPage("attribution", "Attribution")
+		app.get '/security', HomeController.externalPage("security", "Security")
+		app.get '/privacy_policy', HomeController.externalPage("privacy", "Privacy Policy")
+		app.get '/planned_maintenance', HomeController.externalPage("planned_mainteance", "Planned Maintenance")
+
 		app.get '/themes', InfoController.themes
 		app.get '/advisor', InfoController.advisor
 		app.get '/dropbox', InfoController.dropbox
@@ -99,13 +96,10 @@ module.exports = class Router
 		app.post '/project/new', AuthenticationController.requireLogin(), Project.apiNewProject
 		app.get '/project/new/template', TemplatesMiddlewear.saveTemplateDataInSession, AuthenticationController.requireLogin(), TemplatesController.createProjectFromZipTemplate
 
-		app.get  '/Project/:Project_id', SecutiryManager.requestCanAccessProject, Project.loadEditor
-		app.get  '/Project/:Project_id/file/:File_id', SecutiryManager.requestCanAccessProject, FileStoreController.getFile
+		app.get  '/Project/:Project_id', SecurityManager.requestCanAccessProject, Project.loadEditor
+		app.get  '/Project/:Project_id/file/:File_id', SecurityManager.requestCanAccessProject, FileStoreController.getFile
 
-		# This is left for legacy reasons and can be removed once all editors have had a chance to refresh:
-		app.get  '/Project/:Project_id/download/pdf', SecutiryManager.requestCanAccessProject, CompileController.downloadPdf
-
-		app.get  '/Project/:Project_id/output/output.pdf', SecutiryManager.requestCanAccessProject, CompileController.downloadPdf
+		app.get  '/Project/:Project_id/output/output.pdf', SecurityManager.requestCanAccessProject, CompileController.downloadPdf
 		app.get  /^\/project\/([^\/]*)\/output\/(.*)$/,
 			((req, res, next) ->
 				params =
@@ -113,25 +107,26 @@ module.exports = class Router
 					"file":       req.params[1]
 				req.params = params
 				next()
-			), SecutiryManager.requestCanAccessProject, CompileController.getFileFromClsi
+			), SecurityManager.requestCanAccessProject, CompileController.getFileFromClsi
+		app.del "/project/:Project_id/output", SecurityManager.requestCanAccessProject, CompileController.deleteAuxFiles
 
-		app.del  '/Project/:Project_id',  SecutiryManager.requestIsOwner, Project.deleteProject
-		app.post  '/Project/:Project_id/clone', SecutiryManager.requestCanAccessProject, Project.cloneProject
+		app.del  '/Project/:Project_id', SecurityManager.requestIsOwner, Project.deleteProject
+		app.post  '/Project/:Project_id/clone', SecurityManager.requestCanAccessProject, Project.cloneProject
 
-		app.post '/Project/:Project_id/snapshot', SecutiryManager.requestCanModifyProject, versioningController.takeSnapshot
-		app.get  '/Project/:Project_id/version', SecutiryManager.requestCanAccessProject, versioningController.listVersions
-		app.get  '/Project/:Project_id/version/:Version_id', SecutiryManager.requestCanAccessProject, versioningController.getVersion
-		app.get  '/Project/:Project_id/version', SecutiryManager.requestCanAccessProject, versioningController.listVersions
-		app.get  '/Project/:Project_id/version/:Version_id', SecutiryManager.requestCanAccessProject, versioningController.getVersion
+		app.post '/Project/:Project_id/snapshot', SecurityManager.requestCanModifyProject, versioningController.takeSnapshot
+		app.get  '/Project/:Project_id/version', SecurityManager.requestCanAccessProject, versioningController.listVersions
+		app.get  '/Project/:Project_id/version/:Version_id', SecurityManager.requestCanAccessProject, versioningController.getVersion
+		app.get  '/Project/:Project_id/version', SecurityManager.requestCanAccessProject, versioningController.listVersions
+		app.get  '/Project/:Project_id/version/:Version_id', SecurityManager.requestCanAccessProject, versioningController.getVersion
 
-		app.get  "/project/:Project_id/updates", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
-		app.get  "/project/:Project_id/doc/:doc_id/diff", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
-		app.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
+		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
 
 		app.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
-		app.get  '/project/:Project_id/collaborators', SecutiryManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
+		app.get  '/project/:Project_id/collaborators', SecurityManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
 
-		app.get  '/Project/:Project_id/download/zip', SecutiryManager.requestCanAccessProject, ProjectDownloadsController.downloadProject
+		app.get  '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject
 
 
 		app.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags
@@ -164,21 +159,21 @@ module.exports = class Router
 				req.params = params
 				next()
 			),
-			SecutiryManager.requestCanAccessProject, versioningController.getVersionFile
+			SecurityManager.requestCanAccessProject, versioningController.getVersionFile
 
 		app.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
 		app.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
 
 		#Admin Stuff
-		app.get  '/admin', SecutiryManager.requestIsAdmin, AdminController.index
-		app.post '/admin/closeEditor', SecutiryManager.requestIsAdmin, AdminController.closeEditor
-		app.post '/admin/dissconectAllUsers', SecutiryManager.requestIsAdmin, AdminController.dissconectAllUsers
-		app.post '/admin/writeAllDocsToMongo', SecutiryManager.requestIsAdmin, AdminController.writeAllToMongo
-		app.post '/admin/addquote', SecutiryManager.requestIsAdmin, AdminController.addQuote
-		app.post '/admin/syncUserToSubscription', SecutiryManager.requestIsAdmin, AdminController.syncUserToSubscription
-		app.post '/admin/flushProjectToTpds', SecutiryManager.requestIsAdmin, AdminController.flushProjectToTpds
-		app.post '/admin/pollUsersWithDropbox', SecutiryManager.requestIsAdmin, AdminController.pollUsersWithDropbox
-		app.post '/admin/updateProjectCompiler', SecutiryManager.requestIsAdmin, AdminController.updateProjectCompiler
+		app.get  '/admin', SecurityManager.requestIsAdmin, AdminController.index
+		app.post '/admin/closeEditor', SecurityManager.requestIsAdmin, AdminController.closeEditor
+		app.post '/admin/dissconectAllUsers', SecurityManager.requestIsAdmin, AdminController.dissconectAllUsers
+		app.post '/admin/writeAllDocsToMongo', SecurityManager.requestIsAdmin, AdminController.writeAllToMongo
+		app.post '/admin/addquote', SecurityManager.requestIsAdmin, AdminController.addQuote
+		app.post '/admin/syncUserToSubscription', SecurityManager.requestIsAdmin, AdminController.syncUserToSubscription
+		app.post '/admin/flushProjectToTpds', SecurityManager.requestIsAdmin, AdminController.flushProjectToTpds
+		app.post '/admin/pollUsersWithDropbox', SecurityManager.requestIsAdmin, AdminController.pollUsersWithDropbox
+		app.post '/admin/updateProjectCompiler', SecurityManager.requestIsAdmin, AdminController.updateProjectCompiler
 
 		app.get '/perfTest', (req,res)->
 			res.send("hello")
@@ -190,7 +185,7 @@ module.exports = class Router
 
 		app.get '/health_check', HealthCheckController.check
 
-		app.get "/status/compiler/:Project_id", SecutiryManager.requestCanAccessProject, (req, res) ->
+		app.get "/status/compiler/:Project_id", SecurityManager.requestCanAccessProject, (req, res) ->
 			sendRes = _.once (statusCode, message)->
 				res.writeHead statusCode
 				res.end message
@@ -293,15 +288,15 @@ module.exports = class Router
 
 			client.on 'renameEntity', (entity_id, entityType, newName, callback)->
 				AuthorizationManager.ensureClientCanEditProject client, (error, project_id) =>
-					collaberationManager.renameEntity(project_id, entity_id, entityType, newName, callback)
+					EditorController.renameEntity(project_id, entity_id, entityType, newName, callback)
 
 			client.on 'moveEntity', (entity_id, folder_id, entityType, callback)->
 				AuthorizationManager.ensureClientCanEditProject client, (error, project_id) =>
-					collaberationManager.moveEntity(project_id, entity_id, folder_id, entityType, callback)
+					EditorController.moveEntity(project_id, entity_id, folder_id, entityType, callback)
 
 			client.on 'setProjectName', (window_id, newName, callback)->
 				AuthorizationManager.ensureClientCanEditProject client, (error, project_id) =>
-					collaberationManager.renameProject(project_id, window_id, newName, callback)
+					EditorController.renameProject(project_id, window_id, newName, callback)
 
 			client.on 'getProject',(callback)->
 			 	AuthorizationManager.ensureClientCanViewProject client, (error, project_id) =>
@@ -309,15 +304,15 @@ module.exports = class Router
 
 			client.on 'setRootDoc', (newRootDocID, callback)->
 				AuthorizationManager.ensureClientCanEditProject client, (error, project_id) =>
-					collaberationManager.setRootDoc(project_id, newRootDocID, callback)
+					EditorController.setRootDoc(project_id, newRootDocID, callback)
 
 			client.on 'deleteProject', (callback)->
 				AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) =>
-					collaberationManager.deleteProject(project_id, callback)
+					EditorController.deleteProject(project_id, callback)
 
 			client.on 'setPublicAccessLevel', (newAccessLevel, callback)->
 				AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) =>
-					collaberationManager.setPublicAccessLevel(project_id, newAccessLevel, callback)
+					EditorController.setPublicAccessLevel(project_id, newAccessLevel, callback)
 
 			client.on 'pdfProject', (opts, callback)->
 				AuthorizationManager.ensureClientCanViewProject client, (error, project_id) =>
@@ -328,10 +323,6 @@ module.exports = class Router
 				AuthorizationManager.ensureClientCanViewProject client, (error, project_id) =>
 					CompileManager.getLogLines project_id, callback
 
-			client.on 'distributMessage', (message)->
-				AuthorizationManager.ensureClientCanViewProject client, (error, project_id) =>
-					collaberationManager.distributMessage project_id, client, message
-
 			client.on 'changeUsersPrivlageLevel', (user_id, newPrivalageLevel)->
 				AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) =>
 					projectHandler.changeUsersPrivlageLevel project_id, user_id, newPrivalageLevel

+ 0 - 41
services/web/app/views/about/about.jade

@@ -1,41 +0,0 @@
-extends ../layout
-
-block content
-	.container
-		.row
-			.span8.offset2.span-box
-				.page-header
-					h1 About us
-				h3 Meet the team behind your favourite online LaTeX editor
-				p.team-profile
-					img(src='/img/about/henry_oswald.jpg')
-					strong Henry Oswald 
-					| built an experimental LaTeX editor in 2011 which later became ShareLaTeX. He is a trained software engineer who lives in London.
-					| Henry has been responsible for building up a reliable platform for ShareLaTeX that allows instant real-time collaboration. 
-					| Henry is a strong advocate of Test Driven Development and makes sure we keep the ShareLaTeX code clean and easy to maintain.
-				p 
-					a(href='https://twitter.com/henryoswald') Follow me on Twitter
-				p.team-profile
-					img(src='/img/about/james_allen.jpg')
-					strong James Allen 
-					| started working with Henry early in 2012 and finished his PhD in theoretical physics early in 2013. James began working on 
-					| one of the first online LaTeX editors, ScribTeX, in 2008 and he has played a large role in developing the technologies and 
-					| concepts that made ScribTeX and now ShareLaTeX possible. James is also slightly too obsessed with typography and learning the internals of how LaTeX works.
-				p(style="clear: both")
-					
-				h3 Motivation
-				p Our first priority with ShareLaTeX is to build a tool which makes life easier for all the LaTeX users out there. 
-					| The "thank you"s and success stories are a strong motivator for us, and we hope to always be able to offer a fully-functional 
-					| LaTeX editor which anyone can use for free.
-				p We also believe that charging money for tools like ShareLaTeX is important since it helps to guarantee the future of the site and 
-					| the safety of your work, as well as allowing us to focus on it full time to develop new features. We prefer to provide a valuable 
-					| service at a fair price rather than having to try run the site from adverts, or worse. As our customers, we are answerable to you and only you.
-
-				h3 Technologies
-				p
-					| We use a lot of exciting technologies to run ShareLaTeX. We have mutliple APIs behind the scenes that we've written in Node.js, but the one exception is our 
-					a(href='https://github.com/scribtex/clsi') open sourced compiler API. 
-					| This is written in Ruby on Rails. We generally write in 
-					a(href='http://coffeescript.org/') coffee-script 
-					| as we find it helps to speed up development and make our code more readable. Your data is stored in MongoDB, Redis and Amazon S3. We practice Test Driven Development (TDD) to help produce robust and clean code which is easy to refactor.
-	include ../general/small-footer

+ 0 - 31
services/web/app/views/about/attribution.jade

@@ -1,31 +0,0 @@
-extends ../layout
-
-block content
-	.container
-		.row
-			.span6.offset3.span-box
-				.page-header
-					h1 Attribution
-				p 
-					| We've only been able to create ShareLaTeX thanks to the many amazing free and
-					| open source technologies that exist. Here are some that we have been using and would
-					| like to say thank you to:
-				ul
-					li
-						a(href="http://nodejs.org/") Node.js 
-						| and the massive set of modules that are available.
-					li
-						a(href="https://github.com/jviereck/pdfListView") pdfListView. 
-						| A wrapper for PDF.js that powers our built in PDF viewer.
-					li
-						a(href="http://www.iconshock.com/") Icons from Iconshock.
-					li
-						a(href="http://twitter.github.com/bootstrap/") Bootstrap 
-						| for letting us survive without being designers.
-
-	include ../general/small-footer
-
-
-
-
-

+ 0 - 16
services/web/app/views/about/planned_maintenance.jade

@@ -1,16 +0,0 @@
-extends ../layout
-
-block content
-	.container
-		.row
-			.span6.offset3.span-box
-				.page-header
-					h1 Planned Maintenance
-				p There is currently no planned maintenance
-
-	include ../general/small-footer
-
-
-
-
-

+ 0 - 26
services/web/app/views/about/privacy.jade

@@ -1,26 +0,0 @@
-extends ../layout
-
-block content
-	.container
-		.row
-			.span8.offset2.span-box
-				.page-header
-					h1 Privacy Policy
-				h3 Information gathering
-				p When you register for ShareLaTeX we collect information such as your name and email address. ShareLaTeX uses this information to be able to provide our service, for identification and authorization of users, and to be able to contact you.
-				p The information we collect is not shared with other organisations except as detailed below for the provisioning and improvement of our service. The data we collect will never be sold to third parties for commercial purposes.
-
-				h3 Cookies
-				p ShareLaTeX uses a cookie, which is a small amount of data stored by your web browser on your computer. The cookie stores your current session and allows you to stay logged in. Cookies are required to use the ShareLaTeX service.
-
-				h3 Data storage
-				p ShareLaTeX uses third parties to host our services and store your data. You retain all rights to the data you upload to ShareLaTeX.
-
-				h3 Credit cards and billing details
-				p We use a third party vendor, 
-					a(href="recurly.com") Recurly
-					| , to store and process credit card transactions. Your email address, credit card details and billing address are passed on to Recurly and are not stored with ShareLaTeX.
-
-				h3 Third party tracking
-				p We use Google Analytics, Mixpanel and HeapAnalytics to track users' interactions with ShareLaTeX. This data is used for the improvement of our service. 
-	include ../general/small-footer

+ 0 - 94
services/web/app/views/about/security.jade

@@ -1,94 +0,0 @@
-extends ../layout
-
-block content
-	.container
-		.row
-			.span6.offset3.span-box
-				.page-header
-					h1 Security
-				p
-					| Keeping your data safe is one of our top priorities.
-					| We work hard to make sure that ShareLaTeX is as secure as we can make it,
-					| and your input and feedback on our security is always appreciated.
-				h3 Responsible disclosure
-				p
-					| Please send reports of any urgent or sensitive security issues to 
-					a(href="mailto:team@sharelatex.com") team@sharelatex.com. 
-					| Use our 
-					a(href="/sharelatex-security.pub") public key 
-					| to encrypt your message and please provide us with a secure way to
-					| contact you. 
-
-				p
-					| Note that the URLs at <strong>/learn</strong>, <strong>/help</strong>
-					| and <strong>ctan.sharelatex.com</strong> are not under our direct control,
-					| and vulnerabilities should be reported to either MediaWiki or TenderApp
-					| respectively.
-
-				p 
-					| We are very grateful for all the responsibly reported security vulnerabilities,
-					| however listing on the hall of fame is reserved for people who report
-					| vulnerabilities that were previously unknown and we regard as serious.  
-
-				h3 Acknowledgements
-				p
-					| We'd like to thank the following people who have responsibly disclosed
-					| vulnerabilities to us and helped improved the security of ShareLaTeX:
-
-				ul
-					li
-						a(href="https://twitter.com/Abdulahhusam", rel="nofollow") Abdullah Hussam Gazi
-					li
-						a(href="http://adamziaja.com", rel="nofollow") Adam Ziaja
-					li
-						a(href="http://alihassanpenetrationtester.blogspot.com/", rel="nofollow") Ali Hasan Ghauri
-					li
-						a(href="https://twitter.com/EhArvindSingh") Arvind Singh Shekhawat
-					li 
-						a(href="https://www.facebook.com/Dakshxss", rel="nofollow") Daksh Patel
-					li 
-						a(href="https://twitter.com/dibsyhex", rel="nofollow") Dibyendu Sikdar
-					li
-						a(href="https://www.facebook.com/jaymark.pestano", rel="nofollow") Jaymark Pestaño 
-					li
-						a(href="https://twitter.com/korapsyon", rel="nofollow") Jerold Camacho
-					li
-						a(href="https://twitter.com/kamilsevi", rel="nofollow") Kamil Sevi
-					li
-						a(href="https://twitter.com/kingkaustubhp", rel="nofollow") Kaustubh Padwad
-					li 'KoF2002' &amp; 'Sr33h4r!(XSS no0B)'
-					li
-						a(href="http://twitter.com/umenmactech", rel="nofollow") Manish Bhattacharya
-					li
-						a(href="http://twitter.com/Manjesh24", rel="nofollow") Manjesh S
-					li
-						a(href="https://www.facebook.com/Shahmeer.1994", rel="nofollow") Muhammad Shahmeer 
-					li
-						a(href="http://nbsriharsha.blogspot.in", rel="nofollow") N B Sri Harsha 
-					li
-						a(href="http://www.linkedin.com/in/osandamalith", rel="nofollow") Osanda Malith Jayathissa
-					li
-						a(href="https://twitter.com/prasadk14", rel="nofollow") Prasad Kancharla
-					li
-						a(href="https://www.facebook.com/c0m4dr3404", rel="nofollow") Praveen Nair (Kerala Cyber Squad - India)
-					li
-						a(href="https://twitter.com/iAmPr3m", rel="nofollow") Prem Kumar
-					li
-						a(href="https://www.facebook.com/HardNocksHittnHard", rel="nofollow") Sherin Panikar (Kerala Cyber Squad - India)
-					li
-						a(href="https://twitter.com/Simon90_Italy", rel="nofollow") Simone Memoli
-					li
-						a(href="https://twitter.com/tareksiddiki", rel="nofollow") Tarek Siddiki 
-					li
-						a(href="https://twitter.com/venugopalt", rel="nofollow") Venugopal Thotakura
-					li
-						a(href="http://softproweb.blogspot.com/", rel="nofollow") Waqeeh Ul Hasan
-					li 
-						a(href="https://twitter.com/zerodayguys", rel="nofollow") Zeroday Guys (Rakesh Singh &amp; V.Harish Kumar)
-					li 
-						a(href="https://twitter.com/_prashantnegi", rel="nofollow") Prashant Negi 
-						span and 
-						a(href="https://twitter.com/AjaySinghNegi", rel="nofollow") Singh Negi
-					li
-						a(href="https://twitter.com/mohitnitrr") Monendra Sahu
-	include ../general/small-footer

+ 0 - 34
services/web/app/views/about/tos.jade

@@ -1,34 +0,0 @@
-extends ../layout
-
-block content
-	.container
-		.row
-			.span8.offset2.span-box
-				.page-header
-					h1 Terms of Service
-				p 
-					| Thank you for taking the time to read our terms of service.
-					| We've tried to keep it simple, but you must agree to these terms in order to use ShareLaTeX.
-				h3 You must provide a valid email address
-				p 
-					| We expect you to register with a valid email address.
-					| This will be our only way to communicate with you.
-					| Changes to these terms of service and any other notifications about updates 
-					| to the service will be sent to the email address you provide.
-					| We will not be held responsible for any information not being received.
-				h3 You own your content, not us
-				p You retain all ownership, copyright and intellectual property rights to any content uploaded to ShareLaTeX. Your content will only be shared with other users of your choosing and we will never share your content with third parties without your consent. The staff of ShareLaTeX have access to your content, but we make an effort to only access it when absolutely necessary.
-				h3 You're not allowed to abuse the service
-				p ShareLaTeX is provided assuming users will act in good faith. However, we retain the right to remove any account or content that we feel is abusing the service. In the rare event of this happening, it will most likely be for one of the following reasons: using the service for illegal reasons; uploading illegal, unauthorised or objectionable content; consuming an excessive amount of computing resources.
-				h3 We're not infallible
-				p ShareLaTeX is provided on an 'as is' basis, without future promise of availability. ShareLaTeX is not liable for any damages caused by the loss or inability to access your data.
-				p [Founder's note: We try hard to provide a service which is reliable, secure and regularly backed up. While the above paragraph sounds grim, we need it there to cover ourselves incase something does go wrong. We certainly aren't planning to need to fall back on it. This assurance aside, it's always good practice to back up your own data.]
-				h3 Cancellation and refunds policy
-				p ShareLaTeX's services are paid for monthly or yearly in advance and are non-refundable. There will be no refunds or credits for partial months/years of service or downgrades. Subscription downgrades and cancellations will come into effect when your next payment is due. Upgrades to your plan will be billed immediately at the new rate for the remainder of the billing period. You may cancel your account at any time from your account subscription settings.
-				h3 Paypal Reference Transactions
-				p PayPal Reference Transactions are used to allow ShareLaTeX to make subsequent transactions on a monthly or annual basis until you cancel your account. 
-				h3 Pricing changes
-				p We will notify you of any changes to our pricing plans that affect you at least 30 days before your next payment is due. We will notify you via the email address you provided when subscribing for a paid plan.
-				h3 You must be human
-				p To protect ourselves from accidental or malicious abuse, automated scripts and programs are not permitted to register an account or access the service.
-	include ../general/small-footer

+ 0 - 46
services/web/app/views/homepage/comments.jade

@@ -1,46 +0,0 @@
-include header
-
-mixin quote(quote, author)
-	blockquote
-		p #{quote}
-		small #{author}
-
-extends ../layout
-
-block content
-	.container.box
-			.page-header
-				h2 What a few of the thousands of people using sharelatex are saying 
-			.row
-				.span4
-					<blockquote class="twitter-tweet w-align-left"><p>Yes, finally! @<a href="https://twitter.com/sharelatex">sharelatex</a> <a href="https://t.co/5qcwmrdW" title="https://www.sharelatex.com/">sharelatex.com</a> via @<a href="https://twitter.com/henryoswald">henryoswald</a></p>&mdash; Nav (@keyboardkitteh) <a href="https://twitter.com/keyboardkitteh/status/165623092637474816" data-datetime="2012-02-04T02:29:57+00:00">February 4, 2012</a></blockquote>
-				.span4
-					<blockquote class="twitter-tweet w-align-left"><p>Finally someone programmed it: The Real Time LaTeX Collaborative Editor in Your Browser <a href="https://t.co/Ke32JVdG" title="https://www.sharelatex.com">sharelatex.com</a></p>&mdash; Bar Fooz (@barfooz) <a href="https://twitter.com/barfooz/status/165889791538376705" data-datetime="2012-02-04T20:09:43+00:00">February 4, 2012</a></blockquote>
-				.span4
-					<blockquote class="twitter-tweet"><p>Wow, really awesome real time LaTeX editor <a href="https://t.co/VgXQezzr" title="https://www.sharelatex.com/">sharelatex.com</a> /via @<a href="https://twitter.com/cybunk">cybunk</a></p>&mdash; Nicolas (@philogb) <a href="https://twitter.com/philogb/status/168030573229649921" data-datetime="2012-02-10T17:56:25+00:00">February 10, 2012</a></blockquote>
-			.row
-				.span4
-					<blockquote class="twitter-tweet"><p>Its <a href="https://twitter.com/search/%2523GoogleDocs">#GoogleDocs</a> but with added <a href="https://twitter.com/search/%2523LaTeX">#LaTeX</a>, <a href="https://t.co/ZkMh44V2" title="https://www.sharelatex.com/">sharelatex.com</a>. For physicists and astronomers, collaborative editing just became possible.</p>&mdash; Alasdair Allan (@aallan) <a href="https://twitter.com/aallan/status/166472955314966528" data-datetime="2012-02-06T10:47:00+00:00">February 6, 2012</a></blockquote>
-				.span4
-					<blockquote class="twitter-tweet"><p>ShareLaTeX, a real time LaTeX collaborative editor. I *have* to try this. <a href="https://t.co/UVi5f3XY" title="https://www.sharelatex.com/">sharelatex.com</a> HT: @<a href="https://twitter.com/yokofakun">yokofakun</a></p>&mdash; Luis Apiolaza (@zentree) <a href="https://twitter.com/zentree/status/169896014604546048" data-datetime="2012-02-15T21:29:01+00:00">February 15, 2012</a></blockquote>
-				.span4
-					<blockquote class="twitter-tweet"><p>Finally! <a href="https://twitter.com/search/%2523ShareLaTeX">#ShareLaTeX</a> - Real Time LaTeX Collaborative Editor <a href="http://t.co/DYEaIOLi" title="http://ShareLaTeX.com">ShareLaTeX.com</a> (via @<a href="https://twitter.com/infoveille">infoveille</a> @<a href="https://twitter.com/gallypette">gallypette</a> @<a href="https://twitter.com/UrfistNice">UrfistNice</a>) @<a href="https://twitter.com/AdrienSaladin">AdrienSaladin</a></p>&mdash; Pierre Poulain (@pierrepo) <a href="https://twitter.com/pierrepo/status/167642895946498048" data-datetime="2012-02-09T16:15:55+00:00">February 9, 2012</a></blockquote>
-			.row
-				.span4
-					<blockquote class="twitter-tweet"><p>LaTeX geeks, here's a collaborative online LaTeX editor <a href="https://t.co/5xjcRbSy" title="https://www.sharelatex.com/">sharelatex.com</a></p>&mdash; Stephen Kinsella (@stephenkinsella) <a href="https://twitter.com/stephenkinsella/status/166838934968598529" data-datetime="2012-02-07T11:01:16+00:00">February 7, 2012</a></blockquote>
-				.span4
-					<blockquote class="twitter-tweet"><p><a href="https://t.co/uRiailxg" title="https://www.sharelatex.com/">sharelatex.com</a>amazing!</p>&mdash; Guillermo Rauch (@rauchg) <a href="https://twitter.com/rauchg/status/168031797047865344" data-datetime="2012-02-10T18:01:16+00:00">February 10, 2012</a></blockquote>
-				.span4
-					<blockquote class="twitter-tweet"><p>ShareLatex <a href="https://t.co/70tEkjEb" title="https://www.sharelatex.com/">sharelatex.com</a> FInally, the real time collaboration in browser! <a href="https://twitter.com/search/%2523latex">#latex</a></p>&mdash; Ivana Kojovic (@muvica1987) <a href="https://twitter.com/muvica1987/status/166200765638119424" data-datetime="2012-02-05T16:45:25+00:00">February 5, 2012</a></blockquote>
-			.row
-				.span4
-					<blockquote class="twitter-tweet"><p>I just fell in love on my first run with @<a href="https://twitter.com/sharelatex">sharelatex</a> <a href="https://t.co/fygg14Nj" title="https://www.sharelatex.com/">sharelatex.com</a> via @<a href="https://twitter.com/henryoswald">henryoswald</a> . CC @<a href="https://twitter.com/timcameronryan">timcameronryan</a></p>&mdash; Cory Dolphin (@wcdolphin) <a href="https://twitter.com/wcdolphin/status/200411830370312192" data-datetime="2012-05-10T02:27:58+00:00">May 10, 2012</a></blockquote>
-				.span4
-					<blockquote class="twitter-tweet"><p>Tired of sending TeX files back and forth with your coauthors?Try sharelatex to collaborate on tex files in real time <a href="http://t.co/PjoOAayE" title="http://bit.ly/xcksY7">bit.ly/xcksY7</a></p>&mdash; Max Lin (@m4xl1n) <a href="https://twitter.com/m4xl1n/status/171596254864871424" data-datetime="2012-02-20T14:05:09+00:00">February 20, 2012</a></blockquote>
-				.span4
-					<blockquote class="twitter-tweet"><p><a href="https://twitter.com/search/%2523edtech">#edtech</a> <a href="https://twitter.com/search/%2523mathchat">#mathchat</a> Collaborative LaTeX document editing (like Google docs, but for LaTeX!) <a href="https://t.co/XXwnl7SA" title="https://www.sharelatex.com/">sharelatex.com</a></p>&mdash; Joe DiNoto (@mathteacher1729) <a href="https://twitter.com/mathteacher1729/status/185889900615303168" data-datetime="2012-03-31T00:43:00+00:00">March 31, 2012</a></blockquote>
-
-	<script src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
-		
-	- locals.supressDefaultJs = true
-	script(data-main=jsPath+'main.js', src='js/libs/require.js', baseurl=jsPath)

+ 0 - 15
services/web/app/views/homepage/header.jade

@@ -1,15 +0,0 @@
-.container
-	#logoArea
-		.span8
-			h1.logo.large
-				a(href='/').plain ShareLaTeX
-				span.sub.logo Online LaTeX Editor
-		.span3#homePagePills
-			ul.nav.nav-pills
-					li
-						a(href='/') Home
-					li
-						a(href='/blog') Blog
-					li
-						a(href='/comments') Comments
-

+ 0 - 64
services/web/app/views/homepage/home.jade

@@ -1,64 +0,0 @@
-link(rel='canonical', href='https://www.sharelatex.com')
-
-extends ../layout
-
-block content
-	.masthead
-		.container
-			.row
-				.title.span8
-					h1 Online LaTeX Editor
-					h2 Quickly start using LaTeX and work together in real-time
-					div
-						img(src="/img/screenshot.png")
-				.register.span4
-					h2 Register now
-					.messageArea
-					include ../general/partial/registerForm
-	
-	.universities
-		.container
-			.row
-				p Used by students and academics at:
-				#slides
-					.clearfix
-						.span3
-							img(src="/img/crests/harvard.gif", alt="harvard university logo")
-						.span3
-							img(src="/img/crests/mit.gif", alt="mit university logo")
-						.span3
-							img(src="/img/crests/oxford.gif", alt="oxford university logo")
-						.span3
-							img(src="/img/crests/tokyo.png", alt="tokyo university logo")
-					.clearfix
-						.span3
-							img(src="/img/crests/cambridge.png", alt="cambridge university logo")
-						.span3
-							img(src="/img/crests/liverpool.jpg", alt="liverpool university logo")
-						.span3
-							img(src="/img/crests/icl.png", alt="icl university logo")
-						.span3
-							img(src="/img/crests/yale.png", alt="yale university logo")
-					.clearfix
-						.span3
-							img(src="/img/crests/durham.png", alt="durham university logo")
-						.span3
-							img(src="/img/crests/nasa.png", alt="nasa university logo")
-						.span3
-							img(src="/img/crests/toronto.gif", alt="toronto university logo")
-						.span3
-							img(src="/img/crests/stanford.png", alt="stanford university logo")
-
-
-	.container
-		include ../general/long-form-features
-
-		.row
-			.span12.lower-signup-button
-				a(href="/user/subscription/plans").btn.btn-success.btn-huge Sign up now!
-
-	include ../general/social-footer
-	include ../general/small-footer
-
-
-

+ 0 - 7
services/web/app/views/homepage/socialMedia.jade

@@ -1,7 +0,0 @@
-
-.span2.offset3
-	include ../referal/tweet
-.span2
-	include ../referal/googleplus
-.span1
-	include ../referal/facebookLike

+ 1 - 1
services/web/app/views/layout.jade

@@ -28,7 +28,7 @@ html(itemscope, itemtype='http://schema.org/Product')
 				ga('send', 'pageview');
 		- else
 			script(type='text/javascript')
-				window.ga = function() {};
+				window.ga = function() { console.log("Sending to GA", arguments) };
 
 		script
 			window.csrfToken = "#{csrfToken}";

+ 3 - 2
services/web/app/views/menubar.jade

@@ -36,8 +36,9 @@
 							ul.dropdown-menu
 								li
 									a(href='/user/settings').userSettingsLink User Settings
-								li
-									a(href='/user/subscription').subscriptionLink Subscription
+								- if (settings.enableSubscriptions)
+									li
+										a(href='/user/subscription').subscriptionLink Subscription
 								li
 									a(href='/logout').logoutLink Logout
 					-else

+ 6 - 1
services/web/app/views/referal/bonus.jade

@@ -41,6 +41,11 @@ block content
 				.title
 					a(href='#link-modal', data-toggle="modal").link Link to us from your website
 
+		.row
+			.span4.offset4.bonus-banner
+				h2.direct-link Direct Link
+				.well #{buildReferalUrl("d")}
+
 		.row.ab-bonus
 			.span6.offset3
 				p.thanks When someone starts using ShareLaTeX after your recommendation we'll give you some <strong>free stuff</strong> to say thanks! Check your progress below.
@@ -103,7 +108,7 @@ block content
 				method: 'feed',
 				redirect_uri: 'https://www.sharelatex.com',
 				link: '!{buildReferalUrl("fb")}',
-				picture: 'https://www.sharelatex.com/img/logo/logosmall.png',
+				picture: 'https://www.sharelatex.com/brand/logo/logosmall.png',
 				name: 'ShareLaTeX - Online LaTeX Editor',
 				caption: 'Free Unlimited Projects and Compiles',
 				description: 'ShareLaTeX is a free online LaTeX Editor. Real time collaboration like Google Docs, with Dropbox, history and auto-complete'

+ 1 - 1
services/web/app/views/referal/facebookWallPost.jade

@@ -11,7 +11,7 @@ script(type='text/javascript')
           method: 'feed',
           redirect_uri: 'https://www.sharelatex.com',
           link: '#{buildReferalUrl()}',
-          picture: 'https://www.sharelatex.com/img/logo/logosmall.png',
+          picture: 'https://www.sharelatex.com/brand/logo/logosmall.png',
           name: 'ShareLaTeX - Online LaTeX Editor',
           caption: 'Free Unlimited Projects and Compiles',
           description: 'ShareLaTeX is a free Online LaTeX Editor. Real time collaboration like Google Docs, with Dropbox, history and Auto Complete'

+ 0 - 59
services/web/app/views/resources.jade

@@ -1,59 +0,0 @@
-extends layout
-
-block content
-	.container
-		.row
-			.span12.span-box
-				.page-header
-					h1 LaTeX Resources
-						small &nbsp; We are the LaTeX Editor, here are some other LaTeX resources we like
-				div
-					ul
-						li 
-							a(href='http://en.wikibooks.org/wiki/LaTeX') LaTeX Wikibook
-							span &nbsp; - Covering 95% of what  you need to know about LaTeX in a clear and simple way with great examples you can often copy and paste
-						li
-							a(href='http://detexify.kirelabs.org/classify.html') Detexify
-							span &nbsp; - A great way of finding LaTeX symbols
-						li
-							a(href='http://www.tug.dk/FontCatalogue/seriffonts.html') LaTeX Fonts
-							span &nbsp; -  A collection of LaTeX fonts
-						li
-							a(href='http://mathurl.com/') MathUrl
-							span &nbsp; - allows for live equation editing
-						li
-							a(href='http://webdemo.visionobjects.com/equation.html') WebEquation 
-							span &nbsp; - draw the symbol on the screen to see the LaTeX equivalent
-						li
-							a(href='http://www.texample.net/') TeXample.net 
-							span &nbsp; - a great site for tikz reference
-						li
-							a(href='http://www.howtotex.com/') howtoTeX.com 
-							span &nbsp; - a useful collection of templates, tutorials and how-tos
-						li
-							a(href='http://truben.no/latex/table/') LaTeX table editor
-							span &nbsp; - if you struggle with tables in LaTeX this tool gives you a nice 'excel like' view to help you along
-						li
-							a(href="http://www.math.binghamton.edu/erik/beameruserguide.pdf") Beamer user guide
-							span &nbsp; - A good guide into beamer
-				div
-					h3 Mobile Apps
-					ul
-						li
-							a(href='https://play.google.com/store/apps/details?id=coolcherrytrees.software.detexify&feature=search_result#?t=W251bGwsMSwxLDEsImNvb2xjaGVycnl0cmVlcy5zb2Z0d2FyZS5kZXRleGlmeSJd') Android
-						li
-							a(href='https://itunes.apple.com/app/detexify/id328805329?mt=8') Detexify for ios
-						li 
-							a(href='http://www.windowsphone.com/en-us/store/app/detexify/3e6813bd-04b1-455b-bc14-14dfe904c54b') Detexify for Windows Phone
-						li
-							a(href='http://www.windowsphone.com/en-us/store/app/repotex/694085e0-e825-425b-a40d-40bce5cecb3b') Repotex
-						li
-							a(href='http://www.windowsphone.com/en-us/store/app/fasttexdraw/3e1b4255-7798-45ec-b679-a43e74e82769') FastTeXDraw for windows phone
-
-					hr
-					div 
-						| If you know of other resources, please 
-						a.js-tender-widget(href='#') recommend
-						&nbsp; them to us.
-	include ./general/small-footer
-

+ 2 - 1
services/web/app/views/subscriptions/group_admin.jade

@@ -15,6 +15,7 @@ block content
 			thead 
 				tr
 					th
+						input(type="checkbox").select-all
 					th email
 					th Name
 					th Registered
@@ -23,7 +24,7 @@ block content
 				-each user in users
 					tr
 						td 
-							input(type="checkbox")
+							input(type="checkbox").select-one
 						td #{user.email}
 						td #{user.first_name} #{user.last_name}
 						td #{!user.holdingAccount}

+ 42 - 27
services/web/app/views/templates.jade

@@ -68,6 +68,7 @@
 				input.rename.js-rename
 			.dropdown-caret
 				i.icon-chevron-down
+			.entity-label.label.label-success
 
 	script(type="text/template")#folderTemplate
 		.entity-list-item(class="entity-{{ type }}", entity-type="{{ type }}", id="{{ id }}")
@@ -80,6 +81,7 @@
 				input.rename.js-rename
 			.dropdown-caret
 				i.icon-chevron-down
+			.entity-label.label.label-success
 	
 	script(type="text/template")#entityListTemplate
 		.contents
@@ -180,7 +182,11 @@
 					button#downloadPdf.btn Download
 					button#downloadLinksButton.btn.dropdown-toggle(data-toggle="dropdown")
 						span.caret
-					ul.dropdown-menu#downloadLinks
+					ul.dropdown-menu
+						#downloadLinks
+						li.divider
+						li.delete-cached-files
+							a(href="#") Clear cached files
 				.btn-group.pull-right(data-toggle="buttons-radio")
 					button(type="button", title="Flat view")#flatViewButton.btn
 						i.icon-flatview
@@ -286,32 +292,31 @@
 
 
 	script(type="text/template")#publishProjectTemplate
-		-if(session && session.user && session.user.isAdmin)
-			.box
-				.page-header
-					h2 Publish project as template
-					
-				#publishedAsTemplateArea.show-when-published.alert.alert-success
-					p
-						.btn.btn-warning#unPublishProjectAsTemplate.pull-right Unpublish
-						i.icon-ok 
-						|  Your project is currently published. 
-						a#templateLink(href='{{canonicalUrl}}') View in template gallery.
-					p
-						| Lastest version: {{publishedDate}}. 
-
-				#problemWithPublishingArea
-					p There is a problem with our publishing service, please try again in a few minutes.
-				#publishWorkingArea
-					p Working...
-				div.show-when-published.show-when-unpublished.project-description
-					label(for="project-description") Description
-					.row-fluid
-						textarea(placeholder="Template description", name="project-description").span12#projectDescription {{description}}
-				#unpublishedAsTemplateArea.show-when-unpublished
-					.btn.btn-success#publishProjectAsTemplate Publish
-				p.show-when-published
-					button.btn.btn-success#republishProjectAsTemplate Re-Publish
+		.box
+			.page-header
+				h2 Publish project as template
+				
+			#publishedAsTemplateArea.show-when-published.alert.alert-success
+				p
+					.btn.btn-warning#unPublishProjectAsTemplate.pull-right Unpublish
+					i.icon-ok 
+					|  Your project is currently published. 
+					a#templateLink(href='{{canonicalUrl}}') View in template gallery.
+				p
+					| Lastest version: {{publishedDate}}. 
+
+			#problemWithPublishingArea
+				p There is a problem with our publishing service, please try again in a few minutes.
+			#publishWorkingArea
+				p Working...
+			div.show-when-published.show-when-unpublished.project-description
+				label(for="project-description") Description
+				.row-fluid
+					textarea(placeholder="Template description", name="project-description").span12#projectDescription {{description}}
+			#unpublishedAsTemplateArea.show-when-unpublished
+				.btn.btn-success#publishProjectAsTemplate Publish
+			p.show-when-published
+				button.btn.btn-success#republishProjectAsTemplate Re-Publish
 
 
 	script(type="text/template")#settingsPanelTemplate
@@ -425,6 +430,10 @@
 		div
 			a(href="#", title='Show Hot Keys List')#hotkeysLink Hot keys
 
+	script(type='text/template')#DebugLinkTemplate
+		div
+			a(href="#", title='Show Debug Information')#debugLink Debug
+
 	script(type='text/template')#trackChangesPanelTemplate
 		#trackChangesPanel
 			.track-changes-side-bar
@@ -434,6 +443,12 @@
 						i.icon-remove
 				.change-list-area
 			.track-changes-diff
+			.track-changes-upgrade-popup(style="display: none;")
+				.message.show-when-owner
+					p You need to upgrade your plan to use the History feature.
+					button.btn.btn-primary.start-free-trial Start free trial
+				.message.show-when-not-owner
+					p Please ask the project owner to upgrade to use the History feature.
 
 	script(type='text/template')#trackChangesDiffTemplate
 		.track-changes-diff-toolbar.btn-toolbar

+ 11 - 0
services/web/cleanup.js

@@ -0,0 +1,11 @@
+var keys = require('./app/js/infrastructure/Keys');
+var settings = require('settings-sharelatex');
+var queueName = process.argv[2];
+var projectQueueName = process.argv[3];
+var queue = require('fairy').connect(settings.redis.web).queue(queueName);
+console.log("cleaning up queue "+ queueName + " " + projectQueueName);
+queue._requeue_group(projectQueueName);
+
+//fairy should kill the process but just in case
+thirtySeconds = 30 * 1000
+setTimeout(process.exit, thirtySeconds)

+ 15 - 19
services/web/package.json

@@ -5,40 +5,36 @@
     "public": "./public"
   },
   "dependencies": {
-    "heapdump": "0.1.0",
     "express": "3.3.4",
-    "mongoose": "3.6.19",
+    "mongoose": "3.8.8",
+    "mongojs": "0.10.1",
     "jade": "0.28.1",
-    "validator": "0.4.22",
-    "underscore": "1.4.4",
-    "node-fs": "0.1.5",
-    "rimraf": "2.1.2",
+    "underscore": "1.6.0",
+    "rimraf": "2.2.6",
     "connect-redis": "1.4.5",
-    "redis": "0.8.4",
-    "request": "2.14.0",
+    "redis": "0.10.1",
+    "request": "2.34.0",
     "xml2js": "0.2.0",
     "dateformat": "1.0.4-1.2.3",
-    "optimist": "0.3.5",
-    "async": "0.2.9",
-    "lynx": "0.0.11",
+    "optimist": "0.6.1",
+    "async": "0.6.2",
+    "lynx": "0.1.1",
     "session.socket.io": "0.1.4",
-    "socket.io": "0.9.15",
-    "mimelib": "0.2.8",
-    "bufferedstream": "1.4.1",
-    "mixpanel": "0.0.18",
+    "socket.io": "0.9.16",
+    "mimelib": "0.2.14",
+    "bufferedstream": "1.6.0",
     "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#master",
     "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master",
     "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master",
     "soa-req-id": "git+https://github.com/sharelatex/soa-req-id.git#master",
     "fairy": "0.0.2",
-    "node-uuid": "1.4.0",
-    "mongojs": "0.9.8",
+    "node-uuid": "1.4.1",
     "nodemailer": "0.6.1",
     "bcrypt": "0.7.5",
     "archiver": "0.5.1",
-    "nodetime": "0.8.15",
     "mocha": "1.17.1",
-    "redback": "0.3.7"
+    "redback": "0.4.0",
+    "sanitizer": "0.1.1"
   },
   "devDependencies": {
     "chai": "",

+ 22 - 12
services/web/public/coffee/SubscriptionGroupsManager.coffee

@@ -7,12 +7,12 @@ require [
 
 		tableRowTemplate = '''
 			<tr>
-				<td> <input type="checkbox"></td>
+				<td> <input type="checkbox" class="select-one"></td>
 				<td> {{ email }} </td>
 				<td> {{ first_name }} {{ last_name }} </td>
 				<td> {{ !holdingAccount }} </td>
 				<td>
-					<input type="hidden" name="user_id" value="{{user_id}}" class="user_id">
+					<input type="hidden" name="user_id" value="{{_id}}" class="user_id">
 				</td>
 			</tr>
 		'''
@@ -58,16 +58,19 @@ require [
 			$form.find("input").val('')
 
 		removeUsers = (e)->
-			selectedUserRows = $('td input:checked').closest('tr').find(".user_id")
-			selectedUserRows.each (index, userRow)->
-				user_id = $(userRow).val()
-				$.ajax
-					url: "/subscription/group/user/#{user_id}"
-					type: 'DELETE'
-					data:
-						_csrf: csrfToken
-					success: ->
-						$(userRow).parents("tr").fadeOut(250)
+			selectedUserRows = $('td input.select-one:checked').closest('tr').find(".user_id").toArray()
+			do deleteNext = () ->
+				row = selectedUserRows.pop()
+				if row?
+					user_id = $(row).val()
+					$.ajax
+						url: "/subscription/group/user/#{user_id}"
+						type: 'DELETE'
+						data:
+							_csrf: csrfToken
+						success: ->
+							$(row).parents("tr").fadeOut(250)
+							deleteNext()
 
 		$form.on 'keypress', (e)->
 			if(e.keyCode == 13)
@@ -76,3 +79,10 @@ require [
 		$form.find(".addUser").on 'click', addUser
 
 		$('#deleteUsers').on 'click', removeUsers
+
+		$('input.select-all').on "change", () ->
+			if $(@).is(":checked")
+				$("input.select-one").prop( "checked", true )
+			else
+				$("input.select-one").prop( "checked", false )
+

+ 13 - 2
services/web/public/coffee/account/AccountManager.coffee

@@ -25,8 +25,19 @@ define [
 				},{
 					text: "Enter Billing Information"
 					class: "btn-primary"
-					callback: () ->
-						window.location = "/user/subscription/new?planCode=student_free_trial"
+					callback: () =>
+						options.onUpgrade?()
+						@gotoSubscriptionsPage()
+				}]
+
+		gotoSubscriptionsPage: () ->
+			window.open("/user/subscription/new?planCode=student_free_trial")
+			Modal.createModal
+				title: "Please refresh"
+				message: "Please refresh this page after starting your free trial. This will make sure all of your features are enabled."
+				buttons: [{
+					text: "OK"
+					class: ""
 				}]
 
 		showUpgradeDialog: (ide, options = {}) ->

+ 18 - 1
services/web/public/coffee/analytics/AnalyticsManager.coffee

@@ -1,4 +1,6 @@
-define () ->
+define [
+	"libs/md5"
+], () ->
 	class AnalyticsManager
 		constructor: (@ide) ->
 			@ide.editor.on "update:doc", () =>
@@ -14,3 +16,18 @@ define () ->
 					ga('send', 'event', 'editor-interaction', 'single-compile')
 				if @compileCount == 3
 					ga('send', 'event', 'editor-interaction', 'multi-compile')
+
+		getABTestBucket: (test_name, buckets = []) ->
+			hash = CryptoJS.MD5("#{@ide.user.get("id")}:#{test_name}")
+			bucketIndex = parseInt(hash.toString().slice(0,2), 16) % buckets.length
+			return buckets[bucketIndex]
+
+		startABTest: (test_name, buckets = []) ->
+			value = @getABTestBucket(test_name, buckets)
+			ga('send', 'event', 'ab_tests', test_name, "viewed-#{value}")
+			return value
+
+		endABTest: (test_name, buckets = []) ->
+			value = @getABTestBucket(test_name, buckets)
+			ga('send', 'event', 'ab_tests', test_name, "converted-#{value}")
+			return value

+ 17 - 1
services/web/public/coffee/auto-complete/AutoCompleteManager.coffee

@@ -2,10 +2,12 @@ define [
 	"auto-complete/SuggestionManager"
 	"auto-complete/Snippets"
 	"ace/autocomplete/util"
+	"ace/autocomplete"
 	"ace/range"
 	"ace/ext/language_tools"
-], (SuggestionManager, Snippets, Util) ->
+], (SuggestionManager, Snippets, Util, AutoComplete) ->
 	Range = require("ace/range").Range
+	Autocomplete = AutoComplete.Autocomplete
 
 	Util.retrievePrecedingIdentifier = (text, pos, regex) ->
 		currentLineOffset = 0
@@ -38,6 +40,20 @@ define [
 
 			@aceEditor.completers = [@suggestionManager, SnippetCompleter]
 
+			insertMatch = Autocomplete::insertMatch
+			editor = @aceEditor
+			Autocomplete::insertMatch = (data) ->
+				pos = editor.getCursorPosition()
+				range = new Range(pos.row, pos.column, pos.row, pos.column + 1)
+				nextChar = editor.session.getTextRange(range)
+
+				# If we are in \begin{it|}, then we need to remove the trailing }
+				# since it will be adding in with the autocomplete of \begin{item}...
+				if this.completions.filterText.match(/^\\begin\{/) and nextChar == "}"
+					editor.session.remove(range)
+				
+				insertMatch.call editor.completer, data
+
 			@bindToEditorEvents()
 
 		bindToEditorEvents: () ->

+ 32 - 0
services/web/public/coffee/debug/DebugManager.coffee

@@ -0,0 +1,32 @@
+define [
+	"utils/Modal"
+], (Modal) ->
+	class DebugManager
+		template: $("#DebugLinkTemplate").html()
+
+		constructor: (@ide) ->
+			@$el = $(@template)
+			$("#toolbar-footer").append(@$el)
+			@$el.on "click", (e) =>
+				e.preventDefault()
+				@showDebugModal()
+
+		showDebugModal: () ->
+			useragent = navigator.userAgent
+			server_id = document.cookie.match(/SERVERID=([^;]*)/)?[1]
+			transport = @ide.socket.socket.transport.name
+
+			new Modal(
+				title: "Debug info"
+				message: """
+					Please give this information to the ShareLaTeX team:
+					<p><pre>
+					user-agent: #{useragent}
+					server-id: #{server_id}
+					transport: #{transport}
+					</pre></p>
+				"""
+				buttons: [
+					text: "OK"
+				]
+			)

+ 44 - 4
services/web/public/coffee/editor/Document.coffee

@@ -81,6 +81,17 @@ define [
 				delete @_joinCallbacks
 
 		_onUpdateApplied: (update) ->
+			@ide.pushEvent "received-update",
+				doc_id: @doc_id
+				remote_doc_id: update?.doc
+				wantToBeJoined: @wantToBeJoined
+				update: update
+
+			if Math.random() < (@ide.disconnectRate or 0)
+				console.log "Simulating disconnect"
+				@ide.connectionManager.disconnect()
+				return
+
 			if update?.doc == @doc_id and @doc?
 				@doc.processUpdateFromServer update
 
@@ -93,6 +104,8 @@ define [
 			@doc?.updateConnectionState "disconnected"
 
 		_onReconnect: () ->
+			@ide.pushEvent "reconnected:afterJoinProject"
+
 			@connected = true
 			if @wantToBeJoined or @doc?.hasBufferedOps()
 				@_joinDoc (error) =>
@@ -137,10 +150,37 @@ define [
 
 		_bindToShareJsDocEvents: () ->
 			@doc.on "error", (error, meta) => @_onError error, meta
-			@doc.on "externalUpdate", () => @trigger "externalUpdate"
-			@doc.on "remoteop", () => @trigger "remoteop"
-			@doc.on "op:sent", () => @trigger "op:sent"
-			@doc.on "op:acknowledged", () => @trigger "op:acknowledged"
+			@doc.on "externalUpdate", () => 
+				@ide.pushEvent "externalUpdate",
+					doc_id: @doc_id
+				@trigger "externalUpdate"
+			@doc.on "remoteop", () => 
+				@ide.pushEvent "remoteop",
+					doc_id: @doc_id
+				@trigger "remoteop"
+			@doc.on "op:sent", (op) =>
+				@ide.pushEvent "op:sent",
+					doc_id: @doc_id
+					op: op
+				@trigger "op:sent"
+			@doc.on "op:acknowledged", (op) =>
+				@ide.pushEvent "op:acknowledged",
+					doc_id: @doc_id
+					op: op
+				@trigger "op:acknowledged"
+			@doc.on "op:timeout", (op) =>
+				@ide.pushEvent "op:timeout",
+					doc_id: @doc_id
+					op: op
+				@trigger "op:timeout"
+				ga?('send', 'event', 'error', "op timeout", "Op was now acknowledged - #{ide.socket.socket.transport.name}" )
+				@ide.connectionManager.reconnectImmediately()
+			@doc.on "flush", (inflightOp, pendingOp, version) =>
+				@ide.pushEvent "flush",
+					doc_id: @doc_id,
+					inflightOp: inflightOp,
+					pendingOp: pendingOp
+					v: version
 
 		_onError: (error, meta = {}) ->
 			console.error "ShareJS error", error, meta

+ 1 - 1
services/web/public/coffee/editor/Editor.coffee

@@ -71,7 +71,7 @@ define [
 		_saveSplitterState: () ->
 			if $("#editorSplitter").is(":visible")
 				state = $("#editorSplitter").layout().readState()
-				eastWidth = state.east.size
+				eastWidth = state.east.size + $("#editorSplitter .ui-layout-resizer-east").width()
 				percentWidth = eastWidth / $("#editorSplitter").width() * 100 + "%"
 				state.east.size = percentWidth
 				$.localStorage("layout.editor", state)

+ 8 - 3
services/web/public/coffee/editor/ShareJsDoc.coffee

@@ -98,7 +98,7 @@ define [
 				v: update.v
 				op_sent_at: new Date()
 			timer = setTimeout () =>
-				@_handleError new Error("Doc op was not acknowledged in time"), meta
+				@trigger "op:timeout", update
 			, @INFLIGHT_OP_TIMEOUT
 			@_doc.inflightCallbacks.push () =>
 				clearTimeout timer
@@ -109,11 +109,16 @@ define [
 		_bindToDocChanges: (doc) ->
 			submitOp = doc.submitOp
 			doc.submitOp = (args...) =>
-				@trigger "op:sent"
+				@trigger "op:sent", args...
 				doc.pendingCallbacks.push () =>
-					@trigger "op:acknowledged"
+					@trigger "op:acknowledged", args...
 				submitOp.apply(doc, args)
 
+			flush = doc.flush
+			doc.flush = (args...) =>
+				@trigger "flush", doc.inflightOp, doc.pendingOp, doc.version
+				flush.apply(doc, args)
+
 	_.extend(ShareJsDoc::, Backbone.Events)
 
 	return ShareJsDoc

+ 13 - 0
services/web/public/coffee/file-tree/EntityView.coffee

@@ -15,6 +15,7 @@ define [
 		events: () ->
 			events = {}
 			events["click ##{@model.id} > .js-clickable"] = "parentOnClick"
+			events["click ##{@model.id} > .entity-label"] = "parentOnClick"
 			events["click .dropdown-caret"] = "showContextMenuFromCaret"
 			events["contextmenu"] = "showContextMenuFromRightClick"
 			return events
@@ -29,6 +30,7 @@ define [
 			@$nameEl = @$(".name")
 			@$inputEl = @$("input.js-rename")
 			@$entityListItemEl = @$el.children(".entity-list-item")
+			@$labelEl = @$entityListItemEl.children(".entity-label")
 
 		_makeEditable: () ->
 			if @ide.isAllowedToDoIt "readAndWrite"
@@ -48,6 +50,17 @@ define [
 			@$nameEl.hide()
 			@$inputEl.show()
 
+		setLabels: (labels) ->
+			label = labels[@model.get("id")]
+			if label?
+				@$entityListItemEl.addClass("show-label")
+				@$labelEl.text("±")
+				return true
+			else
+				@$entityListItemEl.removeClass("show-label")
+				@$labelEl.text("")
+				return false
+
 		select: () ->
 			@selected = true
 			@$entityListItemEl.addClass("selected")

+ 5 - 4
services/web/public/coffee/file-tree/FileTreeManager.coffee

@@ -75,6 +75,7 @@ define [
 			children.add(entity)
 
 		openDoc: (doc, line) ->
+			return if !doc?
 			doc_id = doc.id or doc
 			@trigger "open:doc", doc_id, line: line
 			@selectEntity(doc_id)
@@ -158,6 +159,7 @@ define [
 
 
 		renameEntity: (entity, name) ->
+			name = name?.trim()
 			@ide.socket.emit 'renameEntity', entity.id, entity.get("type"), name
 			entity.set("name", name)
 
@@ -196,7 +198,7 @@ define [
 			el = $($("#newEntityModalTemplate").html())
 			input = el.find("input")
 			create = _.once () =>
-				name = input.val()
+				name = input.val()?.trim()
 				if name != ""
 					callback(name)
 			modal = new Modal
@@ -277,6 +279,5 @@ define [
 			entity.collection?.remove(entity)
 			delete @views[entity_id]
 			
-				
-
-
+		setLabels: (labels) ->
+			@view.setLabels(labels)

+ 3 - 0
services/web/public/coffee/file-tree/FileTreeView.coffee

@@ -19,4 +19,7 @@ define [
 			@rootFolderView = new RootFolderView(model: rootFolder, manager: @manager)
 			entities.append(@rootFolderView.$el)
 			@rootFolderView.render()
+
+		setLabels: (labels) ->
+			@rootFolderView.setLabels(labels)
 			

+ 16 - 0
services/web/public/coffee/file-tree/FolderView.coffee

@@ -107,11 +107,13 @@ define [
 			@$contents.hide()
 			@$toggle.find(".js-open").hide()
 			@$toggle.find(".js-closed").show()
+			@$entityListItemEl.removeClass("folder-open")
 
 		showEntries: () ->
 			@$contents.show()
 			@$toggle.find(".js-open").show()
 			@$toggle.find(".js-closed").hide()
+			@$entityListItemEl.addClass("folder-open")
 
 		onToggle: (e) ->
 			e.preventDefault()
@@ -146,3 +148,17 @@ define [
 					@manager.showUploadFileModal(@model)
 			}]
 
+		setLabels: (labels) ->
+			showLabel = false
+			for entity in @views
+				if entity.setLabels(labels)
+					showLabel = true
+
+			if showLabel
+				@$entityListItemEl.addClass("show-label")
+				@$labelEl.text("±")
+				return true
+			else
+				@$entityListItemEl.removeClass("show-label")
+				@$labelEl.text("")
+				return false

+ 14 - 7
services/web/public/coffee/ide.coffee

@@ -26,6 +26,7 @@ define [
 	"tour/IdeTour"
 	"analytics/AnalyticsManager"
 	"track-changes/TrackChangesManager"
+	"debug/DebugManager"
 	"ace/ace"
 	"libs/jquery.color"
 	"libs/jquery-layout"
@@ -59,6 +60,7 @@ define [
 	IdeTour,
 	AnalyticsManager,
 	TrackChangesManager
+	DebugManager
 ) ->
 
 
@@ -117,10 +119,10 @@ define [
 			@cursorManager = new CursorManager(@)
 			@fileViewManager = new FileViewManager(@)
 			@analyticsManager = new AnalyticsManager(@)
-			if @userSettings.trackChanges
-				@trackChangesManager = new TrackChangesManager(@)
-			else
+			if @userSettings.oldHistory
 				@historyManager = new HistoryManager(@)
+			else
+				@trackChangesManager = new TrackChangesManager(@)
 
 			@setLoadingMessage("Connecting")
 			firstConnect = true
@@ -159,14 +161,18 @@ define [
 				buttons: [ text: "OK" ]
 			}
 
+		recentEvents: []
+
+		pushEvent: (type, meta = {}) ->
+			@recentEvents.push type: type, meta: meta, date: new Date()
+			if @recentEvents.length > 40
+				@recentEvents.shift()
+
 		reportError: (error, meta = {}) ->
 			meta.client_id = @socket?.socket?.sessionid
 			meta.transport = @socket?.socket?.transport?.name
 			meta.client_now = new Date()
-			meta.last_connected = @connectionManager.lastConnected
-			meta.second_last_connected = @connectionManager.secondLastConnected
-			meta.last_disconnected = @connectionManager.lastDisconnected
-			meta.second_last_disconnected = @connectionManager.secondLastDisconnected
+			meta.recent_events = @recentEvents
 			errorObj = {}
 			for key in Object.getOwnPropertyNames(error)
 				errorObj[key] = error[key]
@@ -194,6 +200,7 @@ define [
 	ide.hotkeysManager = new HotkeysManager ide
 	ide.layoutManager.resizeAllSplitters()
 	ide.tourManager = new IdeTour ide
+	ide.debugManager = new DebugManager(ide)
 
 	ide.savingAreaManager =
 		$savingArea : $('#saving-area')

+ 9 - 4
services/web/public/coffee/ide/ConnectionManager.coffee

@@ -8,15 +8,13 @@ define [
 			@socket = @ide.socket
 			@socket.on "connect", () =>
 				@connected = true
-				@secondLastConnected = @lastConnected
-				@lastConnected = new Date()
+				@ide.pushEvent("connected")
 				@hideModal()
 				@cancelReconnect()
 
 			@socket.on 'disconnect', () =>
 				@connected = false
-				@secondLastDisconnected = @lastDisconnected
-				@lastDisconnected = new Date()
+				@ide.pushEvent("disconnected")
 				@ide.trigger "disconnect"
 				setTimeout(=>
 					ga('send', 'event', 'editor-interaction', 'disconnect')
@@ -35,6 +33,13 @@ define [
 				e.preventDefault()
 				@tryReconnect()
 			@hideModal()
+
+		reconnectImmediately: () ->
+			@disconnect()
+			@tryReconnect()
+
+		disconnect: () ->
+			@socket.disconnect()
 				
 		showModalAndStartAutoReconnect: () ->
 			@hideModal()

+ 1 - 1
services/web/public/coffee/list.coffee

@@ -144,7 +144,7 @@ require [
 		$confirm.click (e) =>
 			$confirm.attr("disabled", true)
 			$confirm.text("Creating...")
-			projectName = $modal.find('input').val()
+			projectName = $modal.find('input').val()?.trim()
 			$.ajax
 				url: '/project/new'
 				type:'POST'

+ 2 - 1
services/web/public/coffee/pdf/CompiledView.coffee

@@ -51,6 +51,7 @@ define [
 			"click #splitViewButton": ->
 				$.localStorage("layout.pdf", "split")
 				@options.manager.switchToSplitView()
+			"click .delete-cached-files > a": -> @options.manager.deleteCachedFiles()
 
 		initialize: (@options) ->
 			@ide = @options.ide
@@ -90,7 +91,7 @@ define [
 			logButtonHtml = "Logs"
 
 			if compileErrors?
-				for error in compileErrors.all
+				for error in compileErrors.errors.concat(compileErrors.warnings).concat(compileErrors.typesetting)
 					errorView = new LatexErrorView(@options.manager.ide, error)
 					errorView.render()
 					@errorViews.push(errorView)

+ 23 - 0
services/web/public/coffee/pdf/PdfManager.coffee

@@ -187,3 +187,26 @@ define [
 
 		downloadPdf: () ->
 			@ide.mainAreaManager.setIframeSrc "/project/#{@ide.project_id}/output/output.pdf?popupDownload=true"
+
+		deleteCachedFiles: () ->
+			modal = new Modal
+				title: "Clear cache?"
+				message: "This will clear all hidden LaTeX files like .aux, .bbl, etc, from our compile server. You generally don't need to do this unless you're having trouble with references. Your project files will not be deleted or changed."
+				buttons: [{
+					text: "Cancel"
+				}, {
+					text: "Clear from cache",
+					class: "btn-primary",
+					close: false
+					callback: ($button) =>
+						$button.text("Clearing...")
+						$button.prop("disabled", true)
+						$.ajax({
+							url: "/project/#{@ide.project_id}/output"
+							type: "DELETE"
+							headers:
+								"X-CSRF-Token": window.csrfToken
+							complete: () -> modal.remove()
+						})
+
+				}]

+ 7 - 5
services/web/public/coffee/project-members/ProjectMembersManager.coffee

@@ -25,10 +25,11 @@ define [
 						@publishProjectView?.refreshPublishStatus()
 
 			setupPublish = _.once =>
-				@publishProjectView = new PublishProjectView
-					ide: @ide
-					el: $("#publishProject")
-				@publishProjectView.render()
+				if @ide.security? and @ide.security.permissionsLevel == "owner"
+					@publishProjectView = new PublishProjectView
+						ide: @ide
+						el: $("#publishProject")
+					@publishProjectView.render()
 
 			setupArea()
 			if @ide?
@@ -76,17 +77,18 @@ define [
 			@ide.socket.emit "removeUserFromProject", member.id
 
 		addMember: (email, privileges) ->
+			console.log "Adding member", email
 			@ide.socket.emit "addUserToProject", email, privileges, (error, added) =>
 				if error?
 					@ide.showGenericServerErrorMessage()
 					return
 				if !added
+					console.log "got response", error, added
 					ga('send', 'event', 'subscription-funnel', 'askToUpgrade', "projectMemebrs")
 					AccountManager.askToUpgrade @ide,
 						why: "to add additional collaborators"
 						onUpgrade: () => 
 							ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', "projectMemebrs")
-							@addMember(email, privileges)
 
 		afterMemberRemoved: (memberId) ->
 			for member in @members.models

+ 2 - 2
services/web/public/coffee/settings/DropboxSettingsManager.coffee

@@ -17,11 +17,11 @@ define [
 					@tab.empty()
 					if !@ide.isAllowedToDoIt "owner"
 					else if !@project.get('features').dropbox
-						ga('send', 'event', 'subscription-funnel', 'askToUpgrade', "dropdown")
+						ga('send', 'event', 'subscription-funnel', 'askToUpgrade', "dropbox")
 						accountManager.askToUpgrade @ide,
 							onUpgrade: =>
 								@checkIfUserIsLinkedToDropbox()
-								ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', "dropdown")
+								ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', "dropbox")
 					else
 						@checkIfUserIsLinkedToDropbox()
 

+ 4 - 1
services/web/public/coffee/settings/SettingsManager.coffee

@@ -126,7 +126,10 @@ define [
 				# http://stackoverflow.com/questions/6692031/check-if-event-is-triggered-by-a-human
 				if e.originalEvent?
 					if @ide.isAllowedToDoIt "readAndWrite"
-						@project.set("name", e.target.value)
+						newName = e.target.value?.trim()
+						$("input.projectName").val(newName)
+						@project.set("name", newName)
+
 
 		bindToCompiler: ->
 			$('select#compilers').val(@project.get("compiler"))

+ 14 - 0
services/web/public/coffee/track-changes/ChangeListView.coffee

@@ -40,6 +40,9 @@ define [
 				overflow: "scroll"
 			this
 
+		remove: () ->
+			@undelegateEvents()
+
 		addItem: (model) ->
 			index = @collection.indexOf(model)
 			previousModel = @collection.models[index - 1]
@@ -208,6 +211,17 @@ define [
 			else
 				@$el.addClass("first-in-day")
 
+			@$(".change-selector-from").tooltip({
+				title: "Show back to this change",
+				placement: "left",
+				animation: false
+			})
+			@$(".change-selector-to").tooltip({
+				title: "Show up to this change",
+				placement: "left",
+				animation: false
+			})
+
 			return this
 
 		onClick: (e) ->

+ 46 - 10
services/web/public/coffee/track-changes/TrackChangesManager.coffee

@@ -3,10 +3,11 @@ define [
 	"track-changes/models/Diff"
 	"track-changes/ChangeListView"
 	"track-changes/DiffView"
+	"account/AccountManager"
 	"utils/Modal"
 	"models/Doc"
 	"moment"
-], (ChangeList, Diff, ChangeListView, DiffView, Modal, Doc, moment) ->
+], (ChangeList, Diff, ChangeListView, DiffView, AccountManager, Modal, Doc, moment) ->
 	class TrackChangesManager
 		template: $("#trackChangesPanelTemplate").html()
 		
@@ -39,23 +40,30 @@ define [
 
 		bindToFileTreeEvents: () ->
 			@ide.fileTreeManager.on "open:doc", (doc_id) =>
+				@doc_id = doc_id
 				if @enabled
-					@doc_id = doc_id
 					@updateDiff()
 
+		AB_BUCKETS: ["control", "one-week", "pop-up"]
 		show: () ->
 			@changes = new ChangeList([], project_id: @project_id, ide: @ide)
 
+			if @changeListView?
+				@changeListView.remove()
 			@changeListView = new ChangeListView(
-				collection : @changes,
-				el         : @$el.find(".change-list-area")
+				el: @$el.find(".change-list-area")
+				collection: @changes
 			)
 			@changeListView.render()
 			@changeListView.loadUntilFull (error) =>
 				@autoSelectDiff()
 
 			@changeListView.on "change_diff", (fromIndex, toIndex) =>
-				@selectDocAndUpdateDiff(fromIndex, toIndex)
+				@findDocsInChange(fromIndex, toIndex)
+				@updateLabels()
+				@updateDiff()
+
+			@showUpgradeView()
 
 			if @diffView?
 				@diffView.remove()
@@ -65,12 +73,26 @@ define [
 			@ide.fileViewManager.disable()
 			@enable()
 
+		showUpgradeView: () ->
+			@$el.find("button.start-free-trial").off "click.track-changes"
+			@$el.find("button.start-free-trial").on "click.track-changes", () => @gotoFreeTrial()
+
+			if !@ide.project.get("features").versioning
+				ga('send', 'event', 'subscription-funnel', 'askToUgrade', "trackchanges")
+				@$el.find(".track-changes-upgrade-popup").show()
+
+				if @ide.project.get("owner") == @ide.user
+					@$el.find(".show-when-not-owner").hide()
+				else
+					@$el.find(".show-when-owner").hide()
+
 		hide: () ->
 			@ide.editor.enable()
 			@ide.fileViewManager.enable()
 			@disable()
 			@ide.fileTreeManager.openDoc(@doc_id)
 			@ide.tabManager.show "code"
+			@resetLabels()
 
 		autoSelectDiff: () ->
 			if @changes.models.length == 0
@@ -92,17 +114,27 @@ define [
 			@changeListView.setSelectionRange(fromIndex, 0)
 			@updateDiff()
 
-		selectDocAndUpdateDiff: (fromIndex, toIndex) ->
-			doc_ids = []
+		findDocsInChange: (fromIndex, toIndex) ->
+			@changed_doc_ids = []
 			for change in @changes.models.slice(toIndex, fromIndex + 1)
 				for doc in change.get("docs") or []
-					doc_ids.push doc.id if doc.id not in doc_ids
+					@changed_doc_ids.push doc.id if doc.id not in @changed_doc_ids
 
-			if !@doc_id? or @doc_id not in doc_ids
-				@doc_id = doc_ids[0]
+			if !@doc_id? or @doc_id not in @changed_doc_ids
+				@doc_id = @changed_doc_ids[0]
 
 			@updateDiff()
 
+		updateLabels: () ->
+			labels = {}
+			for doc_id in @changed_doc_ids
+				labels[doc_id] = true
+			@ide.fileTreeManager.setLabels(labels)
+
+		resetLabels: () ->
+			@ide.fileTreeManager.setLabels({})
+
+
 		updateDiff: () ->
 			fromIndex = @changeListView.selectedFromIndex
 			toIndex   = @changeListView.selectedToIndex
@@ -191,4 +223,8 @@ define [
 		disable: () ->
 			@enabled = false
 
+		gotoFreeTrial: () ->
+			AccountManager.gotoSubscriptionsPage()
+			ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', "trackchanges")
+
 	return TrackChangesManager

+ 39 - 2
services/web/public/js/libs/pdfListView/PdfListView.js

@@ -445,6 +445,7 @@ var RenderingStates = {
 
 var idCounter = 0;
 
+
 /**
  * The view for a single page.
  */
@@ -477,16 +478,30 @@ PageView.prototype = {
         // Only change the width/height property of the canvas if it really
         // changed. Every assignment to the width/height property clears the
         // content of the canvas.
+
+        var outputScale = this.getOutputScale();
+
+        var scaledWidth = (Math.floor(viewport.width) * outputScale.sx) | 0;
+        var scaledHeight = (Math.floor(viewport.height) * outputScale.sy) | 0;
+
         var newWidth = Math.floor(viewport.width);
         var newHeight = Math.floor(viewport.height);
+
         if (this.canvas.width !== newWidth) {
-            this.canvas.width = newWidth;
+            this.canvas.width = scaledWidth;
+            this.canvas.style.width = newWidth + 'px';
             this.resetRenderState();
         }
         if (this.canvas.height !== newHeight) {
-            this.canvas.height = newHeight;
+            this.canvas.height = scaledHeight;
+            this.canvas.style.height = newHeight + 'px';
             this.resetRenderState();
         }
+        
+        if(outputScale.scaled){
+            var ctx = this.getCanvasContext()
+            ctx.scale(outputScale.sx, outputScale.sy)
+        }
 
         this.width = viewport.width;
         this.height = viewport.height;
@@ -523,6 +538,28 @@ PageView.prototype = {
         return this.canvas.getContext('2d');
     },
 
+    /**
+     * Returns scale factor for the canvas. It makes sense for the HiDPI displays.
+     * @return {Object} The object with horizontal (sx) and vertical (sy)
+                        scales. The scaled property is set to false if scaling is
+                        not required, true otherwise.
+     */
+
+    getOutputScale: function(){
+        var ctx = this.getCanvasContext()
+        var devicePixelRatio = window.devicePixelRatio || 1;
+        var backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
+                              ctx.mozBackingStorePixelRatio ||
+                              ctx.msBackingStorePixelRatio ||
+                              ctx.oBackingStorePixelRatio ||
+                              ctx.backingStorePixelRatio || 1;
+        var pixelRatio = devicePixelRatio / backingStoreRatio;
+        return {
+            sx: pixelRatio,
+            sy: pixelRatio,
+            scaled: pixelRatio != 1
+        };
+    },
     createNewCanvas: function() {
         if (this.canvas) {
             this.dom.removeChild(this.canvas);

+ 3 - 0
services/web/public/stylesheets/less/bonus.less

@@ -78,6 +78,7 @@
 		font-size: 20px;
 		line-height: 28px;
 		margin-bottom: 10px;
+		margin-top: 16px;
 	}
 
 	.bonus-banner {
@@ -121,6 +122,8 @@
 			background-repeat: no-repeat;
 			background-position: 16px center;
 		}
+		h2.direct-link {
+		}
 	}
 
 	p.thanks {

+ 30 - 0
services/web/public/stylesheets/less/editor.less

@@ -467,6 +467,9 @@ body.editor {
 		.dropdown-caret {
 			display: none;
 		}
+		.entity-label {
+			display: none;
+		}
 	}
 
 	.entity-folder {
@@ -527,6 +530,33 @@ body.editor {
 			}
 		}
 	}
+
+	.entity-list-item.show-label {
+		.dropdown-caret {
+			display: none;
+		}
+		.entity-label {
+			display: block;
+			position: absolute;
+			top: 3px;
+			right: 3px;
+			font-size: 13px;
+			line-height: 13px;
+			padding: 2px 6px 3px;
+			background-color: hsl(100, 80%, 42%);
+			font-weight: normal;
+			text-shadow: none;
+			&:hover {
+				background-color: hsl(100, 80%, 35%)
+			}
+		}
+	}
+
+	.entity-list-item.folder-open.show-label {
+		.entity-label {
+			display: none;
+		}
+	}
 	
 	li img, .entity-list-item i {
 		margin: 4px;

+ 43 - 0
services/web/public/stylesheets/less/trackchanges.less

@@ -79,6 +79,39 @@
 		}
 	}
 
+	.track-changes-upgrade-control, .track-changes-upgrade-popup {
+		position: absolute;
+		top: 0;
+		bottom: 0;
+		left: 0;
+		right: 0;
+		z-index: 100;
+	}
+
+	.track-changes-upgrade-popup {
+		background-color: rgba(128,128,128,0.4);
+		.message {
+			margin: auto;
+			margin-top: 200px;
+			padding: 10px 10px 14px 10px;
+			width: 400px;
+			font-weight: bold;
+			text-align: center;
+			background-color: white;
+			.border-radius(8px);
+		}
+	}
+
+	.track-changes-upgrade-control {
+		background-color: #eeeeee;
+		text-align: center;
+		.message {
+			font-size: 18px;
+			margin: 12px;
+			margin-top: 36px;
+		}
+	}
+
 	.deleted-change-background,
 	.deleted-change-foreground,
 	.inserted-change-background,
@@ -193,6 +226,10 @@
 		}
 		li.loading-changes, li.empty-message {
 			padding: 6px;
+			cursor: default;
+			&:hover {
+				background-color: inherit;
+			}
 		}
 		li.selected-change {
 			background-color: #eaeaea;
@@ -242,6 +279,11 @@
 				}
 			}
 		}
+		li.track-changes-upgrade-oneweek {
+			padding: 15px;
+			background-color: rgb(255, 251, 210);
+			cursor: default;
+		}
 	}
 	ul.change-list.hover-state {
 		li {
@@ -256,6 +298,7 @@
 		li.hover-selected {
 			.change-selectors {
 				.range {
+					top: 0;
 					background-color: #999;
 				}
 			}

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

@@ -122,6 +122,37 @@ describe "AuthenticationController", ->
 			it "should only redirect to the local path", ->
 				expect(@res.body).to.deep.equal redir: "/test"
 
+	describe "getLoggedInUserId", ->
+
+		beforeEach ->
+			@req = 
+				session :{}
+
+		it "should return the user id from the session", (done)->
+			@user_id = "2134"
+			@req.session.user = 
+				_id:@user_id
+			@AuthenticationController.getLoggedInUserId @req, (err, user_id)=>
+				expect(user_id).to.equal @user_id
+				done()
+
+		it "should return an error if there is no user on the session", (done)->
+			@AuthenticationController.getLoggedInUserId @req, (err, user_id)=>
+				expect(err).to.exist
+				done()
+
+		it "should return an error if there is no session", (done)->
+			@req = {}
+			@AuthenticationController.getLoggedInUserId @req, (err, user_id)=>
+				expect(err).to.exist
+				done()
+
+		it "should return an error if there is no req", (done)->
+			@req = {}
+			@AuthenticationController.getLoggedInUserId @req, (err, user_id)=>
+				expect(err).to.exist
+				done()
+
 	describe "getLoggedInUser", ->
 		beforeEach ->
 			@UserGetter.getUser = sinon.stub().callsArgWith(1, null, @user)

+ 23 - 4
services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee

@@ -16,7 +16,7 @@ describe "CompileController", ->
 				apis:
 					clsi:
 						url: "clsi.example.com"
-			"request": @request = {}
+			"request": @request = sinon.stub()
 			"../../models/Project": Project: @Project = {}
 			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
 			"../../infrastructure/Metrics": @Metrics =  { inc: sinon.stub() }
@@ -68,15 +68,23 @@ describe "CompileController", ->
 
 	describe "proxyToClsi", ->
 		beforeEach ->
-			@request.get = sinon.stub().returns(@proxy = {
+			@request.returns(@proxy = {
 				pipe: sinon.stub()
 				on: sinon.stub()
 			})
+			@upstream =
+				statusCode: 204
+				headers: { "mock": "header" }
+			@req.method = "mock-method"
 			@CompileController.proxyToClsi(@url = "/test", @req, @res, @next)
 
 		it "should open a request to the CLSI", ->
-			@request.get
-				.calledWith(url: "#{@settings.apis.clsi.url}#{@url}", timeout: 60 * 1000)
+			@request
+				.calledWith(
+					method: @req.method
+					url: "#{@settings.apis.clsi.url}#{@url}",
+					timeout: 60 * 1000
+				)
 				.should.equal true
 
 		it "should pass the request on to the client", ->
@@ -87,6 +95,17 @@ describe "CompileController", ->
 		it "should bind an error handle to the request proxy", ->
 			@proxy.on.calledWith("error").should.equal true
 
+	describe "deleteAuxFiles", ->
+		beforeEach ->
+			@CompileController.proxyToClsi = sinon.stub()
+			@req.params =
+				Project_id: @project_id
+			@CompileController.deleteAuxFiles @req, @res, @next
+
+		it "should proxy to the CLSI", ->
+			@CompileController.proxyToClsi
+				.calledWith("/project/#{@project_id}", @req, @res, @next)
+				.should.equal true
 
 	describe "compileAndDownloadPdf", ->
 		beforeEach ->

+ 111 - 0
services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee

@@ -609,3 +609,114 @@ describe "EditorController", ->
 				done()
 
 
+	describe "deleteProject", ->
+
+		beforeEach ->
+			@err = "errro"
+			@ProjectHandler::deleteProject = sinon.stub().callsArgWith(1, @err)
+
+		it "should call the project handler", (done)->
+			@EditorController.deleteProject @project_id, (err)=>
+				err.should.equal @err
+				@ProjectHandler::deleteProject.calledWith(@project_id).should.equal true
+				done()
+
+
+	describe "renameEntity", ->
+
+		beforeEach ->
+			@err = "errro"
+			@entity_id = "entity_id_here"
+			@entityType = "doc"
+			@newName = "bobsfile.tex"
+			@ProjectHandler::renameEntity = sinon.stub().callsArgWith(4, @err)
+			@EditorRealTimeController.emitToRoom = sinon.stub()
+
+		it "should call the project handler", (done)->
+			@EditorController.renameEntity @project_id, @entity_id, @entityType, @newName, =>
+				@ProjectHandler::renameEntity.calledWith(@project_id, @entity_id, @entityType, @newName).should.equal true
+				done()
+
+
+		it "should emit the update to the room", (done)->
+			@EditorController.renameEntity @project_id, @entity_id, @entityType, @newName, =>
+				@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'reciveEntityRename', @entity_id, @newName).should.equal true				
+				done()
+
+	describe "moveEntity", ->
+
+		beforeEach ->
+			@err = "errro"
+			@entity_id = "entity_id_here"
+			@entityType = "doc"
+			@folder_id = "313dasd21dasdsa"
+			@ProjectEntityHandler.moveEntity = sinon.stub().callsArgWith(4, @err)
+			@EditorRealTimeController.emitToRoom = sinon.stub()
+
+		it "should call the ProjectEntityHandler", (done)->
+			@EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, =>
+				@ProjectEntityHandler.moveEntity.calledWith(@project_id, @entity_id, @folder_id, @entityType).should.equal true
+				done()
+
+
+		it "should emit the update to the room", (done)->
+			@EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, =>
+				@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'reciveEntityMove', @entity_id, @folder_id).should.equal true				
+				done()
+
+	describe "renameProject", ->
+
+		beforeEach ->
+			@err = "errro"
+			@window_id = "kdsjklj290jlk"
+			@newName = "new name here"
+			@ProjectHandler::renameProject = sinon.stub().callsArgWith(3, @err)
+			@EditorRealTimeController.emitToRoom = sinon.stub()
+
+		it "should call the ProjectHandler", (done)->
+			@EditorController.renameProject @project_id, @window_id, @newName, =>
+				@ProjectHandler::renameProject.calledWith(@project_id, @window_id, @newName).should.equal true
+				done()
+
+
+		it "should emit the update to the room", (done)->
+			@EditorController.renameProject @project_id, @window_id, @newName, =>
+				@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'projectNameUpdated', @window_id, @newName).should.equal true				
+				done()
+
+
+	describe "setPublicAccessLevel", ->
+
+		beforeEach ->
+			@err = "errro"
+			@newAccessLevel = "public"
+			@ProjectHandler::setPublicAccessLevel = sinon.stub().callsArgWith(2, @err)
+			@EditorRealTimeController.emitToRoom = sinon.stub()
+
+		it "should call the ProjectHandler", (done)->
+			@EditorController.setPublicAccessLevel @project_id, @newAccessLevel, =>
+				@ProjectHandler::setPublicAccessLevel.calledWith(@project_id, @newAccessLevel).should.equal true
+				done()
+
+		it "should emit the update to the room", (done)->
+			@EditorController.setPublicAccessLevel @project_id, @newAccessLevel, =>
+				@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'publicAccessLevelUpdated', @newAccessLevel).should.equal true				
+				done()
+
+	describe "setRootDoc", ->
+
+		beforeEach ->
+			@err = "errro"
+			@newRootDocID = "21312321321"
+			@ProjectEntityHandler.setRootDoc = sinon.stub().callsArgWith(2, @err)
+			@EditorRealTimeController.emitToRoom = sinon.stub()
+
+		it "should call the ProjectEntityHandler", (done)->
+			@EditorController.setRootDoc @project_id, @newRootDocID, =>
+				@ProjectEntityHandler.setRootDoc.calledWith(@project_id, @newRootDocID).should.equal true
+				done()
+
+		it "should emit the update to the room", (done)->
+			@EditorController.setRootDoc @project_id, @newRootDocID, =>
+				@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'rootDocUpdated', @newRootDocID).should.equal true				
+				done()

+ 17 - 12
services/web/test/UnitTests/coffee/Project/ProjectDetailsHandlerTests.coffee

@@ -8,36 +8,41 @@ require('chai').should()
 describe 'Project details handler', ->
 
 	beforeEach ->
+		@project_id = "321l3j1kjkjl"
+		@user_id = "user-id-123"
+		@project = 
+			name: "project"
+			description: "this is a great project"
+			something:"should not exist"
+			compiler: "latexxxxxx"
+			owner_ref: @user_id
+		@user =
+			features: "mock-features"
 		@ProjectGetter = 
-			getProjectWithoutDocLines: sinon.stub()
+			getProjectWithoutDocLines: sinon.stub().callsArgWith(1, null, @project)
 		@ProjectModel =
 			update: sinon.stub()
+		@UserGetter =
+			getUser: sinon.stub().callsArgWith(1, null, @user)
 		@handler = SandboxedModule.require modulePath, requires:
 			"./ProjectGetter":@ProjectGetter
 			'../../models/Project': Project:@ProjectModel
+			"../User/UserGetter": @UserGetter
 			'logger-sharelatex':
 				log:->
 				err:->
-		@project_id = "321l3j1kjkjl"
-		@project = 
-			name: "project"
-			description: "this is a great project"
-			something:"should not exist"
-			compiler: "latexxxxxx"
-
 
 	describe "getDetails", ->
 
-		it "should find the project", (done)->
-			@ProjectGetter.getProjectWithoutDocLines.callsArgWith(1, null, @project)
-			@handler.getDetails @project_id, (err, details)=>
+		it "should find the project and owner", (done)->
+			@handler.getDetails @project_id, (err, details)=>				
 				details.name.should.equal @project.name
 				details.description.should.equal @project.description
 				details.compiler.should.equal @project.compiler
+				details.features.should.equal @user.features
 				assert.equal(details.something, undefined)
 				done()
 
-
 		it "should return the error", (done)->
 			error = "some error"
 			@ProjectGetter.getProjectWithoutDocLines.callsArgWith(1, error)

+ 4 - 2
services/web/test/UnitTests/coffee/Project/ProjectGetterTests.coffee

@@ -5,6 +5,7 @@ expect = chai.expect
 modulePath = "../../../../app/js/Features/Project/ProjectGetter.js"
 SandboxedModule = require('sandboxed-module')
 ObjectId = require("mongojs").ObjectId
+assert = require("chai").assert
 
 describe "ProjectGetter", ->
 	beforeEach ->
@@ -57,7 +58,8 @@ describe "ProjectGetter", ->
 			@db.users.find = (query, callback) =>
 				callback null, [@user_lookup[query._id.toString()]]
 			sinon.spy @db.users, "find"
-			@ProjectGetter.populateProjectWithUsers @project, @callback
+			@ProjectGetter.populateProjectWithUsers @project, (err, project)=>
+				@callback err, project
 
 		it "should look up each user", ->
 			for user in @users
@@ -73,5 +75,5 @@ describe "ProjectGetter", ->
 			expect(@project.collaberator_refs).to.deep.equal [@users[3], @users[4]]
 
 		it "should call the callback", ->
-			@callback.calledWith(null, @project).should.equal true
+			assert.deepEqual @callback.args[0][1], @project
 					

+ 12 - 0
services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupHandlerTests.coffee

@@ -76,11 +76,13 @@ describe "Subscription Group Handler", ->
 			@UserLocator.findById.callsArgWith(1, null, {_id:"31232"})
 
 		it "should locate the subscription", (done)->
+			@UserLocator.findById.callsArgWith(1, null, {_id:"31232"})
 			@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
 				@SubscriptionLocator.getUsersSubscription.calledWith(@adminUser_id).should.equal true
 				done()
 
 		it "should get the users by id", (done)->
+			@UserLocator.findById.callsArgWith(1, null, {_id:"31232"})
 			@subscription.member_ids = ["1234", "342432", "312312"]
 			@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
 				@UserLocator.findById.calledWith(@subscription.member_ids[0]).should.equal true
@@ -88,3 +90,13 @@ describe "Subscription Group Handler", ->
 				@UserLocator.findById.calledWith(@subscription.member_ids[2]).should.equal true
 				users.length.should.equal @subscription.member_ids.length
 				done()
+
+		it "should just return the id if the user can not be found as they may have deleted their account", (done)->
+			@UserLocator.findById.callsArgWith(1)
+			@subscription.member_ids = ["1234", "342432", "312312"]
+			@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
+				assert.deepEqual users[0], {_id:@subscription.member_ids[0]}
+				assert.deepEqual users[1], {_id:@subscription.member_ids[1]}
+				assert.deepEqual users[2], {_id:@subscription.member_ids[2]}
+				done()
+

+ 1 - 1
services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsPollingBackgroundTasksTests.coffee

@@ -32,7 +32,7 @@ describe 'third party data store', ->
 			@poller._sendToTpds = sinon.stub().callsArgWith(1, null)
 			@poller._markPollHappened = sinon.stub()
 			@poller.pollUsersWithDropbox (err)=>
-				@userModel.find.calledWith({"dropbox.access_token.oauth_token_secret":{"$exists":true}}, "_id").should.equal true
+				@userModel.find.calledWith({"dropbox.access_token.oauth_token_secret":{"$exists":true}, "features.dropbox":true}, "_id").should.equal true
 				@poller._sendToTpds.calledWith([users[0]._id, users[1]._id, users[2]._id,]).should.equal true
 				@poller._markPollHappened.called.should.equal true
 				done()

+ 1 - 1
services/web/test/UnitTests/coffee/User/UserDeleterTests.coffee

@@ -25,7 +25,7 @@ describe "UserDeleter", ->
 			cancelSubscription: sinon.stub().callsArgWith(1)
 		@UserDeleter = SandboxedModule.require modulePath, requires:
 			"../../models/User": User: @User
-			"../../managers/NewsletterManager":  @NewsletterManager
+			"../Newsletter/NewsletterManager":  @NewsletterManager
 			"../Subscription/SubscriptionHandler": @SubscriptionHandler
 			"../Project/ProjectDeleter": @ProjectDeleter
 

+ 3 - 8
services/web/test/UnitTests/coffee/Versioning/AutomaticSnapshotManagerTests.coffee

@@ -108,20 +108,15 @@ describe 'AutomaticSnapshotManager', ->
 					callback(null, @project_ids)
 				else
 					throw new Error("unexpected key: #{key}")
-			sinon.stub(@AutomaticSnapshotManager, "takeSnapshotIfRequired")
-				.callsArgWith(1)
-			@AutomaticSnapshotManager.takeAutomaticSnapshots(@callback)
+			sinon.stub(@AutomaticSnapshotManager, "takeSnapshotIfRequired").callsArgWith(1, null)
+			@AutomaticSnapshotManager.takeAutomaticSnapshots @callback
 
 		afterEach ->
 			@AutomaticSnapshotManager.takeSnapshotIfRequired.restore()
 
-		it "should call takeSnapshotIfRequired for each project id", ->
-			for project_id in @project_ids
-				@AutomaticSnapshotManager.takeSnapshotIfRequired.calledWith(project_id)
-					.should.equal true
 
 		it "should call the callback", ->
-			@callback.calledWith(null).should.equal true
+			@callback.calledWith(undefined).should.equal true
 
 
 	describe "removing project from marked set", ->

+ 9 - 7
services/web/test/smoke/coffee/SmokeTests.coffee

@@ -1,18 +1,23 @@
 chai = require("chai")
 chai.should()
 expect = chai.expect
-request = require "request"
 Settings = require "settings-sharelatex"
 
+# Monkey patch request cookies, because the new tough-cookie module
+# assumes it's not a secure cookie if the url is not HTTPS
+request = require "request"
+jar = request.jar()
+jar.getCookieString = (uri) ->
+	return @_jar.getCookieStringSync uri, secure: true
+request = request.defaults jar: jar
+
 port = Settings.internal?.web?.port or Settings.port or 3000
 buildUrl = (path) -> "http://localhost:#{port}/#{path}"
 
 describe "Opening", ->
 	before (done) ->
-		@jar = request.jar()
 		request.get {
 			url: buildUrl("register")
-			jar: @jar
 			headers:
 				"X-Forwarded-Proto": "https"
 		}, (error, response, body) =>
@@ -23,7 +28,6 @@ describe "Opening", ->
 					email: Settings.smokeTest.user
 					password: Settings.smokeTest.password
 					_csrf: csrf
-				jar: @jar
 				headers:
 					"X-Forwarded-Proto": "https"
 			}, (error, response, body) ->
@@ -33,7 +37,6 @@ describe "Opening", ->
 	it "a project", (done) ->
 		request {
 			url: buildUrl("project/#{Settings.smokeTest.projectId}")
-			jar: @jar
 			headers:
 				"X-Forwarded-Proto": "https"
 		}, (error, response, body) ->
@@ -47,12 +50,11 @@ describe "Opening", ->
 	it "the project list", (done) ->
 		request {
 			url: buildUrl("project")
-			jar: @jar
 			headers:
 				"X-Forwarded-Proto": "https"
 		}, (error, response, body) ->
 			expect(error, "smoke test: error returned in getting project list").to.not.exist
-			expect(response.statusCode, "smoke test: response code is not 200 getting project life").to.equal(200)
+			expect(response.statusCode, "smoke test: response code is not 200 getting project list").to.equal(200)
 			expect(!!body.match("<title>Your Projects - Online LaTeX Editor ShareLaTeX</title>"), "smoke test: body does not have correct title").to.equal true
 			expect(!!body.match("<h1>Projects</h1>"), "smoke test: body does not have correct h1").to.equal true
 			done()