Explorar o código

Merge branch 'master' into node-4.2

Henry Oswald %!s(int64=10) %!d(string=hai) anos
pai
achega
23cb6a9419
Modificáronse 100 ficheiros con 5988 adicións e 696 borrados
  1. 125 42
      services/web/Gruntfile.coffee
  2. 16 0
      services/web/README.md
  3. 9 4
      services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
  4. 14 2
      services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee
  5. 39 0
      services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee
  6. 31 0
      services/web/app/coffee/Features/BetaProgram/BetaProgramHandler.coffee
  7. 3 1
      services/web/app/coffee/Features/Blog/BlogController.coffee
  8. 19 2
      services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
  9. 68 0
      services/web/app/coffee/Features/Compile/ClsiFormatChecker.coffee
  10. 39 22
      services/web/app/coffee/Features/Compile/ClsiManager.coffee
  11. 82 22
      services/web/app/coffee/Features/Compile/CompileController.coffee
  12. 14 6
      services/web/app/coffee/Features/Compile/CompileManager.coffee
  13. 4 0
      services/web/app/coffee/Features/Email/EmailBuilder.coffee
  14. 2 2
      services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
  15. 19 8
      services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
  16. 9 6
      services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee
  17. 2 0
      services/web/app/coffee/Features/Project/ProjectController.coffee
  18. 5 3
      services/web/app/coffee/Features/Project/ProjectDeleter.coffee
  19. 5 0
      services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
  20. 4 2
      services/web/app/coffee/Features/References/ReferencesController.coffee
  21. 2 0
      services/web/app/coffee/Features/References/ReferencesHandler.coffee
  22. 32 2
      services/web/app/coffee/Features/StaticPages/UniversityController.coffee
  23. 2 2
      services/web/app/coffee/Features/Subscription/LimitationsManager.coffee
  24. 236 34
      services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee
  25. 4 3
      services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
  26. 2 1
      services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
  27. 11 8
      services/web/app/coffee/Features/User/UserController.coffee
  28. 10 2
      services/web/app/coffee/Features/User/UserCreator.coffee
  29. 1 1
      services/web/app/coffee/Features/User/UserHandler.coffee
  30. 2 0
      services/web/app/coffee/Features/User/UserPagesController.coffee
  31. 2 1
      services/web/app/coffee/Features/User/UserRegistrationHandler.coffee
  32. 126 0
      services/web/app/coffee/Features/User/UserSessionsManager.coffee
  33. 0 81
      services/web/app/coffee/Features/Wiki/WikiController.coffee
  34. 63 7
      services/web/app/coffee/infrastructure/ExpressLocals.coffee
  35. 10 7
      services/web/app/coffee/infrastructure/Server.coffee
  36. 1 0
      services/web/app/coffee/models/User.coffee
  37. 40 12
      services/web/app/coffee/router.coffee
  38. 18 15
      services/web/app/views/admin/index.jade
  39. 47 0
      services/web/app/views/beta_program/opt_in.jade
  40. 14 2
      services/web/app/views/contact-us-modal.jade
  41. 2 2
      services/web/app/views/general/500.jade
  42. 83 13
      services/web/app/views/layout.jade
  43. 3 4
      services/web/app/views/layout/footer.jade
  44. 5 1
      services/web/app/views/layout/navbar.jade
  45. 10 6
      services/web/app/views/project/editor.jade
  46. 1 1
      services/web/app/views/project/editor/editor.jade
  47. 1 1
      services/web/app/views/project/editor/left-menu.jade
  48. 133 7
      services/web/app/views/project/editor/pdf.jade
  49. 1 1
      services/web/app/views/project/editor/share.jade
  50. 5 16
      services/web/app/views/project/list/side-bar.jade
  51. 1 1
      services/web/app/views/sentry.jade
  52. 17 13
      services/web/app/views/subscriptions/dashboard.jade
  53. 1 1
      services/web/app/views/subscriptions/edit-billing-details.jade
  54. 2 2
      services/web/app/views/subscriptions/successful_subscription.jade
  55. 1 1
      services/web/app/views/translations/translation_message.jade
  56. 19 0
      services/web/app/views/university/case_study.jade
  57. 11 0
      services/web/app/views/user/settings.jade
  58. 0 68
      services/web/app/views/wiki/page.jade
  59. 18 6
      services/web/config/settings.defaults.coffee
  60. 3853 0
      services/web/npm-shrinkwrap.json
  61. 12 7
      services/web/package.json
  62. 12 1
      services/web/public/coffee/ide.coffee
  63. 4 2
      services/web/public/coffee/ide/editor/directives/aceEditor.coffee
  64. 2 0
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
  65. 3 1
      services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee
  66. 20 0
      services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee
  67. 91 0
      services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
  68. 150 38
      services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
  69. 12 66
      services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee
  70. 2 2
      services/web/public/coffee/ide/pdfng/directives/pdfPage.coffee
  71. 126 78
      services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee
  72. 5 4
      services/web/public/coffee/ide/pdfng/directives/pdfSpinner.coffee
  73. 41 20
      services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee
  74. 25 1
      services/web/public/coffee/ide/settings/services/settings.coffee
  75. 3 1
      services/web/public/coffee/ide/share/controllers/ShareController.coffee
  76. 2 2
      services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee
  77. 1 0
      services/web/public/coffee/libs.coffee
  78. 3 1
      services/web/public/coffee/main/account-upgrade.coffee
  79. 29 3
      services/web/public/coffee/main/contact-us.coffee
  80. 46 2
      services/web/public/coffee/main/event.coffee
  81. 3 9
      services/web/public/coffee/main/learn.coffee
  82. 19 1
      services/web/public/coffee/main/new-subscription.coffee
  83. 3 4
      services/web/public/coffee/main/project-list/left-hand-menu-promo-controller.coffee
  84. 3 9
      services/web/public/coffee/main/templates.coffee
  85. 14 0
      services/web/public/coffee/services/algolia-search.coffee
  86. 58 0
      services/web/public/coffee/services/log-hints-feedback.coffee
  87. BIN=BIN
      services/web/public/img/about/brian_gough.jpg
  88. BIN=BIN
      services/web/public/img/about/geri.jpg
  89. BIN=BIN
      services/web/public/img/about/henry_oswald.jpg
  90. BIN=BIN
      services/web/public/img/about/james_allen.jpg
  91. BIN=BIN
      services/web/public/img/about/kiri_channon.jpg
  92. BIN=BIN
      services/web/public/img/about/michael.jpg
  93. BIN=BIN
      services/web/public/img/about/paulo_reis.jpg
  94. BIN=BIN
      services/web/public/img/about/shane_kilkelly.jpg
  95. BIN=BIN
      services/web/public/img/crests/cambridge.png
  96. BIN=BIN
      services/web/public/img/crests/durham.png
  97. BIN=BIN
      services/web/public/img/crests/icl.png
  98. BIN=BIN
      services/web/public/img/crests/liverpool.jpg
  99. BIN=BIN
      services/web/public/img/crests/mit.gif
  100. BIN=BIN
      services/web/public/img/crests/nasa.png

+ 125 - 42
services/web/Gruntfile.coffee

@@ -7,18 +7,69 @@ module.exports = (grunt) ->
 	grunt.loadNpmTasks 'grunt-mocha-test'
 	grunt.loadNpmTasks 'grunt-available-tasks'
 	grunt.loadNpmTasks 'grunt-contrib-requirejs'
-	grunt.loadNpmTasks 'grunt-execute'
 	grunt.loadNpmTasks 'grunt-bunyan'
 	grunt.loadNpmTasks 'grunt-sed'
 	grunt.loadNpmTasks 'grunt-git-rev-parse'
 	grunt.loadNpmTasks 'grunt-file-append'
 	grunt.loadNpmTasks 'grunt-file-append'
 	grunt.loadNpmTasks 'grunt-env'
+	grunt.loadNpmTasks 'grunt-newer'
+	grunt.loadNpmTasks 'grunt-contrib-watch'
+	grunt.loadNpmTasks 'grunt-parallel'
+	grunt.loadNpmTasks 'grunt-exec'
+	# grunt.loadNpmTasks 'grunt-contrib-imagemin'
+	# grunt.loadNpmTasks 'grunt-sprity'
 
 	config =
-		execute:
-			app:
-				src: "app.js"
+
+		exec:
+			run:
+				command:"node app.js | ./node_modules/logger-sharelatex/node_modules/bunyan/bin/bunyan --color"
+			cssmin:
+				command:"node_modules/clean-css/bin/cleancss --s0 -o public/stylesheets/style.css public/stylesheets/style.css"
+
+
+		watch:
+			coffee:
+				files: 'public/**/*.coffee'
+				tasks: ['quickcompile:coffee']
+				options: {}
+
+			less:
+				files: '**/*.less'
+				tasks: ['compile:css']
+				options: {}
+
+
+		parallel:
+			run:
+				tasks:['exec', 'watch']
+				options:
+					grunt:true
+					stream:true
+
+
+		# imagemin:
+		# 	dynamic:                       
+		# 		files: [{
+		# 			expand: true
+		# 			cwd: 'public/img/'
+		# 			src: ['**/*.{png,jpg,gif}']
+		# 			dest: 'public/img/'
+		# 		}]
+		# 	options:
+		# 		interlaced:false
+		# 		optimizationLevel: 7
+
+		# sprity:
+		# 	sprite:
+		# 		options:
+		# 			cssPath:"/img/"
+		# 			'style': '../../public/stylesheets/app/sprites.less'
+		# 			margin: 0
+		# 		src: ['./public/img/flags/24/*.png']
+		# 		dest: './public/img/sprite'
+
 
 		coffee:
 			app_dir: 
@@ -84,11 +135,16 @@ module.exports = (grunt) ->
 				files:
 					"public/stylesheets/style.css": "public/stylesheets/style.less"
 
+
+
+
 		env:
 			run:
 				add: 
 					NODE_TLS_REJECT_UNAUTHORIZED:0
 
+
+
 		requirejs:
 			compile:
 				options:
@@ -170,44 +226,47 @@ module.exports = (grunt) ->
 				pattern: "@@RELEASE@@"
 				replacement: process.env.BUILD_NUMBER || "(unknown build)"
 
+
+			
+
 		availabletasks:
 			tasks:
 				options:
-		            filter: 'exclude',
-		            tasks: [
-		            	'coffee'
-		            	'less'
-		            	'clean'
-		            	'mochaTest'
-		            	'availabletasks'
-		            	'wrap_sharejs'
-		            	'requirejs'
-		            	'execute'
-		            	'bunyan'
-		           	]
-		            groups:
-		            	"Compile tasks": [
-		            		"compile:server"
-		            		"compile:client"
-		            		"compile:tests"
-		            		"compile"
-		            		"compile:unit_tests"
-		            		"compile:smoke_tests"
-		            		"compile:css"
-		            		"compile:minify"
-		            		"install"
-		            	]
-		            	"Test tasks": [
-		            		"test:unit"
-		            		"test:acceptance"
-		            	]
-		            	"Run tasks": [
-		            		"run"
-		            		"default"
-		            	]
-		            	"Misc": [
-		            		"help"
-		            	]
+					filter: 'exclude',
+					tasks: [
+						'coffee'
+						'less'
+						'clean'
+						'mochaTest'
+						'availabletasks'
+						'wrap_sharejs'
+						'requirejs'
+						'execute'
+						'bunyan'
+					]
+					groups:
+						"Compile tasks": [
+							"compile:server"
+							"compile:client"
+							"compile:tests"
+							"compile"
+							"compile:unit_tests"
+							"compile:smoke_tests"
+							"compile:css"
+							"compile:minify"
+							"install"
+						]
+						"Test tasks": [
+							"test:unit"
+							"test:acceptance"
+						]
+						"Run tasks": [
+							"run"
+							"default"
+						]
+						"Misc": [
+							"help"
+						]
 
 	moduleCompileServerTasks = []
 	moduleCompileUnitTestTasks = []
@@ -304,12 +363,14 @@ module.exports = (grunt) ->
 	grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir', 'compile:modules:server']
 	grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs', "compile:modules:client", 'compile:modules:inject_clientside_includes']
 	grunt.registerTask 'compile:css', 'Compile the less files to css', ['less']
-	grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append"]
+	grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append", "exec:cssmin",]
 	grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests']
 	grunt.registerTask 'compile:acceptance_tests', 'Compile the acceptance tests', ['clean:acceptance_tests', 'coffee:acceptance_tests']
 	grunt.registerTask 'compile:smoke_tests', 'Compile the smoke tests', ['coffee:smoke_tests']
 	grunt.registerTask 'compile:tests', 'Compile all the tests', ['compile:smoke_tests', 'compile:unit_tests']
 	grunt.registerTask 'compile', 'Compiles everything need to run web-sharelatex', ['compile:server', 'compile:client', 'compile:css']
+	grunt.registerTask 'quickcompile:coffee', 'Compiles only changed coffee files',['newer:coffee']
+
 
 	grunt.registerTask 'install', "Compile everything when installing as an npm module", ['compile']
 
@@ -319,11 +380,13 @@ module.exports = (grunt) ->
 	
 	grunt.registerTask 'test:modules:unit', 'Run the unit tests for the modules', ['compile:modules:server', 'compile:modules:unit_tests'].concat(moduleUnitTestTasks)
 
-	grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'bunyan', 'env:run', 'execute']
+	grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'parallel']
+
 	grunt.registerTask 'default', 'run'
 
 	grunt.registerTask 'version', "Write the version number into sentry.jade", ['git-rev-parse', 'sed']
 
+
 	grunt.registerTask 'create-admin-user', "Create a user with the given email address and make them an admin. Update in place if the user already exists", () ->
 		done = @async()
 		email = grunt.option("email")
@@ -356,4 +419,24 @@ module.exports = (grunt) ->
 							#{settings.siteUrl}/user/password/set?passwordResetToken=#{token}
 						
 					"""
-					done()
+					done()
+
+	grunt.registerTask 'delete-user', "deletes a user and all their data", () ->
+		done = @async()
+		email = grunt.option("email")
+		if !email?
+			console.error "Usage: grunt delete-user --email joe@example.com"
+			process.exit(1)
+		settings = require "settings-sharelatex"
+		UserGetter = require "./app/js/Features/User/UserGetter"
+		UserDeleter = require "./app/js/Features/User/UserDeleter"
+		UserGetter.getUser email:email, (error, user) ->
+			if error?
+				throw error
+			if !user?
+				console.log("user #{email} not in database, potentially already deleted")
+				return done()
+			UserDeleter.deleteUser user._id, (err)->
+				if err?
+					throw err
+				done()

+ 16 - 0
services/web/README.md

@@ -6,9 +6,17 @@ web-sharelatex is the front-end web service of the open-source web-based collabo
 It serves all the HTML pages, CSS and javascript to the client. web-sharelatex also contains 
 a lot of logic around creating and editing projects, and account management.
 
+
 The rest of the ShareLaTeX stack, along with information about contributing can be found in the 
 [sharelatex/sharelatex](https://github.com/sharelatex/sharelatex) repository.
 
+Build process
+----------------
+
+web-sharelatex uses [Grunt](http://gruntjs.com/) to build its front-end related assets.
+
+Image processing tasks are commented out in the gruntfile and the needed packages aren't presently in the project's `package.json`. If the images need to be processed again (minified and sprited), start by fetching the packages (`npm install grunt-contrib-imagemin grunt-sprity`), then *decomment* the tasks in `Gruntfile.coffee`. After this, the tasks can be called (explicitly, via `grunt imagemin` and `grunt sprity`).
+
 Unit test status
 ----------------
 
@@ -42,3 +50,11 @@ in the `public/img/iconshock` directory found via
 [findicons.com](http://findicons.com/icon/498089/height?id=526085#)
 
 
+## Acceptance Tests
+
+To run the Acceptance tests:
+
+- set `allowPublicAccess` to true, either in the configuration file,
+  or by setting the environment variable `SHARELATEX_ALLOW_PUBLIC_ACCESS` to `true`
+- start the server (`grunt`)
+- in a separate terminal, run `grunt test:acceptance`

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

@@ -9,11 +9,12 @@ Url = require("url")
 Settings = require "settings-sharelatex"
 basicAuth = require('basic-auth-connect')
 UserHandler = require("../User/UserHandler")
+UserSessionsManager = require("../User/UserSessionsManager")
 
 module.exports = AuthenticationController =
 	login: (req, res, next = (error) ->) ->
 		AuthenticationController.doLogin req.body, req, res, next
-	
+
 	doLogin: (options, req, res, next) ->
 		email = options.email?.toLowerCase()
 		password = options.password
@@ -61,7 +62,7 @@ module.exports = AuthenticationController =
 	requireLogin: () ->
 		doRequest = (req, res, next = (error) ->) ->
 			if !req.session.user?
-				AuthenticationController._redirectToLoginOrRegisterPage(req, res) 
+				AuthenticationController._redirectToLoginOrRegisterPage(req, res)
 			else
 				req.user = req.session.user
 				return next()
@@ -92,9 +93,9 @@ module.exports = AuthenticationController =
 
 	_redirectToLoginOrRegisterPage: (req, res)->
 		if req.query.zipUrl? or req.query.project_name?
-			return AuthenticationController._redirectToRegisterPage(req, res) 
+			return AuthenticationController._redirectToRegisterPage(req, res)
 		else
-			AuthenticationController._redirectToLoginPage(req, res) 
+			AuthenticationController._redirectToLoginPage(req, res)
 
 
 	_redirectToLoginPage: (req, res) ->
@@ -132,6 +133,8 @@ module.exports = AuthenticationController =
 			isAdmin: user.isAdmin
 			email: user.email
 			referal_id: user.referal_id
+			session_created: (new Date()).toISOString()
+			ip_address: req.ip
 		# Regenerate the session to get a new sessionID (cookie value) to
 		# protect against session fixation attacks
 		oldSession = req.session
@@ -141,4 +144,6 @@ module.exports = AuthenticationController =
 			req.session[key] = value
 
 		req.session.user = lightUser
+
+		UserSessionsManager.trackSession(user, req.sessionID, () ->)
 		callback()

+ 14 - 2
services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee

@@ -1,8 +1,11 @@
+Settings = require "settings-sharelatex"
 User = require("../../models/User").User
 {db, ObjectId} = require("../../infrastructure/mongojs")
 crypto = require 'crypto'
 bcrypt = require 'bcrypt'
 
+BCRYPT_ROUNDS = Settings?.security?.bcryptRounds or 12
+
 module.exports = AuthenticationManager =
 	authenticate: (query, password, callback = (error, user) ->) ->
 		# Using Mongoose for legacy reasons here. The returned User instance
@@ -15,7 +18,9 @@ module.exports = AuthenticationManager =
 					bcrypt.compare password, user.hashedPassword, (error, match) ->
 						return callback(error) if error?
 						if match
-							callback null, user
+							AuthenticationManager.checkRounds user, user.hashedPassword, password, (err) ->
+								return callback(err) if err?
+								callback null, user
 						else
 							callback null, null
 				else
@@ -24,7 +29,7 @@ module.exports = AuthenticationManager =
 				callback null, null
 
 	setUserPassword: (user_id, password, callback = (error) ->) ->
-		bcrypt.genSalt 7, (error, salt) ->
+		bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) ->
 			return callback(error) if error?
 			bcrypt.hash password, salt, (error, hash) ->
 				return callback(error) if error?
@@ -35,3 +40,10 @@ module.exports = AuthenticationManager =
 					$unset: password: true
 				}, callback)
 
+	checkRounds: (user, hashedPassword, password, callback = (error) ->) ->
+		# check current number of rounds and rehash if necessary
+		currentRounds = bcrypt.getRounds hashedPassword
+		if currentRounds < BCRYPT_ROUNDS
+			AuthenticationManager.setUserPassword user._id, password, callback
+		else
+			callback()

+ 39 - 0
services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee

@@ -0,0 +1,39 @@
+BetaProgramHandler = require './BetaProgramHandler'
+UserLocator = require "../User/UserLocator"
+Settings = require "settings-sharelatex"
+logger = require 'logger-sharelatex'
+
+
+module.exports = BetaProgramController =
+
+	optIn: (req, res, next) ->
+		user_id = req?.session?.user?._id
+		logger.log {user_id}, "user opting in to beta program"
+		if !user_id
+			return next(new Error("no user id in session"))
+		BetaProgramHandler.optIn user_id, (err) ->
+			if err
+				return next(err)
+			return res.redirect "/beta/participate"
+
+	optOut: (req, res, next) ->
+		user_id = req?.session?.user?._id
+		logger.log {user_id}, "user opting out of beta program"
+		if !user_id
+			return next(new Error("no user id in session"))
+		BetaProgramHandler.optOut user_id, (err) ->
+			if err
+				return next(err)
+			return res.redirect "/beta/participate"
+
+	optInPage: (req, res, next)->
+		user_id = req.session?.user?._id
+		logger.log {user_id}, "showing beta participation page for user"
+		UserLocator.findById user_id, (err, user)->
+			if err
+				logger.err {err, user_id}, "error fetching user"
+				return next(err)
+			res.render 'beta_program/opt_in',
+				title:'sharelatex_beta_program'
+				user: user,
+				languages: Settings.languages,

+ 31 - 0
services/web/app/coffee/Features/BetaProgram/BetaProgramHandler.coffee

@@ -0,0 +1,31 @@
+User = require("../../models/User").User
+logger = require 'logger-sharelatex'
+metrics = require("../../infrastructure/Metrics")
+
+module.exports = BetaProgramHandler =
+
+	optIn: (user_id, callback=(err)->) ->
+		User.findById user_id, (err, user) ->
+			if err
+				logger.err {err, user_id}, "problem adding user to beta"
+				return callback(err)
+			metrics.inc "beta-program.opt-in"
+			user.betaProgram = true
+			user.save (err) ->
+				if err
+					logger.err {err, user_id}, "problem adding user to beta"
+					return callback(err)
+				return callback(null)
+
+	optOut: (user_id, callback=(err)->) ->
+		User.findById user_id, (err, user) ->
+			if err
+				logger.err {err, user_id}, "problem removing user from beta"
+				return callback(err)
+			metrics.inc "beta-program.opt-out"
+			user.betaProgram = false
+			user.save (err) ->
+				if err
+					logger.err {err, user_id}, "problem removing user from beta"
+					return callback(err)
+				return callback(null)

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

@@ -20,13 +20,15 @@ module.exports = BlogController =
 
 		logger.log url:url, "proxying request to blog api"
 		request.get blogUrl, (err, r, data)->
-			if r?.statusCode == 404
+			if r?.statusCode == 404 or r?.statusCode == 403
 				return ErrorController.notFound(req, res, next)
 			if err?
 				return res.send 500
 			data = data.trim()
 			try
 				data = JSON.parse(data)
+				if settings.cdn?.web?.host?
+					data?.content = data?.content?.replace(/src="([^"]+)"/g, "src='#{settings.cdn?.web?.host}$1'");
 			catch err
 				logger.err err:err, data:data, "error parsing data from data"
 			res.render "blog/blog_holder", data

+ 19 - 2
services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee

@@ -30,12 +30,17 @@ module.exports = CollaboratorsHandler =
 	getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
 		CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
 			return callback(error) if error?
+			result = []
 			async.mapLimit members, 3,
 				(member, cb) ->
 					UserGetter.getUser member.id, (error, user) ->
 						return cb(error) if error?
-						return cb(null, { user: user, privilegeLevel: member.privilegeLevel })
-				callback
+						if user?
+							result.push { user: user, privilegeLevel: member.privilegeLevel }
+						cb()
+				(error) ->
+					return callback(error) if error?
+					callback null, result
 	
 	getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) ->
 		# In future if the schema changes and getting all member ids is more expensive (multiple documents)
@@ -82,6 +87,18 @@ module.exports = CollaboratorsHandler =
 				logger.error err: err, "problem removing user from project collaberators"
 			callback(err)
 	
+	removeUserFromAllProjets: (user_id, callback = (error) ->) ->
+		CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, { _id: 1 }, (error, readAndWriteProjects = [], readOnlyProjects = []) ->
+			return callback(error) if error?
+			allProjects = readAndWriteProjects.concat(readOnlyProjects)
+			jobs = []
+			for project in allProjects
+				do (project) ->
+					jobs.push (cb) ->
+						return cb() if !project?
+						CollaboratorsHandler.removeUserFromProject project._id, user_id, cb
+			async.series jobs, callback
+	
 	addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) ->
 		emails = mimelib.parseAddresses(unparsed_email)
 		email = emails[0]?.address?.toLowerCase()

+ 68 - 0
services/web/app/coffee/Features/Compile/ClsiFormatChecker.coffee

@@ -0,0 +1,68 @@
+_ = require("lodash")
+async = require("async")
+
+module.exports = ClsiFormatChecker =
+
+	checkRecoursesForProblems: (resources, callback)->
+		jobs = 
+			conflictedPaths: (cb)->
+				ClsiFormatChecker._checkForConflictingPaths resources, cb
+
+			sizeCheck: (cb)->
+				ClsiFormatChecker._checkDocsAreUnderSizeLimit resources, cb
+
+		async.series jobs, (err, problems)->
+			if err? 
+				return callback(err)
+			
+			problems = _.omitBy(problems, _.isEmpty)
+
+			if _.isEmpty(problems)
+				return callback()
+			else
+				callback(null, problems)
+
+
+	_checkForConflictingPaths: (resources, callback)->
+		paths = _.map(resources, 'path')
+
+		conflicts = _.filter paths, (path)->
+			matchingPaths = _.filter paths, (checkPath)->
+				return checkPath.indexOf(path+"/") != -1
+
+			return matchingPaths.length > 0
+
+		conflictObjects = _.map conflicts, (conflict)->
+			path:conflict
+
+		callback null, conflictObjects
+
+	_checkDocsAreUnderSizeLimit: (resources, callback)->
+		
+		FIVEMB = 1000 * 1000 * 5
+
+		totalSize = 0
+
+		sizedResources = _.map resources, (resource)->
+			result = {path:resource.path}
+			if resource.content?
+				result.size = resource.content.replace(/\n/g).length
+				result.kbSize = Math.ceil(result.size / 1000)
+			else
+				result.size = 0
+			totalSize += result.size
+			return result
+
+		tooLarge = totalSize > FIVEMB
+		if !tooLarge
+			return callback()
+		else
+			sizedResources = _.sortBy(sizedResources, "size").reverse().slice(0, 10)
+			return callback(null, {resources:sizedResources, totalSize:totalSize})
+
+
+
+
+
+
+

+ 39 - 22
services/web/app/coffee/Features/Compile/ClsiManager.coffee

@@ -7,15 +7,24 @@ ProjectEntityHandler = require("../Project/ProjectEntityHandler")
 logger = require "logger-sharelatex"
 Url = require("url")
 ClsiCookieManager = require("./ClsiCookieManager")
-
+_ = require("underscore")
+async = require("async")
+ClsiFormatChecker = require("./ClsiFormatChecker")
 
 module.exports = ClsiManager =
 
-	sendRequest: (project_id, options = {}, callback = (error, success) ->) ->
+	sendRequest: (project_id, user_id, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) ->
 		ClsiManager._buildRequest project_id, options, (error, req) ->
 			return callback(error) if error?
 			logger.log project_id: project_id, "sending compile to CLSI"
-			ClsiManager._postToClsi project_id, req, options.compileGroup, (error, response) ->
+			ClsiFormatChecker.checkRecoursesForProblems req.compile?.resources, (err, validationProblems)->
+				if err?
+					logger.err err, project_id, "could not check resources for potential problems before sending to clsi"
+					return callback(err)
+				if validationProblems?
+					logger.log project_id:project_id, validationProblems:validationProblems, "problems with users latex before compile was attempted"
+					return callback(null, "validation-problems", null, null, validationProblems)
+			ClsiManager._postToClsi project_id, user_id, req, options.compileGroup, (error, response) ->
 				if error?
 					logger.err err:error, project_id:project_id, "error sending request to clsi"
 					return callback(error)
@@ -27,13 +36,19 @@ module.exports = ClsiManager =
 					outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles)
 					callback(null, response?.compile?.status, outputFiles, clsiServerId)
 
-	deleteAuxFiles: (project_id, options, callback = (error) ->) ->
-		compilerUrl = @_getCompilerUrl(options?.compileGroup)
+	stopCompile: (project_id, user_id, options, callback = (error) ->) ->
+		compilerUrl = @_getCompilerUrl(options?.compileGroup, project_id, user_id, "compile/stop")
 		opts =
-			url:"#{compilerUrl}/project/#{project_id}"
-			method:"DELETE"
+			url:compilerUrl
+			method:"POST"
 		ClsiManager._makeRequest project_id, opts, callback
 
+	deleteAuxFiles: (project_id, user_id, options, callback = (error) ->) ->
+		compilerUrl = @_getCompilerUrl(options?.compileGroup, project_id, user_id)
+		opts =
+			url:compilerUrl
+			method:"DELETE"
+		ClsiManager._makeRequest project_id, opts, callback
 
 	_makeRequest: (project_id, opts, callback)->
 		ClsiCookieManager.getCookieJar project_id, (err, jar)->
@@ -51,14 +66,17 @@ module.exports = ClsiManager =
 						
 					return callback err, response, body
 
+	_getCompilerUrl: (compileGroup, project_id, user_id, action) ->
+		host = Settings.apis.clsi.url
+		path = "/project/#{project_id}"
+		path += "/user/#{user_id}" if user_id?
+		path += "/#{action}" if action?
+		return "#{host}#{path}"
 
-	_getCompilerUrl: (compileGroup) ->
-		return Settings.apis.clsi.url
-
-	_postToClsi: (project_id, req, compileGroup, callback = (error, response) ->) ->
-		compilerUrl = Settings.apis.clsi.url
-		opts = 
-			url:  "#{compilerUrl}/project/#{project_id}/compile"
+	_postToClsi: (project_id, user_id, req, compileGroup, callback = (error, response) ->) ->
+		compileUrl = @_getCompilerUrl(compileGroup, project_id, user_id, "compile")
+		opts =
+			url:  compileUrl
 			json: req
 			method: "POST"
 		ClsiManager._makeRequest project_id, opts, (error, response, body) ->
@@ -75,10 +93,9 @@ module.exports = ClsiManager =
 	_parseOutputFiles: (project_id, rawOutputFiles = []) ->
 		outputFiles = []
 		for file in rawOutputFiles
-			path = Url.parse(file.url).path
-			path = path.replace("/project/#{project_id}/output/", "")
 			outputFiles.push
-				path: path
+				path: file.path # the clsi is now sending this to web
+				url: Url.parse(file.url).path # the location of the file on the clsi, excluding the host part
 				type: file.type
 				build: file.build
 		return outputFiles
@@ -134,15 +151,15 @@ module.exports = ClsiManager =
 							resources: resources
 					}
 
-	wordCount: (project_id, file, options, callback = (error, response) ->) ->
+	wordCount: (project_id, user_id, file, options, callback = (error, response) ->) ->
 		ClsiManager._buildRequest project_id, options, (error, req) ->
-			compilerUrl = ClsiManager._getCompilerUrl(options?.compileGroup)
 			filename = file || req?.compile?.rootResourcePath
-			wordcount_url = "#{compilerUrl}/project/#{project_id}/wordcount?file=#{encodeURIComponent(filename)}"
-			if req.compile.options.imageName?
-				wordcount_url += "&image=#{encodeURIComponent(req.compile.options.imageName)}"
+			wordcount_url = ClsiManager._getCompilerUrl(options?.compileGroup, project_id, user_id, "wordcount")
 			opts =
 				url: wordcount_url
+				qs:
+					file: filename
+					image: req.compile.options.imageName
 				method: "GET"
 			ClsiManager._makeRequest project_id, opts, (error, response, body) ->
 				return callback(error) if error?

+ 82 - 22
services/web/app/coffee/Features/Compile/CompileController.coffee

@@ -29,8 +29,8 @@ module.exports = CompileController =
 				options.compiler = req.body.compiler
 			if req.body?.draft
 				options.draft = req.body.draft
-			logger.log {options, project_id}, "got compile request"
-			CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits) ->
+			logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
+			CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
 				return next(error) if error?
 				res.contentType("application/json")
 				res.status(200).send JSON.stringify {
@@ -38,8 +38,32 @@ module.exports = CompileController =
 					outputFiles: outputFiles
 					compileGroup: limits?.compileGroup
 					clsiServerId:clsiServerId
+					validationProblems:validationProblems
 				}
 
+	stopCompile: (req, res, next = (error) ->) ->
+		project_id = req.params.Project_id
+		AuthenticationController.getLoggedInUserId req, (error, user_id) ->
+			return next(error) if error?
+			logger.log {project_id:project_id, user_id:user_id}, "stop compile request"
+			CompileManager.stopCompile project_id, user_id, (error) ->
+				return next(error) if error?
+				res.status(200).send()
+
+	_compileAsUser: (req, callback) ->
+		# callback with user_id if per-user, undefined otherwise
+		if not Settings.disablePerUserCompiles
+			AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
+		else
+			callback() # do a per-project compile, not per-user
+
+	_downloadAsUser: (req, callback) ->
+		# callback with user_id if per-user, undefined otherwise
+		if not Settings.disablePerUserCompiles
+			AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
+		else
+			callback() # do a per-project compile, not per-user
+
 	downloadPdf: (req, res, next = (error) ->)->
 		Metrics.inc "pdf-downloads"
 		project_id = req.params.Project_id
@@ -72,16 +96,22 @@ module.exports = CompileController =
 					logger.log project_id:project_id, ip:req.ip, "rate limit hit downloading pdf"
 					res.send 500
 				else
-					CompileController.proxyToClsi(project_id, "/project/#{project_id}/output/output.pdf", req, res, next)
+					CompileController._downloadAsUser req, (error, user_id) ->
+						url = CompileController._getFileUrl project_id, user_id, req.params.build_id, "output.pdf"
+						CompileController.proxyToClsi(project_id, url, req, res, next)
 
 	deleteAuxFiles: (req, res, next) ->
 		project_id = req.params.Project_id
-		CompileManager.deleteAuxFiles project_id, (error) ->
+		CompileController._compileAsUser req, (error, user_id) ->
 			return next(error) if error?
-			res.sendStatus(200)
+			CompileManager.deleteAuxFiles project_id, user_id, (error) ->
+				return next(error) if error?
+				res.sendStatus(200)
 
+	# this is only used by templates, so is not called with a user_id
 	compileAndDownloadPdf: (req, res, next)->
 		project_id = req.params.project_id
+		# pass user_id as null, since templates are an "anonymous" compile
 		CompileManager.compile project_id, null, {}, (err)->
 			if err?
 				logger.err err:err, project_id:project_id, "something went wrong compile and downloading pdf"
@@ -91,12 +121,28 @@ module.exports = CompileController =
 
 	getFileFromClsi: (req, res, next = (error) ->) ->
 		project_id = req.params.Project_id
-		build = req.params.build
-		if build?
-			url = "/project/#{project_id}/build/#{build}/output/#{req.params.file}"
+		CompileController._downloadAsUser req, (error, user_id) ->
+			return next(error) if error?
+			url = CompileController._getFileUrl project_id, user_id, req.params.build_id, req.params.file
+			CompileController.proxyToClsi(project_id, url, req, res, next)
+
+	# compute a GET file url for a given project, user (optional), build (optional) and file
+	_getFileUrl: (project_id, user_id, build_id, file) ->
+		if user_id? and build_id?
+			url = "/project/#{project_id}/user/#{user_id}/build/#{build_id}/output/#{file}"
+		else if user_id?
+			url = "/project/#{project_id}/user/#{user_id}/output/#{file}"
+		else if build_id?
+			url = "/project/#{project_id}/build/#{build_id}/output/#{file}"
 		else
-			url = "/project/#{project_id}/output/#{req.params.file}"
-		CompileController.proxyToClsi(project_id, url, req, res, next)
+			url = "/project/#{project_id}/output/#{file}"
+		return url
+
+	# compute a POST url for a project, user (optional) and action
+	_getUrl: (project_id, user_id, action) ->
+		path = "/project/#{project_id}"
+		path += "/user/#{user_id}" if user_id?
+		return "#{path}/#{action}"
 
 	proxySyncPdf: (req, res, next = (error) ->) ->
 		project_id = req.params.Project_id
@@ -107,20 +153,35 @@ module.exports = CompileController =
 			return next(new Error("invalid h parameter"))
 		if not v?.match(/^\d+\.\d+$/)
 			return next(new Error("invalid v parameter"))
-		destination = {url: "/project/#{project_id}/sync/pdf", qs: {page, h, v}}
-		CompileController.proxyToClsi(project_id, destination, req, res, next)
+		# whether this request is going to a per-user container
+		CompileController._compileAsUser req, (error, user_id) ->
+			return next(error) if error?
+			url = CompileController._getUrl(project_id, user_id, "sync/pdf")
+			destination = {url: url, qs: {page, h, v}}
+			CompileController.proxyToClsi(project_id, destination, req, res, next)
 
 	proxySyncCode: (req, res, next = (error) ->) ->
 		project_id = req.params.Project_id
 		{file, line, column} = req.query
-		if not file? or Path.resolve("/", file) isnt "/#{file}"
+		if not file?
+			return next(new Error("missing file parameter"))
+		# Check that we are dealing with a simple file path (this is not
+		# strictly needed because synctex uses this parameter as a label
+		# to look up in the synctex output, and does not open the file
+		# itself).  Since we have valid synctex paths like foo/./bar we
+		# allow those by replacing /./ with /
+		testPath = file.replace '/./', '/'
+		if Path.resolve("/", testPath) isnt "/#{testPath}"
 			return next(new Error("invalid file parameter"))
 		if not line?.match(/^\d+$/)
 			return next(new Error("invalid line parameter"))
 		if not column?.match(/^\d+$/)
 			return next(new Error("invalid column parameter"))
-		destination = {url:"/project/#{project_id}/sync/code", qs: {file, line, column}}
-		CompileController.proxyToClsi(project_id, destination, req, res, next)
+		CompileController._compileAsUser req, (error, user_id) ->
+			return next(error) if error?
+			url = CompileController._getUrl(project_id, user_id, "sync/code")
+			destination = {url:url, qs: {file, line, column}}
+			CompileController.proxyToClsi(project_id, destination, req, res, next)
 
 	proxyToClsi: (project_id, url, req, res, next = (error) ->) ->
 		if req.query?.compileGroup
@@ -138,10 +199,7 @@ module.exports = CompileController =
 			# expand any url parameter passed in as {url:..., qs:...}
 			if typeof url is "object"
 				{url, qs} = url
-			if limits.compileGroup == "priority"
-				compilerUrl = Settings.apis.clsi_priority.url
-			else
-				compilerUrl = Settings.apis.clsi.url
+			compilerUrl = Settings.apis.clsi.url
 			url = "#{compilerUrl}#{url}"
 			logger.log url: url, "proxying to CLSI"
 			oneMinute = 60 * 1000
@@ -168,7 +226,9 @@ module.exports = CompileController =
 	wordCount: (req, res, next) ->
 		project_id = req.params.Project_id
 		file   = req.query.file || false
-		CompileManager.wordCount project_id, file, (error, body) ->
+		CompileController._compileAsUser req, (error, user_id) ->
 			return next(error) if error?
-			res.contentType("application/json")
-			res.send body
+			CompileManager.wordCount project_id, user_id, file, (error, body) ->
+				return next(error) if error?
+				res.contentType("application/json")
+				res.send body

+ 14 - 6
services/web/app/coffee/Features/Compile/CompileManager.coffee

@@ -37,15 +37,23 @@ module.exports = CompileManager =
 							return callback(error) if error?
 							for key, value of limits
 								options[key] = value
-							ClsiManager.sendRequest project_id, options, (error, status, outputFiles, clsiServerId) ->
+							# only pass user_id down to clsi if this is a per-user compile
+							compileAsUser = if Settings.disablePerUserCompiles then undefined else user_id
+							ClsiManager.sendRequest project_id, compileAsUser, options, (error, status, outputFiles, clsiServerId, validationProblems) ->
 								return callback(error) if error?
 								logger.log files: outputFiles, "output files"
-								callback(null, status, outputFiles, clsiServerId, limits)
+								callback(null, status, outputFiles, clsiServerId, limits, validationProblems)
 								
-	deleteAuxFiles: (project_id, callback = (error) ->) ->
+
+	stopCompile: (project_id, user_id, callback = (error) ->) ->
+		CompileManager.getProjectCompileLimits project_id, (error, limits) ->
+			return callback(error) if error?
+			ClsiManager.stopCompile project_id, user_id, limits, callback
+
+	deleteAuxFiles: (project_id, user_id, callback = (error) ->) ->
 		CompileManager.getProjectCompileLimits project_id, (error, limits) ->
 			return callback(error) if error?
-			ClsiManager.deleteAuxFiles project_id, limits, callback
+			ClsiManager.deleteAuxFiles project_id, user_id, limits, callback
 
 	getProjectCompileLimits: (project_id, callback = (error, limits) ->) ->
 		Project.findById project_id, {owner_ref: 1}, (error, project) ->
@@ -92,7 +100,7 @@ module.exports = CompileManager =
 			else
 				ProjectRootDocManager.setRootDocAutomatically project_id, callback
 		
-	wordCount: (project_id, file, callback = (error) ->) ->
+	wordCount: (project_id, user_id, file, callback = (error) ->) ->
 		CompileManager.getProjectCompileLimits project_id, (error, limits) ->
 			return callback(error) if error?
-			ClsiManager.wordCount project_id, file, limits, callback
+			ClsiManager.wordCount project_id, user_id, file, limits, callback

+ 4 - 0
services/web/app/coffee/Features/Email/EmailBuilder.coffee

@@ -3,6 +3,8 @@ PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
 NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
 settings = require("settings-sharelatex")
 
+
+
 templates = {}
 
 templates.registered =	
@@ -114,6 +116,8 @@ module.exports =
 		template = templates[templateName]
 		opts.siteUrl = settings.siteUrl
 		opts.body = template.compiledTemplate(opts)
+		if settings.email?.templates?.customFooter?
+			opts.body += settings.email?.templates?.customFooter
 		return {
 			subject : template.subject(opts)
 			html: template.layout(opts)

+ 2 - 2
services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee

@@ -1,15 +1,15 @@
-
+logger = require("logger-sharelatex")
 NotificationsHandler = require("./NotificationsHandler")
 
 module.exports = 
 
 	groupPlan: (user, licence)->
 		key : "join-sub-#{licence.subscription_id}"
-
 		create: (callback = ->)->
 			messageOpts = 
 				groupName: licence.name
 				subscription_id: licence.subscription_id
+			logger.log user_id:user._id, key:key, "creating notification key for user"
 			NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, callback
 
 		read: (callback = ->)->

+ 19 - 8
services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee

@@ -3,14 +3,22 @@ request = require("request")
 logger = require("logger-sharelatex")
 
 oneSecond = 1000
+
+makeRequest = (opts, callback)->
+	if !settings.apis.notifications?.url?
+		return callback(null, statusCode:200)
+	else
+		request(opts, callback)
+
 module.exports = 
 
 	getUserNotifications: (user_id, callback)->
 		opts = 
-			uri: "#{settings.apis.notifications.url}/user/#{user_id}"
+			uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
 			json: true
 			timeout: oneSecond
-		request.get opts, (err, res, unreadNotifications)->
+			method: "GET"
+		makeRequest opts, (err, res, unreadNotifications)->
 			statusCode =  if res? then res.statusCode else 500
 			if err? or statusCode != 200
 				e = new Error("something went wrong getting notifications, #{err}, #{statusCode}")
@@ -23,30 +31,33 @@ module.exports =
 
 	createNotification: (user_id, key, templateKey, messageOpts, callback)->
 		opts = 
-			uri: "#{settings.apis.notifications.url}/user/#{user_id}"
+			uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
 			timeout: oneSecond
+			method:"POST"
 			json: {
 				key:key
 				messageOpts:messageOpts
 				templateKey:templateKey
 			}
 		logger.log opts:opts, "creating notification for user"
-		request.post opts, callback
+		makeRequest opts, callback
 
 	markAsReadWithKey: (user_id, key, callback)->
 		opts = 
-			uri: "#{settings.apis.notifications.url}/user/#{user_id}"
+			uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
+			method: "DELETE"
 			timeout: oneSecond
 			json: {
 				key:key
 			}
 		logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api"
-		request.del opts, callback
+		makeRequest opts, callback
 	
 
 	markAsRead: (user_id, notification_id, callback)->
 		opts =
-			uri: "#{settings.apis.notifications.url}/user/#{user_id}/notification/#{notification_id}"
+			method: "DELETE"
+			uri: "#{settings.apis.notifications?.url}/user/#{user_id}/notification/#{notification_id}"
 			timeout:oneSecond
 		logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api"
-		request.del opts, callback
+		makeRequest opts, callback

+ 9 - 6
services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee

@@ -2,6 +2,7 @@ PasswordResetHandler = require("./PasswordResetHandler")
 RateLimiter = require("../../infrastructure/RateLimiter")
 AuthenticationController = require("../Authentication/AuthenticationController")
 UserGetter = require("../User/UserGetter")
+UserSessionsManager = require("../User/UserSessionsManager")
 logger = require "logger-sharelatex"
 
 module.exports =
@@ -47,11 +48,13 @@ module.exports =
 		PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found, user_id) ->
 			return next(err) if err?
 			if found
-				if req.body.login_after
-					UserGetter.getUser user_id, {email: 1}, (err, user) ->
-						return next(err) if err?
-						AuthenticationController.doLogin {email:user.email, password: password}, req, res, next
-				else
-					res.sendStatus 200
+				UserSessionsManager.revokeAllUserSessions {_id: user_id}, [], (err) ->
+					return next(err) if err?
+					if req.body.login_after
+						UserGetter.getUser user_id, {email: 1}, (err, user) ->
+							return next(err) if err?
+							AuthenticationController.doLogin {email:user.email, password: password}, req, res, next
+					else
+						res.sendStatus 200
 			else
 				res.sendStatus 404

+ 2 - 0
services/web/app/coffee/Features/Project/ProjectController.coffee

@@ -245,11 +245,13 @@ module.exports = ProjectController =
 						first_name : user.first_name
 						last_name  : user.last_name
 						referal_id : user.referal_id
+						signUpDate : user.signUpDate
 						subscription :
 							freeTrial: {allowed: allowedFreeTrial}
 						featureSwitches: user.featureSwitches
 						features: user.features
 						refProviders: user.refProviders
+						betaProgram: user.betaProgram
 					}
 					userSettings: {
 						mode  : user.ace.mode

+ 5 - 3
services/web/app/coffee/Features/Project/ProjectDeleter.coffee

@@ -24,9 +24,11 @@ module.exports = ProjectDeleter =
 		update = {deletedByExternalDataSource: false}
 		Project.update conditions, update, {}, callback
 
-	deleteUsersProjects: (owner_id, callback)->
-		logger.log owner_id:owner_id, "deleting users projects"
-		Project.remove owner_ref:owner_id, callback
+	deleteUsersProjects: (user_id, callback)->
+		logger.log {user_id}, "deleting users projects"
+		Project.remove owner_ref:user_id, (error) ->
+			return callback(error) if error?
+			CollaboratorsHandler.removeUserFromAllProjets user_id, callback
 
 	deleteProject: (project_id, callback = (error) ->) ->
 		# archiveProject takes care of the clean-up

+ 5 - 0
services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee

@@ -512,6 +512,11 @@ module.exports = ProjectEntityHandler =
 			return callback(e)
 		type = sanitizeTypeOfElement type
 
+		if path.resolve("/", element.name) isnt "/#{element.name}" or element.name.match("/")
+			e = new Error("invalid element name")
+			logger.err project_id:project._id, folder_id:folder_id, element:element, type:type, "failed trying to insert element as name was invalid"
+			return callback(e)
+
 		if !folder_id?
 			folder_id = project.rootFolder[0]._id
 		ProjectEntityHandler._countElements project, (err, count)->

+ 4 - 2
services/web/app/coffee/Features/References/ReferencesController.coffee

@@ -15,7 +15,7 @@ module.exports = ReferencesController =
 			return res.sendStatus 400
 		logger.log {projectId, docIds: docIds}, "index references for project"
 		ReferencesHandler.index projectId, docIds, (err, data) ->
-			if err
+			if err?
 				logger.err {err, projectId}, "error indexing all references"
 				return res.sendStatus 500
 			ReferencesController._handleIndexResponse(req, res, projectId, shouldBroadcast, data)
@@ -25,12 +25,14 @@ module.exports = ReferencesController =
 		shouldBroadcast = req.body.shouldBroadcast
 		logger.log {projectId}, "index all references for project"
 		ReferencesHandler.indexAll projectId, (err, data) ->
-			if err
+			if err?
 				logger.err {err, projectId}, "error indexing all references"
 				return res.sendStatus 500
 			ReferencesController._handleIndexResponse(req, res, projectId, shouldBroadcast, data)
 
 	_handleIndexResponse: (req, res, projectId, shouldBroadcast, data) ->
+		if !data? or !data.keys?
+			return res.json({projectId, keys: []})
 		if shouldBroadcast
 			logger.log {projectId}, "emitting new references keys to connected clients"
 			EditorRealTimeController.emitToRoom projectId, 'references:keys:updated', data.keys

+ 2 - 0
services/web/app/coffee/Features/References/ReferencesHandler.coffee

@@ -66,6 +66,8 @@ module.exports = ReferencesHandler =
 			ReferencesHandler._doIndexOperation(projectId, project, docIds, [], callback)
 
 	_doIndexOperation: (projectId, project, docIds, fileIds, callback) ->
+		if !settings.apis?.references?.url?
+			return callback()
 		ReferencesHandler._isFullIndex project, (err, isFullIndex) ->
 			if err
 				logger.err {err, projectId}, "error checking whether to do full index"

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

@@ -5,6 +5,12 @@ _ = require("underscore")
 ErrorController = require "../Errors/ErrorController"
 StaticPageHelpers = require("./StaticPageHelpers")
 sanitize = require('sanitizer')
+Settings = require("settings-sharelatex")
+contentful = require('contentful')
+marked = require("marked")
+
+
+
 
 module.exports = UniversityController = 
 
@@ -17,7 +23,7 @@ module.exports = UniversityController =
 		logger.log url:url, "proxying request to university api"
 		request.get universityUrl, (err, r, data)->
 			if r?.statusCode == 404
-				return ErrorController.notFound(req, res, next)
+				return UniversityController.getContentfulPage(req, res, next)
 			if err?
 				return res.send 500
 			data = data.trim()
@@ -37,4 +43,28 @@ module.exports = UniversityController =
 		upstream = request.get(originUrl)
 		upstream.on "error", (error) ->
 			logger.error err: error, "university proxy error"
-		upstream.pipe res
+		upstream.pipe res
+
+	getContentfulPage: (req, res, next)->
+		console.log Settings.contentful
+		if !Settings.contentful?.uni?.space? and !Settings.contentful?.uni?.accessToken?
+			return ErrorController.notFound(req, res, next)
+
+		client = contentful.createClient({
+			space: Settings.contentful?.uni?.space
+			accessToken: Settings.contentful?.uni?.accessToken
+		})
+
+		url = req.url?.toLowerCase().replace("/university/","")
+		client.getEntries({content_type: 'caseStudy', 'fields.slug':url})
+			.catch (e)->
+				return res.send 500
+			.then (entry)->
+				if !entry? or !entry.items? or entry.items.length == 0
+					return ErrorController.notFound(req, res, next)
+				viewData = entry.items[0].fields
+				viewData.html = marked(viewData.content)
+				res.render "university/case_study", viewData:viewData
+
+
+

+ 2 - 2
services/web/app/coffee/Features/Subscription/LimitationsManager.coffee

@@ -38,12 +38,12 @@ module.exports =
 		SubscriptionLocator.getUsersSubscription user._id, (err, subscription)->
 			if err?
 				return callback(err)
-			hasValidSubscription = subscription? and subscription.recurlySubscription_id?
+			hasValidSubscription = subscription? and (subscription.recurlySubscription_id? or subscription?.customAccount == true)
 			logger.log user:user, hasValidSubscription:hasValidSubscription, subscription:subscription, "checking if user has subscription"
 			callback err, hasValidSubscription, subscription
 			
 	userIsMemberOfGroupSubscription: (user, callback = (error, isMember, subscriptions) ->) ->
-		logger.log user_id: user._ud, "checking is user is member of subscription groups"
+		logger.log user_id: user._id, "checking is user is member of subscription groups"
 		SubscriptionLocator.getMemberSubscriptions user._id, (err, subscriptions = []) ->
 			return callback(err) if err?
 			callback err, subscriptions.length > 0, subscriptions

+ 236 - 34
services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee

@@ -4,11 +4,201 @@ request = require 'request'
 Settings = require "settings-sharelatex"
 xml2js = require "xml2js"
 logger = require("logger-sharelatex")
+Async = require('async')
 
 module.exports = RecurlyWrapper =
 	apiUrl : "https://api.recurly.com/v2"
 
-	createSubscription: (user, subscriptionDetails, recurly_token_id, callback)->
+	_addressToXml: (address) ->
+		allowedKeys = ['address1', 'address2', 'city', 'country', 'state', 'zip', 'postal_code']
+		resultString = "<billing_info>\n"
+		for k, v of address
+			if k == 'postal_code'
+				k = 'zip'
+			if v and (k in allowedKeys)
+				resultString += "<#{k}#{if k == 'address2' then ' nil="nil"' else ''}>#{v || ''}</#{k}>\n"
+		resultString += "</billing_info>\n"
+		return resultString
+
+	_paypal:
+		checkAccountExists: (cache, next) ->
+			user = cache.user
+			recurly_token_id = cache.recurly_token_id
+			subscriptionDetails = cache.subscriptionDetails
+			logger.log {user_id: user._id, recurly_token_id}, "checking if recurly account exists for user"
+			RecurlyWrapper.apiRequest({
+				url:    "accounts/#{user._id}"
+				method: "GET"
+			}, (error, response, responseBody) ->
+				if error
+					if response.statusCode == 404  # actually not an error in this case, just no existing account
+						cache.userExists = false
+						return next(null, cache)
+					logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while checking account"
+					return next(error)
+				logger.log {user_id: user._id, recurly_token_id}, "user appears to exist in recurly"
+				RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
+					if err
+						logger.error {err, user_id: user._id, recurly_token_id}, "error parsing account"
+						return next(err)
+					cache.userExists = true
+					cache.account = account
+					return next(null, cache)
+			)
+		createAccount: (cache, next) ->
+			user = cache.user
+			recurly_token_id = cache.recurly_token_id
+			subscriptionDetails = cache.subscriptionDetails
+			address = subscriptionDetails.address
+			if !address
+				return next(new Error('no address in subscriptionDetails at createAccount stage'))
+			if cache.userExists
+				logger.log {user_id: user._id, recurly_token_id}, "user already exists in recurly"
+				return next(null, cache)
+			logger.log {user_id: user._id, recurly_token_id}, "creating user in recurly"
+			requestBody = """
+			<account>
+				<account_code>#{user._id}</account_code>
+				<email>#{user.email}</email>
+				<first_name>#{user.first_name}</first_name>
+				<last_name>#{user.last_name}</last_name>
+				<address>
+					<address1>#{address.address1}</address1>
+					<address2>#{address.address2}</address2>
+					<city>#{address.city || ''}</city>
+					<state>#{address.state || ''}</state>
+					<zip>#{address.zip || ''}</zip>
+					<country>#{address.country}</country>
+				</address>
+			</account>
+			"""
+			RecurlyWrapper.apiRequest({
+				url    : "accounts"
+				method : "POST"
+				body   : requestBody
+			}, (error, response, responseBody) =>
+				if error
+					logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating account"
+					return next(error)
+				RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
+					if err
+						logger.error {err, user_id: user._id, recurly_token_id}, "error creating account"
+						return next(err)
+					cache.account = account
+					return next(null, cache)
+			)
+		createBillingInfo: (cache, next) ->
+			user = cache.user
+			recurly_token_id = cache.recurly_token_id
+			subscriptionDetails = cache.subscriptionDetails
+			logger.log {user_id: user._id, recurly_token_id}, "creating billing info in recurly"
+			accountCode = cache?.account?.account_code
+			if !accountCode
+				return next(new Error('no account code at createBillingInfo stage'))
+			requestBody = """
+			<billing_info>
+				<token_id>#{recurly_token_id}</token_id>
+			</billing_info>
+			"""
+			RecurlyWrapper.apiRequest({
+				url: "accounts/#{accountCode}/billing_info"
+				method: "POST"
+				body: requestBody
+			}, (error, response, responseBody) =>
+				if error
+					logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating billing info"
+					return next(error)
+				RecurlyWrapper._parseBillingInfoXml responseBody, (err, billingInfo) ->
+					if err
+						logger.error {err, user_id: user._id, accountCode, recurly_token_id}, "error creating billing info"
+						return next(err)
+					cache.billingInfo = billingInfo
+					return next(null, cache)
+			)
+
+		setAddress: (cache, next) ->
+			user = cache.user
+			recurly_token_id = cache.recurly_token_id
+			subscriptionDetails = cache.subscriptionDetails
+			logger.log {user_id: user._id, recurly_token_id}, "setting billing address in recurly"
+			accountCode = cache?.account?.account_code
+			if !accountCode
+				return next(new Error('no account code at setAddress stage'))
+			address = subscriptionDetails.address
+			if !address
+				return next(new Error('no address in subscriptionDetails at setAddress stage'))
+			requestBody = RecurlyWrapper._addressToXml(address)
+			RecurlyWrapper.apiRequest({
+				url: "accounts/#{accountCode}/billing_info"
+				method: "PUT"
+				body: requestBody
+			}, (error, response, responseBody) =>
+				if error
+					logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while setting address"
+					return next(error)
+				RecurlyWrapper._parseBillingInfoXml responseBody, (err, billingInfo) ->
+					if err
+						logger.error {err, user_id: user._id, recurly_token_id}, "error updating billing info"
+						return next(err)
+					cache.billingInfo = billingInfo
+					return next(null, cache)
+			)
+		createSubscription: (cache, next) ->
+			user = cache.user
+			recurly_token_id = cache.recurly_token_id
+			subscriptionDetails = cache.subscriptionDetails
+			logger.log {user_id: user._id, recurly_token_id}, "creating subscription in recurly"
+			requestBody = """
+			<subscription>
+				<plan_code>#{subscriptionDetails.plan_code}</plan_code>
+				<currency>#{subscriptionDetails.currencyCode}</currency>
+				<coupon_code>#{subscriptionDetails.coupon_code}</coupon_code>
+				<account>
+					<account_code>#{user._id}</account_code>
+				</account>
+			</subscription>
+			"""  # TODO: check account details and billing
+			RecurlyWrapper.apiRequest({
+				url    : "subscriptions"
+				method : "POST"
+				body   : requestBody
+			}, (error, response, responseBody) =>
+				if error
+					logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating subscription"
+					return next(error)
+				RecurlyWrapper._parseSubscriptionXml responseBody, (err, subscription) ->
+					if err
+						logger.error {err, user_id: user._id, recurly_token_id}, "error creating subscription"
+						return next(err)
+					cache.subscription = subscription
+					return next(null, cache)
+			)
+
+	_createPaypalSubscription: (user, subscriptionDetails, recurly_token_id, callback) ->
+		logger.log {user_id: user._id, recurly_token_id}, "starting process of creating paypal subscription"
+		# We use `async.waterfall` to run each of these actions in sequence
+		# passing a `cache` object along the way. The cache is initialized
+		# with required data, and `async.apply` to pass the cache to the first function
+		cache = {user, recurly_token_id, subscriptionDetails}
+		Async.waterfall([
+			Async.apply(RecurlyWrapper._paypal.checkAccountExists, cache),
+			RecurlyWrapper._paypal.createAccount,
+			RecurlyWrapper._paypal.createBillingInfo,
+			RecurlyWrapper._paypal.setAddress,
+			RecurlyWrapper._paypal.createSubscription,
+		], (err, result) ->
+			if err
+				logger.error {err, user_id: user._id, recurly_token_id}, "error in paypal subscription creation process"
+				return callback(err)
+			if !result.subscription
+				err = new Error('no subscription object in result')
+				logger.error {err, user_id: user._id, recurly_token_id}, "error in paypal subscription creation process"
+				return callback(err)
+			logger.log {user_id: user._id, recurly_token_id}, "done creating paypal subscription for user"
+			callback(null, result.subscription)
+		)
+
+	_createCreditCardSubscription: (user, subscriptionDetails, recurly_token_id, callback) ->
 		requestBody = """
 		          <subscription>
 		            <plan_code>#{subscriptionDetails.plan_code}</plan_code>
@@ -25,17 +215,23 @@ module.exports = RecurlyWrapper =
 		            </account>
 		          </subscription>
 		          """
-		@apiRequest({
+		RecurlyWrapper.apiRequest({
 			url    : "subscriptions"
 			method : "POST"
 			body   : requestBody
 		}, (error, response, responseBody) =>
 			return callback(error) if error?
-			@_parseSubscriptionXml responseBody, callback
-		)		
+			RecurlyWrapper._parseSubscriptionXml responseBody, callback
+		)
+
+	createSubscription: (user, subscriptionDetails, recurly_token_id, callback)->
+		isPaypal = subscriptionDetails.isPaypal
+		logger.log {user_id: user._id, isPaypal, recurly_token_id}, "setting up subscription in recurly"
+		fn = if isPaypal then RecurlyWrapper._createPaypalSubscription else RecurlyWrapper._createCreditCardSubscription
+		fn user, subscriptionDetails, recurly_token_id, callback
 
 	apiRequest : (options, callback) ->
-		options.url = @apiUrl + "/" + options.url
+		options.url = RecurlyWrapper.apiUrl + "/" + options.url
 		options.headers =
 			"Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64")
 			"Accept"        : "application/xml"
@@ -60,7 +256,7 @@ module.exports = RecurlyWrapper =
 						newAttributes[key] = value
 				else
 					newAttributes[newKey] = value
-					
+
 			return newAttributes
 
 		crypto.randomBytes 32, (error, buffer) ->
@@ -74,14 +270,14 @@ module.exports = RecurlyWrapper =
 			signature = "#{signed}|#{unsignedQuery}"
 
 			callback null, signature
-	
+
 
 	getSubscriptions: (accountId, callback)->
-		@apiRequest({
+		RecurlyWrapper.apiRequest({
 			url: "accounts/#{accountId}/subscriptions"
 		}, (error, response, body) =>
 			return callback(error) if error?
-			@_parseXml body, callback
+			RecurlyWrapper._parseXml body, callback
 		)
 
 
@@ -94,11 +290,11 @@ module.exports = RecurlyWrapper =
 		else
 			url = "subscriptions/#{subscriptionId}"
 
-		@apiRequest({
+		RecurlyWrapper.apiRequest({
 			url: url
 		}, (error, response, body) =>
 			return callback(error) if error?
-			@_parseSubscriptionXml body, (error, recurlySubscription) =>
+			RecurlyWrapper._parseSubscriptionXml body, (error, recurlySubscription) =>
 				return callback(error) if error?
 				if options.includeAccount
 					if recurlySubscription.account? and recurlySubscription.account.url?
@@ -106,7 +302,7 @@ module.exports = RecurlyWrapper =
 					else
 						return callback "I don't understand the response from Recurly"
 
-					@getAccount accountId, (error, account) ->
+					RecurlyWrapper.getAccount accountId, (error, account) ->
 						return callback(error) if error?
 						recurlySubscription.account = account
 						callback null, recurlySubscription
@@ -124,9 +320,9 @@ module.exports = RecurlyWrapper =
 					per_page:200
 			if cursor?
 				opts.qs.cursor = cursor
-			@apiRequest opts, (error, response, body) =>
+			RecurlyWrapper.apiRequest opts, (error, response, body) =>
 				return callback(error) if error?
-				@_parseXml body, (err, data)->
+				RecurlyWrapper._parseXml body, (err, data)->
 					if err?
 						logger.err err:err, "could not get accoutns"
 						callback(err)
@@ -142,19 +338,19 @@ module.exports = RecurlyWrapper =
 
 
 	getAccount: (accountId, callback) ->
-		@apiRequest({
+		RecurlyWrapper.apiRequest({
 			url: "accounts/#{accountId}"
 		}, (error, response, body) =>
 			return callback(error) if error?
-			@_parseAccountXml body, callback
+			RecurlyWrapper._parseAccountXml body, callback
 		)
 
 	getBillingInfo: (accountId, callback)->
-		@apiRequest({
+		RecurlyWrapper.apiRequest({
 			url: "accounts/#{accountId}/billing_info"
 		}, (error, response, body) =>
 			return callback(error) if error?
-			@_parseXml body, callback
+			RecurlyWrapper._parseXml body, callback
 		)
 
 
@@ -166,13 +362,13 @@ module.exports = RecurlyWrapper =
 		            <timeframe>#{options.timeframe}</timeframe>
 		          </subscription>
 		          """
-		@apiRequest({
+		RecurlyWrapper.apiRequest({
 			url    : "subscriptions/#{subscriptionId}"
 			method : "put"
 			body   : requestBody
 		}, (error, response, responseBody) =>
 			return callback(error) if error?
-			@_parseSubscriptionXml responseBody, callback
+			RecurlyWrapper._parseSubscriptionXml responseBody, callback
 		)
 
 	createFixedAmmountCoupon: (coupon_code, name, currencyCode, discount_in_cents, plan_code, callback)->
@@ -191,7 +387,7 @@ module.exports = RecurlyWrapper =
 			</coupon>
 		"""
 		logger.log coupon_code:coupon_code, requestBody:requestBody, "creating coupon"
-		@apiRequest({
+		RecurlyWrapper.apiRequest({
 			url    : "coupons"
 			method : "post"
 			body   : requestBody
@@ -203,16 +399,16 @@ module.exports = RecurlyWrapper =
 
 
 	lookupCoupon: (coupon_code, callback)->
-		@apiRequest({
+		RecurlyWrapper.apiRequest({
 			url: "coupons/#{coupon_code}"
 		}, (error, response, body) =>
 			return callback(error) if error?
-			@_parseXml body, callback
+			RecurlyWrapper._parseXml body, callback
 		)
 
 	cancelSubscription: (subscriptionId, callback) ->
 		logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription"
-		@apiRequest({
+		RecurlyWrapper.apiRequest({
 			url: "subscriptions/#{subscriptionId}/cancel",
 			method: "put"
 		}, (error, response, body) ->
@@ -221,13 +417,13 @@ module.exports = RecurlyWrapper =
 
 	reactivateSubscription: (subscriptionId, callback) ->
 		logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription"
-		@apiRequest({
+		RecurlyWrapper.apiRequest({
 			url: "subscriptions/#{subscriptionId}/reactivate",
 			method: "put"
 		}, (error, response, body) ->
 			callback(error)
 		)
-		
+
 
 	redeemCoupon: (account_code, coupon_code, callback)->
 		requestBody = """
@@ -237,7 +433,7 @@ module.exports = RecurlyWrapper =
 			</redemption>
 		"""
 		logger.log account_code:account_code, coupon_code:coupon_code, requestBody:requestBody, "redeeming coupon for user"
-		@apiRequest({
+		RecurlyWrapper.apiRequest({
 			url    : "coupons/#{coupon_code}/redeem"
 			method : "post"
 			body   : requestBody
@@ -251,7 +447,7 @@ module.exports = RecurlyWrapper =
 		next_renewal_date = new Date()
 		next_renewal_date.setDate(next_renewal_date.getDate() + daysUntilExpire)
 		logger.log subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "Exending Free trial for user"
-		@apiRequest({
+		RecurlyWrapper.apiRequest({
 			url    : "/subscriptions/#{subscriptionId}/postpone?next_renewal_date=#{next_renewal_date}&bulk=false"
 			method : "put"
 		}, (error, response, responseBody) =>
@@ -261,7 +457,7 @@ module.exports = RecurlyWrapper =
 		)
 
 	_parseSubscriptionXml: (xml, callback) ->
-		@_parseXml xml, (error, data) ->
+		RecurlyWrapper._parseXml xml, (error, data) ->
 			return callback(error) if error?
 			if data? and data.subscription?
 				recurlySubscription = data.subscription
@@ -270,7 +466,7 @@ module.exports = RecurlyWrapper =
 			callback null, recurlySubscription
 
 	_parseAccountXml: (xml, callback) ->
-		@_parseXml xml, (error, data) ->
+		RecurlyWrapper._parseXml xml, (error, data) ->
 			return callback(error) if error?
 			if data? and data.account?
 				account = data.account
@@ -278,6 +474,15 @@ module.exports = RecurlyWrapper =
 				return callback "I don't understand the response from Recurly"
 			callback null, account
 
+	_parseBillingInfoXml: (xml, callback) ->
+		RecurlyWrapper._parseXml xml, (error, data) ->
+			return callback(error) if error?
+			if data? and data.billing_info?
+				billingInfo = data.billing_info
+			else
+				return callback "I don't understand the response from Recurly"
+			callback null, billingInfo
+
 	_parseXml: (xml, callback) ->
 		convertDataTypes = (data) ->
 			if data? and data["$"]?
@@ -299,7 +504,7 @@ module.exports = RecurlyWrapper =
 						else
 							array.push(convertDataTypes(value))
 					data = array
-			
+
 			if data instanceof Array
 				data = (convertDataTypes(entry) for entry in data)
 			else if typeof data == "object"
@@ -315,6 +520,3 @@ module.exports = RecurlyWrapper =
 			return callback(error) if error?
 			result = convertDataTypes(data)
 			callback null, result
-
-			
-

+ 4 - 3
services/web/app/coffee/Features/Subscription/SubscriptionController.coffee

@@ -82,9 +82,9 @@ module.exports = SubscriptionController =
 					logger.log user: user, "redirecting to plans"
 					res.redirect "/user/subscription/plans"
 				else
-					SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groups) ->
+					SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
 						return next(error) if error?
-						logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groups:groups, "showing subscription dashboard"
+						logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard"
 						plans = SubscriptionViewModelBuilder.buildViewModel()
 						res.render "subscriptions/dashboard",
 							title: "your_subscription"
@@ -92,8 +92,9 @@ module.exports = SubscriptionController =
 							taxRate:subscription?.taxRate
 							plans: plans
 							subscription: subscription || {}
-							groups: groups
+							groupSubscriptions: groupSubscriptions
 							subscriptionTabActive: true
+							user:user
 
 
 	userCustomSubscriptionPage: (req, res, next)->

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

@@ -8,7 +8,7 @@ _ = require("underscore")
 
 module.exports =
 
-	buildUsersSubscriptionViewModel: (user, callback = (error, subscription, groups) ->) ->
+	buildUsersSubscriptionViewModel: (user, callback = (error, subscription, memberSubscriptions) ->) ->
 		SubscriptionLocator.getUsersSubscription user, (err, subscription) ->
 			return callback(err) if err?
 			SubscriptionLocator.getMemberSubscriptions user, (err, memberSubscriptions = []) ->
@@ -19,6 +19,7 @@ module.exports =
 					RecurlyWrapper.getSubscription subscription.recurlySubscription_id, (err, recurlySubscription)->
 						tax = recurlySubscription?.tax_in_cents || 0
 						callback null, {
+							admin_id:subscription.admin_id
 							name: plan.name
 							nextPaymentDueAt: SubscriptionFormatters.formatDate(recurlySubscription?.current_period_ends_at)
 							state: recurlySubscription?.state

+ 11 - 8
services/web/app/coffee/Features/User/UserController.coffee

@@ -8,6 +8,7 @@ logger = require("logger-sharelatex")
 metrics = require("../../infrastructure/Metrics")
 Url = require("url")
 AuthenticationManager = require("../Authentication/AuthenticationManager")
+UserSessionsManager = require("./UserSessionsManager")
 UserUpdater = require("./UserUpdater")
 settings = require "settings-sharelatex"
 
@@ -72,6 +73,7 @@ module.exports = UserController =
 							if err?
 								logger.err err:err, user_id:user_id, "error getting user for email update"
 								return res.send 500
+							req.session.user.email = user.email
 							UserHandler.populateGroupLicenceInvite user, (err)-> #need to refresh this in the background
 								if err?
 									logger.err err:err, "error populateGroupLicenceInvite"
@@ -80,9 +82,12 @@ module.exports = UserController =
 	logout : (req, res)->
 		metrics.inc "user.logout"
 		logger.log user: req?.session?.user, "logging out"
+		sessionId = req.sessionID
+		user = req?.session?.user
 		req.session.destroy (err)->
 			if err
 				logger.err err: err, 'error destorying session'
+			UserSessionsManager.untrackSession(user, sessionId)
 			res.redirect '/login'
 
 	register : (req, res, next = (error) ->)->
@@ -116,17 +121,15 @@ module.exports = UserController =
 					logger.log user: user, "password changed"
 					AuthenticationManager.setUserPassword user._id, newPassword1, (error) ->
 						return next(error) if error?
-						res.send
-							message:
-							  type:'success'
-							  text:'Your password has been changed'
+						UserSessionsManager.revokeAllUserSessions user, [req.sessionID], (err) ->
+							return next(err) if err?
+							res.send
+								message:
+									type:'success'
+									text:'Your password has been changed'
 			else
 				logger.log user: user, "current password wrong"
 				res.send
 					message:
 					  type:'error'
 					  text:'Your old password is wrong'
-
-	changeEmailAddress: (req, res)->
-
-

+ 10 - 2
services/web/app/coffee/Features/User/UserCreator.coffee

@@ -1,5 +1,6 @@
 User = require("../../models/User").User
 UserLocator = require("./UserLocator")
+logger = require("logger-sharelatex")
 
 module.exports =
 
@@ -12,16 +13,23 @@ module.exports =
 				self.createNewUser email:email, holdingAccount:true, callback
 
 	createNewUser: (opts, callback)->
+		logger.log opts:opts, "creating new user"
 		user = new User()
 		user.email = opts.email
 		user.holdingAccount = opts.holdingAccount
 
 		username = opts.email.match(/^[^@]*/)
-		if username?
+		if opts.first_name? and opts.first_name.length != 0
+			user.first_name = opts.first_name
+		else if username?
 			user.first_name = username[0]
 		else
 			user.first_name = ""
-		user.last_name = ""
+
+		if opts.last_name?
+			user.last_name = opts.last_name
+		else
+			user.last_name = ""
 
 		user.featureSwitches?.pdfng = true
 

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

@@ -6,7 +6,7 @@ logger = require("logger-sharelatex")
 
 module.exports = UserHandler =
 
-	populateGroupLicenceInvite: (user, callback)->
+	populateGroupLicenceInvite: (user, callback = ->)->
 		logger.log user_id:user._id, "populating any potential group licence invites"
 		licence = SubscriptionDomainHandler.getLicenceUserCanJoin user
 		if !licence?

+ 2 - 0
services/web/app/coffee/Features/User/UserPagesController.coffee

@@ -26,6 +26,7 @@ module.exports =
 	activateAccountPage: (req, res) ->
 		# An 'activation' is actually just a password reset on an account that
 		# was set with a random password originally.
+		logger.log query:req.query, "activiate account page called"
 		if !req.query?.user_id? or !req.query?.token?
 			return ErrorController.notFound(req, res)
 			
@@ -34,6 +35,7 @@ module.exports =
 			if !user
 				return ErrorController.notFound(req, res)
 			if user.loginCount > 0
+				logger.log user:user, "user has already logged in so is active, sending them to /login"
 				# Already seen this user, so account must be activate
 				# This lets users keep clicking the 'activate' link in their email
 				# as a way to log in which, if I know our users, they will.

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

@@ -35,7 +35,8 @@ module.exports = UserRegistrationHandler =
 
 	_createNewUserIfRequired: (user, userDetails, callback)->
 		if !user?
-			UserCreator.createNewUser {holdingAccount:false, email:userDetails.email}, callback
+			userDetails.holdingAccount = false
+			UserCreator.createNewUser {holdingAccount:false, email:userDetails.email, first_name:userDetails.first_name, last_name:userDetails.last_name}, callback
 		else
 			callback null, user
 

+ 126 - 0
services/web/app/coffee/Features/User/UserSessionsManager.coffee

@@ -0,0 +1,126 @@
+Settings = require('settings-sharelatex')
+redis = require('redis-sharelatex')
+logger = require("logger-sharelatex")
+Async = require('async')
+_ = require('underscore')
+
+rclient = redis.createClient(Settings.redis.web)
+
+module.exports = UserSessionsManager =
+
+	_sessionSetKey: (user) ->
+		return "UserSessions:#{user._id}"
+
+	# mimic the key used by the express sessions
+	_sessionKey: (sessionId) ->
+		return "sess:#{sessionId}"
+
+	trackSession: (user, sessionId, callback=(err)-> ) ->
+		if !user?
+			logger.log {sessionId}, "no user to track, returning"
+			return callback(null)
+		if !sessionId?
+			logger.log {user_id: user._id}, "no sessionId to track, returning"
+			return callback(null)
+		logger.log {user_id: user._id, sessionId}, "onLogin handler"
+		sessionSetKey = UserSessionsManager._sessionSetKey(user)
+		value = UserSessionsManager._sessionKey sessionId
+		rclient.multi()
+			.sadd(sessionSetKey, value)
+			.expire(sessionSetKey, "#{Settings.cookieSessionLength}")
+			.exec (err, response) ->
+				if err?
+					logger.err {err, user_id: user._id, sessionSetKey}, "error while adding session key to UserSessions set"
+					return callback(err)
+				UserSessionsManager._checkSessions(user, () ->)
+				callback()
+
+	untrackSession: (user, sessionId, callback=(err)-> ) ->
+		if !user?
+			logger.log {sessionId}, "no user to untrack, returning"
+			return callback(null)
+		if !sessionId?
+			logger.log {user_id: user._id}, "no sessionId to untrack, returning"
+			return callback(null)
+		logger.log {user_id: user._id, sessionId}, "onLogout handler"
+		sessionSetKey = UserSessionsManager._sessionSetKey(user)
+		value = UserSessionsManager._sessionKey sessionId
+		rclient.multi()
+			.srem(sessionSetKey, value)
+			.expire(sessionSetKey, "#{Settings.cookieSessionLength}")
+			.exec (err, response) ->
+				if err?
+					logger.err {err, user_id: user._id, sessionSetKey}, "error while removing session key from UserSessions set"
+					return callback(err)
+				UserSessionsManager._checkSessions(user, () ->)
+				callback()
+
+	revokeAllUserSessions: (user, retain, callback=(err)->) ->
+		if !retain
+			retain = []
+		retain = retain.map((i) -> UserSessionsManager._sessionKey(i))
+		if !user
+			logger.log {}, "no user to revoke sessions for, returning"
+			return callback(null)
+		logger.log {user_id: user._id}, "revoking all existing sessions for user"
+		sessionSetKey = UserSessionsManager._sessionSetKey(user)
+		rclient.smembers sessionSetKey, (err, sessionKeys) ->
+			if err?
+				logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set"
+				return callback(err)
+			keysToDelete = _.filter(sessionKeys, (k) -> k not in retain)
+			if keysToDelete.length == 0
+				logger.log {user_id: user._id}, "no sessions in UserSessions set to delete, returning"
+				return callback(null)
+			logger.log {user_id: user._id, count: keysToDelete.length}, "deleting sessions for user"
+			rclient.multi()
+				.del(keysToDelete)
+				.srem(sessionSetKey, keysToDelete)
+				.exec (err, result) ->
+					if err?
+						logger.err {err, user_id: user._id, sessionSetKey}, "error revoking all sessions for user"
+						return callback(err)
+					callback(null)
+
+	touch: (user, callback=(err)->) ->
+		if !user
+			logger.log {}, "no user to touch sessions for, returning"
+			return callback(null)
+		sessionSetKey = UserSessionsManager._sessionSetKey(user)
+		rclient.expire sessionSetKey, "#{Settings.cookieSessionLength}", (err, response) ->
+			if err?
+				logger.err {err, user_id: user._id}, "error while updating ttl on UserSessions set"
+				return callback(err)
+			callback(null)
+
+	_checkSessions: (user, callback=(err)->) ->
+		if !user
+			logger.log {}, "no user, returning"
+			return callback(null)
+		logger.log {user_id: user._id}, "checking sessions for user"
+		sessionSetKey = UserSessionsManager._sessionSetKey(user)
+		rclient.smembers sessionSetKey, (err, sessionKeys) ->
+			if err?
+				logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set"
+				return callback(err)
+			logger.log {user_id: user._id, count: sessionKeys.length}, "checking sessions for user"
+			Async.series(
+				sessionKeys.map(
+					(key) ->
+						(next) ->
+							rclient.get key, (err, val) ->
+								if err?
+									return next(err)
+								if !val?
+									logger.log {user_id: user._id, key}, ">> removing key from UserSessions set"
+									rclient.srem sessionSetKey, key, (err, result) ->
+										if err?
+											return next(err)
+										return next(null)
+								else
+									next()
+				)
+				, (err, results) ->
+					logger.log {user_id: user._id}, "done checking sessions for user"
+					return callback(err)
+			)

+ 0 - 81
services/web/app/coffee/Features/Wiki/WikiController.coffee

@@ -1,81 +0,0 @@
-request = require("request")
-settings = require("settings-sharelatex")
-logger = require("logger-sharelatex")
-ErrorController = require "../Errors/ErrorController"
-_ = require("underscore")
-AuthenticationController = require("../Authentication/AuthenticationController")
-async = require("async")
-other_lngs = ["es"]
-
-module.exports = WikiController = 
-
-
-	_checkIfLoginIsNeeded: (req, res, next)->
-		if settings.apis.wiki.requireLogin
-			AuthenticationController.requireLogin()(req, res, next)
-		else
-			next()
-
-	getPage: (req, res, next) ->
-		WikiController._checkIfLoginIsNeeded req, res, ->
-			
-			page = req.url.replace(/^\/learn/, "").replace(/^\//, "")
-			if page == ""
-				page = "Main_Page"
-
-			logger.log page: page, "getting page from wiki"
-			if _.include(other_lngs, req.lng)
-				lngPage = "#{page}_#{req.lng}"
-			else
-				lngPage = page
-			jobs =
-				contents: (cb)-> 
-					WikiController._getPageContent "Contents", cb
-				pageData: (cb)->
-					WikiController._getPageContent lngPage, cb
-			async.parallel jobs, (error, results)->
-				return next(error) if error?
-				{pageData, contents} = results
-				if pageData.content?.length > 280
-					if _.include(other_lngs, req.lng)
-						pageData.title = pageData.title.slice(0, pageData.title.length - (req.lng.length+1) )
-					WikiController._renderPage(pageData, contents, res)
-				else
-					WikiController._getPageContent page, (error, pageData) ->
-						return next(error) if error?
-						WikiController._renderPage(pageData, contents, res)
-
-
-					
-
-	_getPageContent: (page, callback = (error, data = { content: "", title: "" }) ->) ->
-		request {
-			url: "#{settings.apis.wiki.url}/learn-scripts/api.php"
-			qs: {
-				page: decodeURI(page)
-				action: "parse"
-				format: "json"
-			}
-		}, (err, response, data)->
-			return callback(err) if err?
-			try
-				data = JSON.parse(data)
-			catch err
-				logger.err err:err, data:data, "error parsing data from wiki"
-			result = 
-				content: data?.parse?.text?['*']
-				title: data?.parse?.title
-			callback null, result
-
-
-	_renderPage: (page, contents, res)->
-		if page.title == "Main Page"
-			title = "Documentation"
-		else
-			title = page.title
-			
-		res.render "wiki/page", {
-			page: page
-			contents: contents
-			title: title
-		}

+ 63 - 7
services/web/app/coffee/infrastructure/ExpressLocals.coffee

@@ -7,15 +7,19 @@ querystring = require('querystring')
 SystemMessageManager = require("../Features/SystemMessages/SystemMessageManager")
 _ = require("underscore")
 Modules = require "./Modules"
+Url = require "url"
 
 fingerprints = {}
 Path = require 'path'
+
+
 jsPath =
 	if Settings.useMinifiedJs
 		"/minjs/"
 	else
 		"/js/"
 
+
 logger.log "Generating file fingerprints..."
 for path in [
 	"#{jsPath}libs/require.js",
@@ -37,7 +41,18 @@ for path in [
 		fingerprints[path] = hash
 	else
 		logger.log filePath:filePath, "file does not exist for fingerprints"
-	
+
+getFingerprint = (path) ->
+	if fingerprints[path]?
+		return fingerprints[path]
+	else
+		logger.err "No fingerprint for file: #{path}"
+		return ""
+
+logger.log "Finished generating file fingerprints"
+
+cdnAvailable = Settings.cdn?.web?.host?
+darkCdnAvailable = Settings.cdn?.web?.darkHost?
 
 module.exports = (app, webRouter, apiRouter)->
 	webRouter.use (req, res, next)->
@@ -45,9 +60,55 @@ module.exports = (app, webRouter, apiRouter)->
 		next()
 
 	webRouter.use (req, res, next)-> 
+
+		isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark"
+		isSmoke = req.headers?.host?.slice(0,5)?.toLowerCase() == "smoke"
+		isLive = !isDark and !isSmoke
+
+		if cdnAvailable and isLive
+			staticFilesBase = Settings.cdn?.web?.host
+		else if darkCdnAvailable and isDark
+			staticFilesBase = Settings.cdn?.web?.darkHost
+		else
+			staticFilesBase = ""
+		
 		res.locals.jsPath = jsPath
+		res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath)
+
+
+		res.locals.buildJsPath = (jsFile, opts = {})->
+			path = Path.join(jsPath, jsFile)
+
+			doFingerPrint = opts.fingerprint != false
+			
+			if !opts.qs?
+				opts.qs = {}
+
+			if !opts.qs?.fingerprint? and doFingerPrint
+				opts.qs.fingerprint = getFingerprint(path)
+
+			if opts.cdn != false
+				path = Url.resolve(staticFilesBase, path)
+				
+			qs = querystring.stringify(opts.qs)
+
+			if qs? and qs.length > 0
+				path = path + "?" + qs
+			return path
+
+
+		res.locals.buildCssPath = (cssFile)->
+			path = Path.join("/stylesheets/", cssFile)
+			return Url.resolve(staticFilesBase, path) + "?fingerprint=" + getFingerprint(path)
+
+		res.locals.buildImgPath = (imgFile)->
+			path = Path.join("/img/", imgFile)
+			return Url.resolve(staticFilesBase, path)
+
 		next()
 
+
+
 	webRouter.use (req, res, next)-> 
 		res.locals.settings = Settings
 		next()
@@ -113,12 +174,7 @@ module.exports = (app, webRouter, apiRouter)->
 		next()
 
 	webRouter.use (req, res, next)-> 
-		res.locals.fingerprint = (path) ->
-			if fingerprints[path]?
-				return fingerprints[path]
-			else
-				logger.err "No fingerprint for file: #{path}"
-				return ""
+		res.locals.fingerprint = getFingerprint
 		next()
 
 	webRouter.use (req, res, next)-> 

+ 10 - 7
services/web/app/coffee/infrastructure/Server.coffee

@@ -31,6 +31,7 @@ translations = require("translations-sharelatex").setup(Settings.i18n)
 Modules = require "./Modules"
 
 ErrorController = require "../Features/Errors/ErrorController"
+UserSessionsManager = require "../Features/User/UserSessionsManager"
 
 metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongojs/node_modules/mongodb"), logger)
 metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongoose/node_modules/mongodb"), logger)
@@ -89,6 +90,8 @@ webRouter.use translations.setLangBasedOnDomainMiddlewear
 # Measure expiry from last request, not last login
 webRouter.use (req, res, next) ->
 	req.session.touch()
+	if req?.session?.user?
+		UserSessionsManager.touch(req.session.user, (err)->)
 	next()
 
 webRouter.use ReferalConnect.use
@@ -98,23 +101,23 @@ if app.get('env') == 'production'
 	logger.info "Production Enviroment"
 	app.enable('view cache')
 
-
-
 app.use (req, res, next)->
 	metrics.inc "http-request"
 	crawlerLogger.log(req)
 	next()
 
-app.use (req, res, next) ->
-	if !Settings.editorIsOpen
+webRouter.use (req, res, next) ->
+	if Settings.editorIsOpen
+		next()
+	else if req.url.indexOf("/admin") == 0
+		next()
+	else
 		res.status(503)
 		res.render("general/closed", {title:"maintenance"})
-	else
-		next()
 
 apiRouter.get "/status", (req, res)->
 	res.send("web sharelatex is alive")
-	
+
 profiler = require "v8-profiler"
 apiRouter.get "/profile", (req, res) ->
 	time = parseInt(req.query.time || "1000")

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

@@ -58,6 +58,7 @@ UserSchema = new Schema
 		mendeley: Boolean  # coerce the refProviders values to Booleans
 		zotero: Boolean
 	}
+	betaProgram:   { type:Boolean, default: false}
 
 conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: 10)
 

+ 40 - 12
services/web/app/coffee/router.coffee

@@ -30,7 +30,6 @@ PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter")
 StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter")
 ChatController = require("./Features/Chat/ChatController")
 BlogController = require("./Features/Blog/BlogController")
-WikiController = require("./Features/Wiki/WikiController")
 Modules = require "./infrastructure/Modules"
 RateLimiterMiddlewear = require('./Features/Security/RateLimiterMiddlewear')
 RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter')
@@ -38,6 +37,7 @@ InactiveProjectController = require("./Features/InactiveData/InactiveProjectCont
 ContactRouter = require("./Features/Contacts/ContactRouter")
 ReferencesController = require('./Features/References/ReferencesController')
 AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear')
+BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
 
 logger = require("logger-sharelatex")
 _ = require("underscore")
@@ -79,6 +79,7 @@ module.exports = class Router
 		webRouter.get '/blog/*', BlogController.getPage
 
 		webRouter.get '/user/activate', UserPagesController.activateAccountPage
+		AuthenticationController.addEndpointToLoginWhitelist '/user/activate'
 
 		webRouter.get  '/user/settings', AuthenticationController.requireLogin(), UserPagesController.settingsPage
 		webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings
@@ -104,7 +105,11 @@ module.exports = class Router
 		webRouter.post '/project/:Project_id/settings/admin', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.updateProjectAdminSettings
 
 		webRouter.post '/project/:Project_id/compile', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.compile
-		webRouter.get  '/Project/:Project_id/output/output.pdf', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.downloadPdf
+		webRouter.post '/project/:Project_id/compile/stop', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.stopCompile
+
+		# Used by the web download buttons, adds filename header
+		webRouter.get  '/project/:Project_id/output/output.pdf', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.downloadPdf
+		# Used by the pdf viewers
 		webRouter.get  /^\/project\/([^\/]*)\/output\/(.*)$/,
 			((req, res, next) ->
 				params =
@@ -118,12 +123,36 @@ module.exports = class Router
 			((req, res, next) ->
 				params =
 					"Project_id": req.params[0]
-					"build":      req.params[1]
+					"build_id":   req.params[1]
+					"file":       req.params[2]
+				req.params = params
+				next()
+			), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi
+
+		# direct url access to output files for user but no build, to retrieve files when build fails
+		webRouter.get  /^\/project\/([^\/]*)\/user\/([0-9a-f-]+)\/output\/(.*)$/,
+			((req, res, next) ->
+				params =
+					"Project_id": req.params[0]
+					"user_id":   req.params[1]
 					"file":       req.params[2]
 				req.params = params
 				next()
 			), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi
 
+		# direct url access to output files for a specific user and build (query string not required)
+		webRouter.get  /^\/project\/([^\/]*)\/user\/([0-9a-f]+)\/build\/([0-9a-f-]+)\/output\/(.*)$/,
+			((req, res, next) ->
+				params =
+					"Project_id": req.params[0]
+					"user_id":    req.params[1]
+					"build_id":   req.params[2]
+					"file":       req.params[3]
+				req.params = params
+				next()
+			), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi
+
+
 		webRouter.delete "/project/:Project_id/output", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.deleteAuxFiles
 		webRouter.get "/project/:Project_id/sync/code", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySyncCode
 		webRouter.get "/project/:Project_id/sync/pdf", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySyncPdf
@@ -187,16 +216,13 @@ module.exports = class Router
 		webRouter.get  "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
 		webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
 
-		webRouter.get  /learn(\/.*)?/, RateLimiterMiddlewear.rateLimit({
-			endpointName: "wiki"
-			params: []
-			maxRequests: 60
-			timeInterval: 60
-		}), WikiController.getPage
-
 		webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
 		webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll
 
+		webRouter.get "/beta/participate",  AuthenticationController.requireLogin(), BetaProgramController.optInPage
+		webRouter.post "/beta/opt-in", AuthenticationController.requireLogin(), BetaProgramController.optIn
+		webRouter.post "/beta/opt-out", AuthenticationController.requireLogin(), BetaProgramController.optOut
+
 		#Admin Stuff
 		webRouter.get  '/admin', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.index
 		webRouter.get  '/admin/user', AuthorizationMiddlewear.ensureUserIsSiteAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon
@@ -216,9 +242,11 @@ module.exports = class Router
 		apiRouter.get '/status', (req,res)->
 			res.send("websharelatex is up")
 
+		webRouter.get '/dev/csrf', (req, res) ->
+			res.send res.locals.csrfToken
 
-		webRouter.get '/health_check', HealthCheckController.check
-		webRouter.get '/health_check/redis', HealthCheckController.checkRedis
+		apiRouter.get '/health_check', HealthCheckController.check
+		apiRouter.get '/health_check/redis', HealthCheckController.checkRedis
 
 		apiRouter.get "/status/compiler/:Project_id", AuthorizationMiddlewear.ensureUserCanReadProject, (req, res) ->
 			sendRes = _.once (statusCode, message)->

+ 18 - 15
services/web/app/views/admin/index.jade

@@ -9,6 +9,22 @@ block content
 						.page-header
 							h1 Admin Panel
 						tabset(ng-cloak)
+							tab(heading="System Messages")
+								each message in systemMessages 
+									.alert.alert-info.row-spaced !{message.content}
+								hr
+								form(enctype='multipart/form-data', method='post', action='/admin/messages')
+									input(name="_csrf", type="hidden", value=csrfToken)
+									.form-group
+										label(for="content")
+										input.form-control(name="content", type="text", placeholder="Message...", required)
+									button.btn.btn-primary(type="submit") Post Message
+								hr
+								form(enctype='multipart/form-data', method='post', action='/admin/messages/clear')
+									input(name="_csrf", type="hidden", value=csrfToken)
+									button.btn.btn-danger(type="submit") Clear all messages
+
+
 							tab(heading="Open Sockets")
 								.row-spaced
 									ul
@@ -17,7 +33,7 @@ block content
 												ul
 													-each agent in agents
 														li #{agent}
-														
+
 							tab(heading="Close Editor")
 								.row-spaced
 									form(enctype='multipart/form-data', method='post',action='/admin/closeEditor')
@@ -66,19 +82,6 @@ block content
 										.form-group
 											button.btn-primary.btn(type='submit') Poll
 										
-							tab(heading="System Messages")
-								each message in systemMessages 
-									.alert.alert-info.row-spaced !{message.content}
-								hr
-								form(enctype='multipart/form-data', method='post', action='/admin/messages')
-									input(name="_csrf", type="hidden", value=csrfToken)
-									.form-group
-										label(for="content")
-										input.form-control(name="content", type="text", placeholder="Message...", required)
-									button.btn.btn-primary(type="submit") Post Message
-								hr
-								form(enctype='multipart/form-data', method='post', action='/admin/messages/clear')
-									input(name="_csrf", type="hidden", value=csrfToken)
-									button.btn.btn-danger(type="submit") Clear all messages
+
 
 

+ 47 - 0
services/web/app/views/beta_program/opt_in.jade

@@ -0,0 +1,47 @@
+extends ../layout
+
+block content
+	.content.content-alt
+		.container.beta-opt-in-wrapper
+			.row
+				.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
+					.card
+						.page-header.text-centered
+							h1 
+								| #{translate("sharelatex_beta_program")}
+						.beta-opt-in
+							.container-fluid
+								.row
+									.col-md-12
+										p.text-centered #{translate("beta_program_benefits")}
+										p.text-centered
+											| #{translate("beta_program_badge_description")}
+											span.beta-feature-badge
+										p.text-centered
+											| #{translate("beta_program_current_beta_features_description")}
+											ul.list-unstyled.text-center
+												li 
+													i.fa.fa-fw.fa-book
+													| &nbsp;#{translate("mendeley_integration")}
+								.row.text-centered
+									.col-md-12
+										if user.betaProgram
+											p #{translate("beta_program_already_participating")}
+											form(id="beta-program-opt-out", method="post", action="/beta/opt-out", novalidate)
+												.form-group
+													input(type="hidden", name="_csrf", value=csrfToken)
+													button.btn.btn-primary(
+														type="submit"
+													)
+														span #{translate("beta_program_opt_out_action")}
+												.form-group
+													a(href="/project").btn.btn-info #{translate("back_to_your_projects")}
+										else
+											form(id="beta-program-opt-in", method="post", action="/beta/opt-in", novalidate)
+												.form-group
+													input(type="hidden", name="_csrf", value=csrfToken)
+													button.btn.btn-primary(
+														type="submit"
+													)
+														span #{translate("beta_program_opt_in_action")}
+											

+ 14 - 2
services/web/app/views/contact-us-modal.jade

@@ -11,7 +11,19 @@ script(type='text/ng-template', id='supportModalTemplate')
 			label
 				| #{translate("subject")}
 			.form-group
-				input.field.text.medium.span8.form-control(ng-model="form.subject", maxlength='255', tabindex='1', onkeyup='')
+				input.field.text.medium.span8.form-control(
+					ng-model="form.subject",
+					ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }"
+					maxlength='255',
+					tabindex='1',
+					onkeyup='')
+			.contact-suggestions(ng-show="suggestions.length")
+				p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "<a href='learn/kb' target='_blank'>__kb__</a>", kb: translate("knowledge_base") })} 
+				ul.contact-suggestion-list
+					li(ng-repeat="suggestion in suggestions")
+						a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
+							span(ng-bind-html="suggestion.name")
+							i.fa.fa-angle-right
 			label.desc(ng-show="'#{getUserEmail()}'.length < 1")
 				| #{translate("email")}
 			.form-group(ng-show="'#{getUserEmail()}'.length < 1")
@@ -21,7 +33,7 @@ script(type='text/ng-template', id='supportModalTemplate')
 			.form-group
 				input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='')
 			label.desc
-				| #{translate("suggestion")}
+				| #{translate("contact_message_label")}
 			.form-group
 				textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='')
 			.form-group.text-center

+ 2 - 2
services/web/app/views/general/500.jade

@@ -3,7 +3,7 @@ html(itemscope, itemtype='http://schema.org/Product')
 	head
 		title Something went wrong
 		link(rel="icon", href="/favicon.ico")
-		link(rel='stylesheet', href='/stylesheets/style.css')
+		link(rel='stylesheet', href=buildCssPath('/style.css'))
 		link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet")
 	body
 		.content
@@ -12,7 +12,7 @@ html(itemscope, itemtype='http://schema.org/Product')
 					.col-md-8.col-md-offset-2.text-center
 						.page-header
 							h2 Oh dear, something went wrong.
-						p: img(src="/img/lion-sad-128.png", alt="Sad Lion")
+						p: img(src=buildImgPath("lion-sad-128.png"), alt="Sad Lion")
 						p
 							| Something went wrong with your request, sorry. Our staff are probably looking into this, but if it continues, please contact us at #{settings.adminEmail}
 						p

+ 83 - 13
services/web/app/views/layout.jade

@@ -13,13 +13,12 @@ html(itemscope, itemtype='http://schema.org/Product')
 		
 
 		-if (typeof(title) == "undefined")
-			title ShareLaTeX, the Online LaTeX Editor
+			title= 'ShareLaTeX, '+ translate("online_latex_editor") 
 		-else
-			title= translate(title) + ' - ShareLaTeX, '+translate("online_latex_editor") 
+			title= translate(title) + ' - ShareLaTeX, ' + translate("online_latex_editor") 
 
 		link(rel="icon", href="/favicon.ico")
-		link(rel='stylesheet', href='/stylesheets/style.css?fingerprint='+fingerprint('/stylesheets/style.css'))
-		link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet")
+		link(rel='stylesheet', href=buildCssPath('/style.css'))
 
 		if settings.i18n.subdomainLang
 			each subdomainDetails in settings.i18n.subdomainLang
@@ -30,7 +29,7 @@ html(itemscope, itemtype='http://schema.org/Product')
 		meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor")
 		
 		-if (typeof(meta) == "undefined")
-			meta(itemprop="description", name="description", content="An online LaTeX editor that's easy to use. No installation, real-time collaboration, version control, hundreds of LaTeX templates, and more.")
+			meta(itemprop="description", name="description", content='#{translate("site_description")}')
 		-else
 			meta(itemprop="description", name="description" , content=meta)
 		
@@ -46,15 +45,84 @@ html(itemscope, itemtype='http://schema.org/Product')
 				ga('send', 'pageview');
 		- else
 			script(type='text/javascript').
-				window.ga = function() { console.log("Sending to GA", arguments) };
+				window.ga = function() { console.log("would send to GA", arguments) };
 
+		// Countly Analytics
+		if (settings.analytics && settings.analytics.countly && settings.analytics.countly.token)
+			script(type="text/javascript").
+				var Countly = Countly || {};
+				Countly.q = Countly.q || [];
+				Countly.app_key = '#{settings.analytics.countly.token}';
+				Countly.url = '#{settings.analytics.countly.server}';
+				!{ session.user ? 'Countly.device_id = "' + session.user._id + '";' : '' } 
+
+				(function() {
+					var cly = document.createElement('script'); cly.type = 'text/javascript';
+					cly.async = true;
+					//enter url of script here
+					cly.src = 'https://cdnjs.cloudflare.com/ajax/libs/countly-sdk-web/16.6.0/countly.min.js';
+					cly.onload = function(){Countly.init()};
+					var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(cly, s);
+				})();
+
+			script(type="text/javascript")
+				if (session && session.user)
+					- var name = session.user.first_name + (session.user.last_name ? ' ' + session.user.last_name : '');
+					| Countly.q.push(['user_details', { email: '#{session.user.email}', name: '#{name}' }]);
+
+				if (justRegistered)
+					| Countly.q.push(['add_event',{ key: 'user-registered' }]);
+
+				if (justLoggedIn)
+					| Countly.q.push(['add_event',{ key: 'user-logged-in' }]);
+
+				if (user && user.features) 
+					- featureFlagSet = false;
+
+					if user.features.hasOwnProperty('collaborators')
+						| Countly.q.push([ 'userData.set', 'features-collaborators', #{ user.features.collaborators } ]);
+						- featureFlagSet = true;
+
+					if user.features.hasOwnProperty('compileGroup')
+						| Countly.q.push([ 'userData.set', 'features-compileGroup', '#{ user.features.compileGroup }' ]);
+						- featureFlagSet = true;
+
+					if user.features.hasOwnProperty('compileTimeout')
+						| Countly.q.push([ 'userData.set', 'features-compileTimeout', #{ user.features.compileTimeout } ]);
+						- featureFlagSet = true;
+
+					if user.features.hasOwnProperty('dropbox')
+						| Countly.q.push([ 'userData.set', 'features-dropbox', #{ user.features.dropbox } ]);
+						- featureFlagSet = true;
+
+					if user.features.hasOwnProperty('github')
+						| Countly.q.push([ 'userData.set', 'features-github', #{ user.features.github } ]);
+						- featureFlagSet = true;
+
+					if user.features.hasOwnProperty('references')
+						| Countly.q.push([ 'userData.set', 'features-references', #{ user.features.references } ]);
+						- featureFlagSet = true;
+
+					if user.features.hasOwnProperty('templates')
+						| Countly.q.push([ 'userData.set', 'features-templates', #{ user.features.templates } ]);
+						- featureFlagSet = true;
+
+					if user.features.hasOwnProperty('versioning')
+						| Countly.q.push([ 'userData.set', 'features-versioning', #{ user.features.versioning } ]);
+						- featureFlagSet = true;
+
+
+					if featureFlagSet
+						| Countly.q.push(['userData.save'])
+
+		// End countly Analytics
+				
 		script(type="text/javascript").
 			window.csrfToken = "#{csrfToken}";
 			
 		block scripts
-		script(src="#{jsPath}libs/jquery-1.11.1.min.js")
-		script(src="#{jsPath}libs/angular-1.3.15.min.js")
-		include sentry
+		script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
+		script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
 		script.
 			window.sharelatex = {
 				siteUrl: '#{settings.siteUrl}',
@@ -123,12 +191,14 @@ html(itemscope, itemtype='http://schema.org/Product')
 					}
 				};
 			script(
-				data-main=jsPath+'main.js',
-				baseurl=jsPath,
-				src=jsPath+'libs/require.js?fingerprint='+fingerprint(jsPath + 'libs/require.js')
+				data-main=buildJsPath('main.js', {fingerprint:false}),
+				baseurl=fullJsPath,
+				src=buildJsPath('libs/require.js')
 			)
 
-		include contact-us-modal
 
+		include contact-us-modal
+		include sentry
+		
 
 

+ 3 - 4
services/web/app/views/layout/footer.jade

@@ -15,7 +15,7 @@ footer.site-footer
 							aria-expanded="false",
 							tooltip="#{translate('language')}"
 						)
-							img(src="/img/flags/24/#{currentLngCode}.png")
+							figure(class="sprite-icon sprite-icon-lang sprite-icon-#{currentLngCode}")
 
 						ul.dropdown-menu(role="menu")
 							li.dropdown-header #{translate("language")}
@@ -23,9 +23,9 @@ footer.site-footer
 								if !subdomainDetails.hide
 									li.lngOption
 										a.menu-indent(href=subdomainDetails.url+currentUrl)
-											img(src="/img/flags/24/#{subdomainDetails.lngCode}.png")
+											figure(class="sprite-icon sprite-icon-lang sprite-icon-#{subdomainDetails.lngCode}")
 											|  #{translate(subdomainDetails.lngCode)}
-										
+										//- img(src="/img/flags/24/.png")
 				each item in nav.left_footer
 					li
 						if item.url
@@ -41,4 +41,3 @@ footer.site-footer
 							a(href=item.url, class=item.class) !{item.text}
 						else
 							| !{item.text}
-

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

@@ -3,7 +3,9 @@ nav.navbar.navbar-default
 		.navbar-header
 			button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}")
 				i.fa.fa-bars
-			if nav.title
+			if settings.nav.custom_logo
+				a(href='/', style='background-image:url("#{settings.nav.custom_logo}")').navbar-brand
+			else if (nav.title)
 				a(href='/').navbar-title #{nav.title}
 			else
 				a(href='/').navbar-brand
@@ -17,6 +19,8 @@ nav.navbar.navbar-default
 							| Admin
 							b.caret
 						ul.dropdown-menu
+							li
+								a(href="/admin") Manage Site
 							li
 								a(href="/admin/user") Manage Users
 

+ 10 - 6
services/web/app/views/project/editor.jade

@@ -97,9 +97,10 @@ block content
 		window.csrfToken = "!{csrfToken}";
 		window.anonymous = #{anonymous};
 		window.maxDocLength = #{maxDocLength};
+		window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
 		window.requirejs = {
 			"paths" : {
-				"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML",
+				"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {qs:{config:'TeX-AMS_HTML', fingerprint:false}})}",
 				"moment": "libs/moment-2.7.0",
 				"libs/pdf": "libs/pdfjs-1.3.91/pdf"
 			},
@@ -128,15 +129,18 @@ block content
 
 	- var pdfPath = 'libs/pdfjs-1.3.91/pdf.worker.js'
 	- var fingerprintedPath = fingerprint(jsPath+pdfPath)
-	- var pdfJsWorkerPath = jsPath+pdfPath+'?fingerprint='+fingerprintedPath
+	- var pdfJsWorkerPath = buildJsPath(pdfPath, {cdn:false,qs:{fingerprint:fingerprintedPath}}) // don't use worker for cdn
+
+
 	script(type='text/javascript').
 		window.pdfJsWorkerPath = "#{pdfJsWorkerPath}";
 
 	script(
-		data-main=jsPath+"ide.js",
-		baseurl=jsPath,
-		data-ace-base=jsPath+'ace',
-		src=jsPath+'libs/require.js?fingerprint='+fingerprint(jsPath + 'libs/require.js')
+		data-main=buildJsPath("ide.js", {fingerprint:false}),
+		baseurl=fullJsPath,
+		data-ace-base=buildJsPath('ace', {fingerprint:false}),
+		src=buildJsPath('libs/require.js')
 	)
+
 		
 

+ 1 - 1
services/web/app/views/project/editor/editor.jade

@@ -35,7 +35,7 @@ div.full-size(
 			resize-on="layout:main:resize,layout:pdf:resize",
 			annotations="pdf.logEntryAnnotations[editor.open_doc_id]",
 			read-only="!permissions.write",
-			on-ctrl-enter="recompile"
+			on-ctrl-enter="recompileViaKey"
 		)
 
 	.ui-layout-east

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

@@ -15,7 +15,7 @@ aside#left-menu.full-size(
 				| #{translate("source")}
 		li
 			a(
-				ng-href="{{pdf.url}}"
+				ng-href="{{pdf.downloadUrl || pdf.url}}"
 				target="_blank"
 				ng-if="pdf.url"
 			)

+ 133 - 7
services/web/app/views/project/editor/pdf.jade

@@ -1,6 +1,13 @@
 div.full-size.pdf(ng-controller="PdfController")
 	.toolbar.toolbar-tall
-		.btn-group(dropdown)
+		.btn-group(
+			dropdown,
+			tooltip-html="'#{translate('recompile_pdf')} <span class=\"keyboard-shortcut\">({{modifierKey}} + Enter)</span>'"
+			tooltip-class="keyboard-tooltip"
+			tooltip-popup-delay="500"
+			tooltip-append-to-body="true"
+			tooltip-placement="bottom"
+		)
 			a.btn.btn-info(
 				href,
 				ng-disabled="pdf.compiling",
@@ -29,6 +36,14 @@ div.full-size.pdf(ng-controller="PdfController")
 						i.fa.fa-fw(ng-class="{'fa-check': draft}")
 						| &nbsp;#{translate("fast")}&nbsp;
 						span.subdued [draft]
+		a(
+			href
+			ng-click="stop()"
+			ng-show="pdf.compiling",
+			tooltip="#{translate('stop_compile')}"
+			tooltip-placement="bottom"
+		)
+			i.fa.fa-stop()
 		a.log-btn(
 			href
 			ng-click="toggleLogs()"
@@ -46,7 +61,7 @@ div.full-size.pdf(ng-controller="PdfController")
 			) {{ pdf.logEntries.errors.length + pdf.logEntries.warnings.length }}
 			
 		a(
-			ng-href="{{pdf.url}}"
+			ng-href="{{pdf.downloadUrl || pdf.url}}"
 			target="_blank"
 			ng-if="pdf.url"
 			tooltip="#{translate('download_pdf')}"
@@ -93,15 +108,98 @@ div.full-size.pdf(ng-controller="PdfController")
 					'alert-info':    entry.level == 'typesetting'\
 				}"
 				ng-click="openInEditor(entry)"
+				ng-init="feedbackSent = false; showNegFeedbackUI = false; negFeedbackReason = ''; negFeedbackReasonFreeText = ''"
 			)
 				span.line-no
+					i.fa.fa-link(aria-hidden="true")
+					| &nbsp;
 					span(ng-show="entry.file") {{ entry.file }}
 					span(ng-show="entry.line") , line {{ entry.line }}
 				p.entry-message(ng-show="entry.message") {{ entry.message }}
-				p.entry-content(ng-show="entry.content") {{ entry.content }}
+				.card.card-hint(
+					ng-if="entry.humanReadableHint"
+					stop-propagation="click"
+				)
+					figure.card-hint-icon-container
+						i.fa.fa-lightbulb-o(aria-hidden="true")
+					p.card-hint-text(
+						ng-show="entry.humanReadableHint",
+						ng-bind-html="wikiEnabled ? entry.humanReadableHint : stripHTMLFromString(entry.humanReadableHint)")
+					.card-hint-actions.clearfix
+						.card-hint-ext-link(ng-if="wikiEnabled")
+							a(
+								ng-href="{{ entry.extraInfoURL }}",
+								ng-click="trackLogHintsLearnMore()"
+								target="_blank"
+							)
+								i.fa.fa-external-link
+								|&nbsp;#{translate("log_hint_extra_info")}
+						.card-hint-feedback(
+							ng-hide="feedbackSent || showNegFeedbackUI"
+							ng-class="entry.ruleId"
+						)
+							label.card-hint-feedback-label #{translate("log_hint_feedback_label")}
+							a.card-hint-feedback-positive(
+								ng-click="trackLogHintsPositiveFeedback(entry.ruleId); feedbackSent = true;"
+								href
+							) #{translate("answer_yes")}
+							span &nbsp;/&nbsp;
+							a.card-hint-feedback-negative(
+								ng-click="trackLogHintsNegativeFeedback(entry.ruleId); showNegFeedbackUI = true;"
+								href
+							) #{translate("answer_no")}
+						.card-hint-extra-feedback(ng-hide="!showNegFeedbackUI || feedbackSent")
+							p.card-hint-extra-feedback-label #{translate("log_hint_ask_extra_feedback")}
+							.radio: label
+								input(
+									type="radio"
+									name="{{ 'neg-feedback-reason-' + $index }}"
+									ng-model="negFeedbackReason"
+									value="{{ logHintsNegFeedbackValues.DIDNT_UNDERSTAND }}"
+								)
+								| #{translate("log_hint_extra_feedback_didnt_understand")}
+							.radio: label
+								input(
+									type="radio"
+									name="{{ 'neg-feedback-reason-' + $index }}"
+									ng-model="negFeedbackReason"
+									value="{{ logHintsNegFeedbackValues.NOT_APPLICABLE }}"
+								)
+								| #{translate("log_hint_extra_feedback_not_applicable")}
+							.radio: label
+								input(
+									type="radio"
+									name="{{ 'neg-feedback-reason-' + $index }}"
+									ng-model="negFeedbackReason"
+									value="{{ logHintsNegFeedbackValues.INCORRECT }}"
+								)
+								| #{translate("log_hint_extra_feedback_incorrect")}
+							.radio: label
+								input(
+									type="radio"
+									name="{{ 'neg-feedback-reason-' + $index }}"
+									ng-model="negFeedbackReason"
+									value="{{ logHintsNegFeedbackValues.OTHER }}"
+								)
+								| #{translate("log_hint_extra_feedback_other")}
+							textarea.form-control(
+								ng-show="negFeedbackReason === logHintsNegFeedbackValues.OTHER"
+								ng-model="negFeedbackReasonFreeText"
+								rows="2"
+							)
+							.clearfix
+								button.btn.btn-default.btn-sm.pull-right(
+									ng-disabled="!negFeedbackReason"
+									ng-click="trackLogHintsNegFeedbackDetails(entry.ruleId, negFeedbackReason, negFeedbackReasonFreeText); feedbackSent = true;"
+								) #{translate("log_hint_extra_feedback_submit")}
+
+						.card-hint-feedback(ng-show="feedbackSent")
+							label.card-hint-feedback-label #{translate("log_hint_feedback_gratitude")}
+
+				p.entry-content(ng-show="entry.content") {{ entry.content.trim() }}
 
 		p
-			.pull-right
+			.files-dropdown-container
 				a.btn.btn-default.btn-sm(
 					href,
 					tooltip="#{translate('clear_cached_files')}",
@@ -111,12 +209,15 @@ div.full-size.pdf(ng-controller="PdfController")
 				)
 					i.fa.fa-trash-o
 				| &nbsp;
-				div.dropdown(style="display: inline-block;", dropdown)
+				div.files-dropdown(
+					ng-class="shouldDropUp ? 'dropup' : 'dropdown'"
+					dropdown
+				)
 					a.btn.btn-default.btn-sm(
 						href
 						dropdown-toggle
 					)
-						| !{translate("other_logs_and_files")} 
+						| !{translate("other_logs_and_files")}
 						span.caret
 					ul.dropdown-menu.dropdown-menu-right
 						li(ng-repeat="file in pdf.outputFiles")
@@ -154,6 +255,27 @@ div.full-size.pdf(ng-controller="PdfController")
 				ng-if="settings.pdfViewer == 'native'"
 			)
 
+		.pdf-validation-problems(ng-switch-when="validation-problems")
+
+			.alert.alert-danger(ng-show="pdf.validation.duplicatePaths")
+				strong #{translate("latex_error")}
+				span #{translate("duplicate_paths_found")}
+
+
+			.alert.alert-danger(ng-show="pdf.validation.sizeCheck")
+				strong #{translate("project_too_large")} 
+				div #{translate("project_too_large_please_reduce")}
+				div
+					li(ng-repeat="entry in pdf.validation.sizeCheck.resources") {{ '/'+entry['path'] }} - {{entry['kbSize']}}kb
+	
+			.alert.alert-danger(ng-show="pdf.validation.conflictedPaths")
+				div
+					strong #{translate("conflicting_paths_found")}
+				div #{translate("following_paths_conflict")}
+				div
+					li(ng-repeat="entry in pdf.validation.conflictedPaths") {{ '/'+entry['path'] }}
+
+
 		.pdf-errors(ng-switch-when="errors")
 	
 			.alert.alert-danger(ng-show="pdf.error")
@@ -171,7 +293,11 @@ div.full-size.pdf(ng-controller="PdfController")
 			.alert.alert-danger(ng-show="pdf.tooRecentlyCompiled")
 				strong #{translate("server_error")} 
 				span #{translate("too_recently_compiled")}
-	
+
+			.alert.alert-danger(ng-show="pdf.compileTerminated")
+				strong #{translate("terminated")}. 
+				span #{translate("compile_terminated_by_user")}
+
 			.alert.alert-danger(ng-show="pdf.timedout")
 				p
 					strong #{translate("timedout")}. 

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

@@ -49,7 +49,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 					.form-group
 						tags-input(
 							template="shareTagTemplate"
-							placeholder="joe@example.com, sue@example.com, ..."
+							placeholder="#{settings.customisation.shareProjectPlaceholder || 'joe@example.com, sue@example.com, ...'}"
 							ng-model="inputs.contacts"
 							focus-on="open"
 							display-property="display"

+ 5 - 16
services/web/app/views/project/list/side-bar.jade

@@ -94,18 +94,7 @@
 - if (showUserDetailsArea)
 	span(ng-controller="LeftHandMenuPromoController",  ng-cloak)
 
-		.row-spaced(ng-if="showDatajoy")
-			hr
-			.card.card-thin
-				p.text-center.small
-					| <strong>Python</strong> or <strong>R</strong> user?
-					p.text-center.small
-						a(href="https://www.getdatajoy.com/", target="_blank").btn.btn-info.btn-small Try DataJoy
-				p.text-center.small(style="font-size: 0.8em")
-					a(href="https://www.getdatajoy.com/", target="_blank") DataJoy
-					|  is a new online Python and R editor from ShareLaTeX.
-
-		.row-spaced#userProfileInformation(ng-if="hasProjects && !showDatajoy")
+		.row-spaced#userProfileInformation(ng-if="hasProjects")
 			div(ng-controller="UserProfileController")
 				hr(ng-show="percentComplete < 100")
 				.text-centered.user-profile(ng-show="percentComplete < 100")
@@ -120,7 +109,7 @@
 					) #{translate("complete")}
 
 
-		.row-spaced(ng-if="hasProjects && userHasSubscription", ng-cloak, sixpack-switch="left-menu-upgraed-rotation").text-centered
+		.row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak, sixpack-switch="left-menu-upgraed-rotation").text-centered
 			span(sixpack-default).text-centered
 				hr
 				p.small #{translate("on_free_sl")}
@@ -146,7 +135,7 @@
 						p
 							span Get Dropbox Sync
 						p
-							img(src="/img/dropbox/simple_logo.png")
+							img(src=buildImgPath("dropbox/simple_logo.png"))
 						p
 							a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
 						p.small.text-centered
@@ -159,13 +148,13 @@
 						p
 							span Get Github Sync
 						p
-							img(src="/img/github/octocat.jpg")
+							img(src=buildImgPath("github/octocat.jpg"))
 						p
 							a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
 						p.small.text-centered
 							| #{translate("or_unlock_features_bonus")} 
 							a(href="/user/bonus") #{translate("sharing_sl")} .
 script.
-	window.userHasSubscription = #{settings.enableSubscriptions && !hasSubscription}
+	window.userHasNoSubscription = #{settings.enableSubscriptions && !hasSubscription}
 
 

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

@@ -2,7 +2,7 @@
 	- if (sentrySrc.match(/^([a-z]+:)?\/\//i))
 		script(src="#{sentrySrc}")
 	- else
-		script(src="#{jsPath}libs/#{sentrySrc}")
+		script(src=buildJsPath("libs/#{sentrySrc}", {fingerprint:false}))
 - if (typeof(sentrySrc) != "undefined")
 	script(type="text/javascript").
 		if (typeof(Raven) != "undefined" && Raven.config) {

+ 17 - 13
services/web/app/views/subscriptions/dashboard.jade

@@ -43,12 +43,8 @@ block content
 					.card(ng-if="view == 'overview'")
 						.page-header
 							h1 #{translate("your_subscription")}
-						-if (groups.length != 0)
-							each groupSubscription in groups
-								p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
-								span 
-									button.btn.btn-danger(ng-click="removeSelfFromGroup('#{groupSubscription.admin_id._id}')") #{translate("leave_group")}
-						- else if (subscription)
+
+						- if (subscription && user._id+'' == subscription.admin_id+'')
 							case subscription.state
 								when "free-trial"
 									p !{translate("on_free_trial_expiring_at", {expiresAt:"<strong>" + subscription.expiresAt + "</strong>"})}
@@ -72,15 +68,9 @@ block content
 									p !{translate("your_subscription_has_expired")}	
 										a(href="/user/subscription/plans") !{translate("create_new_subscription")}
 								default
-									-if(groups.length == 0)
+									-if(groupSubscriptions.length == 0)
 										p !{translate("problem_with_subscription_contact_us")}
 
-							-if(subscription.groupPlan)
-								a(href="/subscription/group").btn.btn-primary !{translate("manage_group")}
-
-								
-
-
 							div(ng-show="changePlan", ng-cloak)#changePlanSection
 								h2.col-md-7 !{translate("change_plan")}
 								span.dropdown.col-md-1.changePlanButton(ng-controller="CurrenyDropdownController", style="display:none", dropdown)
@@ -105,6 +95,20 @@ block content
 									mixin printPlans(plans.individualMonthlyPlans)
 									mixin printPlans(plans.individualAnnualPlans)
 							
+
+						each groupSubscription in groupSubscriptions
+							- if (user._id+'' != groupSubscription.admin_id._id+'')
+								div 
+									p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
+									span 
+										button.btn.btn-danger(ng-click="removeSelfFromGroup('#{groupSubscription.admin_id._id}')") #{translate("leave_group")}
+
+						-if(subscription.groupPlan && user._id+'' == subscription.admin_id+'')
+							div
+								a(href="/subscription/group").btn.btn-primary !{translate("manage_group")}
+
+
+
 					.card(ng-if="view == 'cancelation'")
 						.page-header
 							h1 #{translate("Cancel Subscription")}

+ 1 - 1
services/web/app/views/subscriptions/edit-billing-details.jade

@@ -3,7 +3,7 @@ extends ../layout
 block content
 	- locals.supressDefaultJs = true
 	script(data-main=jsPath+'main.js', src=jsPath+'libs/require.js', baseurl=jsPath)
-	script(src=jsPath+'libs/recurly.min.js')
+	script(src=buildJsPath('libs/recurly.min.js'))
 
 	.content.content-alt
 		.container

+ 2 - 2
services/web/app/views/subscriptions/successful_subscription.jade

@@ -30,10 +30,10 @@ block content
 								| Henry and James
 							.portraits
 								span.img-circle
-									img(src="/img/about/henry_oswald.jpg") 
+									img(src=buildImgPath("about/henry_oswald.jpg"))
 								| &nbsp;
 								span.img-circle
-									img(src="/img/about/james_allen.jpg") 
+									img(src=buildImgPath("about/james_allen.jpg"))
 						p
 							a.btn.btn-primary(href="/project") &lt; #{translate("back_to_your_projects")}
 

+ 1 - 1
services/web/app/views/translations/translation_message.jade

@@ -2,7 +2,7 @@
 	span(ng-controller="TranslationsPopupController", ng-cloak)
 		.translations-message(ng-hide="hidei18nNotification")
 			a(href=recomendSubdomain.url+currentUrl) !{translate("click_here_to_view_sl_in_lng", {lngName:"<strong>" + translate(recomendSubdomain.lngCode) + "</strong>"})} 
-				img(src="/img/flags/24/#{recomendSubdomain.lngCode}.png")
+				img(src=buildImgPath("flags/24/#{recomendSubdomain.lngCode}.png"))
 			button(ng-click="dismiss()").close.pull-right
 				span(aria-hidden="true") &times;
 				span.sr-only #{translate("close")}

+ 19 - 0
services/web/app/views/university/case_study.jade

@@ -0,0 +1,19 @@
+extends ../layout
+
+block content
+	.masthead
+		.container
+			.row
+				.col-md-12
+					h1  !{viewData.title}
+			.row.row-spaced
+
+	.pattern-container
+		.content
+			.container
+				.row
+					.col-md-10.col-md-offset-1
+						.card
+							.row
+								| !{viewData.html}
+

+ 11 - 0
services/web/app/views/user/settings.jade

@@ -99,6 +99,17 @@ block content
 
 							| !{moduleIncludes("userSettings", locals)}
 
+							if (user.betaProgram)
+								hr
+
+								h3
+									| #{translate("sharelatex_beta_program")}
+
+								p.small
+									| #{translate("beta_program_already_participating")}
+								div
+									a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")}
+
 							hr
 
 							if !externalAuthenticationSystemUsed()

+ 0 - 68
services/web/app/views/wiki/page.jade

@@ -1,68 +0,0 @@
-extends ../layout
-
-block content
-	.content.content-alt(ng-cloak)
-		.container.wiki
-			.row.template-page-header
-				.col-md-8(ng-cloak)
-
-			.row
-				.col-xs-3.contents(ng-non-bindable)
-					| !{contents.content}
-
-				.col-xs-9.page
-					- if(typeof(settings.algolia) != "undefined" && typeof(settings.algolia.indexes) != "undefined" && typeof(settings.algolia.indexes.wiki) != "undefined")
-						span(ng-controller="SearchWikiController")
-							.row
-								form.project-search.form-horizontal.col-md-9(role="form")
-									.form-group.has-feedback.has-feedback-left.col-md-12
-										input.form-control.col-md-12(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....")
-										i.fa.fa-search.form-control-feedback-left
-										i.fa.fa-times.form-control-feedback(
-											ng-click="clearSearchText()",
-											style="cursor: pointer;",
-											ng-show="searchQueryText.length > 0"
-										)
-								.col-md-3.text-right
-									a.btn.btn-primary(ng-click="showMissingTemplateModal()") #{translate("suggest_new_doc")}
-
-							.row
-								.col-md-12(ng-cloak)
-									a(ng-href='{{hit.url}}',ng-repeat='hit in hits').search-result.card.card-thin
-										span(ng-bind-html='hit.name')
-										div.search-result-content(ng-show="hit.content != ''", ng-bind-html='hit.content')
-
-					.card.row-spaced(ng-non-bindable)
-						.page-header
-							h1 #{title}
-							
-						| !{page.content}
-
-
-
-
-	script(type="text/ng-template", id="missingWikiPageModal")
-		.modal-header
-			button.close(
-				type="button"
-				data-dismiss="modal"
-				ng-click="close()"
-			) &times;
-			h3 #{translate("suggest_new_doc")}
-		.modal-body.contact-us-modal
-			span(ng-show="sent == false")
-				label.desc
-					| #{translate("email")} (#{translate("optional")})
-				.form-group
-					input.field.text.medium.span8.form-control(ng-model="form.email",  ng-init="form.email = '#{getUserEmail()}'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
-				label.desc
-					| #{translate("suggestion")}
-				.form-group
-					textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', maxlength='255', tabindex='4', onkeyup='')
-			span(ng-show="sent")
-				p #{translate("request_sent_thank_you")}
-		.modal-footer
-			button.btn.btn-default(ng-click="close()") 
-				span #{translate("dismiss")}
-			button.btn-success.btn(type='submit', ng-disabled="sending", ng-click="contactUs()") #{translate("contact_us")}
-

+ 18 - 6
services/web/config/settings.defaults.coffee

@@ -15,7 +15,7 @@ httpAuthUsers[httpAuthUser] = httpAuthPass
 
 sessionSecret = "secret-please-change"
 
-module.exports =
+module.exports = settings =
 	# File storage
 	# ------------
 	#
@@ -104,16 +104,21 @@ module.exports =
 			url: "http://localhost:3036"
 		sixpack:
 			url: ""
-		references:
-			url: "http://localhost:3040"
-		notifications:
-			url: "http://localhost:3042" 
+		# references:
+		# 	url: "http://localhost:3040"
+		# notifications:
+		# 	url: "http://localhost:3042" 
 			
 	templates:
 		user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
 		showSocialButtons: false
 		showComments: false
 
+	# cdn:
+	# 	web:
+	# 		host:"http://cdn.sharelatex.dev:3000"
+	#		darkHost:"http://cdn.sharelatex.dev:3000"
+
 	# Where your instance of ShareLaTeX can be found publically. Used in emails
 	# that are sent out, generated links, etc.
 	siteUrl : siteUrl = 'http://localhost:3000'
@@ -137,6 +142,7 @@ module.exports =
 	# --------
 	security:
 		sessionSecret: sessionSecret
+		bcryptRounds: 12 # number of rounds used to hash user passwords (raised to power 2)
 
 	httpAuthUsers: httpAuthUsers
 
@@ -261,6 +267,10 @@ module.exports =
 	# Should we allow access to any page without logging in? This includes
 	# public projects, /learn, /templates, about pages, etc.
 	allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false
+
+	# Use a single compile directory for all users in a project
+	# (otherwise each user has their own directory)
+	# disablePerUserCompiles: true
 	
 	# Maximum size of text documents in the real-time editing system.
 	max_doc_length: 2 * 1024 * 1024 # 2mb
@@ -334,7 +344,9 @@ module.exports =
 				url: "/logout"
 			}]
 		}]
-	
+
+	customisation: {}
+
 #	templates: [{
 #		name : "cv_or_resume",
 #		url : "/templates/cv"

+ 3853 - 0
services/web/npm-shrinkwrap.json

@@ -0,0 +1,3853 @@
+{
+  "name": "web-sharelatex",
+  "version": "0.1.4",
+  "dependencies": {
+    "archiver": {
+      "version": "0.9.0",
+      "from": "archiver@0.9.0",
+      "resolved": "https://registry.npmjs.org/archiver/-/archiver-0.9.0.tgz",
+      "dependencies": {
+        "buffer-crc32": {
+          "version": "0.2.5",
+          "from": "buffer-crc32@~0.2.1",
+          "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.5.tgz"
+        },
+        "readable-stream": {
+          "version": "1.0.34",
+          "from": "readable-stream@~1.0.24",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+          "dependencies": {
+            "core-util-is": {
+              "version": "1.0.2",
+              "from": "core-util-is@~1.0.0",
+              "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+            },
+            "isarray": {
+              "version": "0.0.1",
+              "from": "isarray@0.0.1",
+              "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+            },
+            "string_decoder": {
+              "version": "0.10.31",
+              "from": "string_decoder@~0.10.x",
+              "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+            },
+            "inherits": {
+              "version": "2.0.1",
+              "from": "inherits@~2.0.1",
+              "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+            }
+          }
+        },
+        "tar-stream": {
+          "version": "0.3.3",
+          "from": "tar-stream@~0.3.0",
+          "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-0.3.3.tgz",
+          "dependencies": {
+            "bl": {
+              "version": "0.6.0",
+              "from": "bl@~0.6.0",
+              "resolved": "https://registry.npmjs.org/bl/-/bl-0.6.0.tgz"
+            },
+            "end-of-stream": {
+              "version": "0.1.5",
+              "from": "end-of-stream@~0.1.3",
+              "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz",
+              "dependencies": {
+                "once": {
+                  "version": "1.3.3",
+                  "from": "once@~1.3.0",
+                  "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz",
+                  "dependencies": {
+                    "wrappy": {
+                      "version": "1.0.2",
+                      "from": "wrappy@1",
+                      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+        "zip-stream": {
+          "version": "0.3.7",
+          "from": "zip-stream@~0.3.0",
+          "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-0.3.7.tgz",
+          "dependencies": {
+            "crc32-stream": {
+              "version": "0.2.0",
+              "from": "crc32-stream@~0.2.0",
+              "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-0.2.0.tgz"
+            },
+            "debug": {
+              "version": "1.0.4",
+              "from": "debug@~1.0.2",
+              "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz",
+              "dependencies": {
+                "ms": {
+                  "version": "0.6.2",
+                  "from": "ms@0.6.2",
+                  "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz"
+                }
+              }
+            },
+            "deflate-crc32-stream": {
+              "version": "0.1.2",
+              "from": "deflate-crc32-stream@~0.1.0",
+              "resolved": "https://registry.npmjs.org/deflate-crc32-stream/-/deflate-crc32-stream-0.1.2.tgz"
+            }
+          }
+        },
+        "lazystream": {
+          "version": "0.1.0",
+          "from": "lazystream@~0.1.0",
+          "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-0.1.0.tgz"
+        },
+        "file-utils": {
+          "version": "0.1.5",
+          "from": "file-utils@~0.1.5",
+          "resolved": "https://registry.npmjs.org/file-utils/-/file-utils-0.1.5.tgz",
+          "dependencies": {
+            "lodash": {
+              "version": "2.1.0",
+              "from": "lodash@~2.1.0",
+              "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.1.0.tgz"
+            },
+            "iconv-lite": {
+              "version": "0.2.11",
+              "from": "iconv-lite@~0.2.11",
+              "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz"
+            },
+            "glob": {
+              "version": "3.2.11",
+              "from": "glob@~3.2.6",
+              "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
+              "dependencies": {
+                "inherits": {
+                  "version": "2.0.1",
+                  "from": "inherits@2",
+                  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                },
+                "minimatch": {
+                  "version": "0.3.0",
+                  "from": "minimatch@0.3",
+                  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz",
+                  "dependencies": {
+                    "lru-cache": {
+                      "version": "2.7.3",
+                      "from": "lru-cache@2",
+                      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
+                    },
+                    "sigmund": {
+                      "version": "1.0.1",
+                      "from": "sigmund@~1.0.0",
+                      "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
+                    }
+                  }
+                }
+              }
+            },
+            "minimatch": {
+              "version": "0.2.14",
+              "from": "minimatch@~0.2.12",
+              "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
+              "dependencies": {
+                "lru-cache": {
+                  "version": "2.7.3",
+                  "from": "lru-cache@2",
+                  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
+                },
+                "sigmund": {
+                  "version": "1.0.1",
+                  "from": "sigmund@~1.0.0",
+                  "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
+                }
+              }
+            },
+            "findup-sync": {
+              "version": "0.1.3",
+              "from": "findup-sync@~0.1.2",
+              "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz",
+              "dependencies": {
+                "lodash": {
+                  "version": "2.4.2",
+                  "from": "lodash@~2.4.1",
+                  "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
+                }
+              }
+            },
+            "isbinaryfile": {
+              "version": "0.1.9",
+              "from": "isbinaryfile@~0.1.9",
+              "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-0.1.9.tgz"
+            }
+          }
+        },
+        "lodash": {
+          "version": "2.4.2",
+          "from": "lodash@~2.4.1",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
+        }
+      }
+    },
+    "async": {
+      "version": "0.6.2",
+      "from": "async@0.6.2",
+      "resolved": "https://registry.npmjs.org/async/-/async-0.6.2.tgz"
+    },
+    "base64-stream": {
+      "version": "0.1.3",
+      "from": "base64-stream@^0.1.2",
+      "resolved": "https://registry.npmjs.org/base64-stream/-/base64-stream-0.1.3.tgz",
+      "dependencies": {
+        "readable-stream": {
+          "version": "2.1.4",
+          "from": "readable-stream@^2.0.2",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz",
+          "dependencies": {
+            "buffer-shims": {
+              "version": "1.0.0",
+              "from": "buffer-shims@^1.0.0",
+              "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz"
+            },
+            "core-util-is": {
+              "version": "1.0.2",
+              "from": "core-util-is@~1.0.0",
+              "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+            },
+            "inherits": {
+              "version": "2.0.1",
+              "from": "inherits@~2.0.1",
+              "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+            },
+            "isarray": {
+              "version": "1.0.0",
+              "from": "isarray@~1.0.0",
+              "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
+            },
+            "process-nextick-args": {
+              "version": "1.0.7",
+              "from": "process-nextick-args@~1.0.6",
+              "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz"
+            },
+            "string_decoder": {
+              "version": "0.10.31",
+              "from": "string_decoder@~0.10.x",
+              "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+            },
+            "util-deprecate": {
+              "version": "1.0.2",
+              "from": "util-deprecate@~1.0.1",
+              "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
+            }
+          }
+        }
+      }
+    },
+    "basic-auth-connect": {
+      "version": "1.0.0",
+      "from": "basic-auth-connect@^1.0.0",
+      "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz"
+    },
+    "bcrypt": {
+      "version": "0.8.3",
+      "from": "bcrypt@0.8.3",
+      "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-0.8.3.tgz",
+      "dependencies": {
+        "bindings": {
+          "version": "1.2.1",
+          "from": "bindings@1.2.1",
+          "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz"
+        },
+        "nan": {
+          "version": "1.8.4",
+          "from": "nan@1.8.4",
+          "resolved": "https://registry.npmjs.org/nan/-/nan-1.8.4.tgz"
+        }
+      }
+    },
+    "body-parser": {
+      "version": "1.15.1",
+      "from": "body-parser@^1.13.1",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.15.1.tgz",
+      "dependencies": {
+        "bytes": {
+          "version": "2.3.0",
+          "from": "bytes@2.3.0",
+          "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.3.0.tgz"
+        },
+        "content-type": {
+          "version": "1.0.2",
+          "from": "content-type@~1.0.1",
+          "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz"
+        },
+        "debug": {
+          "version": "2.2.0",
+          "from": "debug@~2.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+          "dependencies": {
+            "ms": {
+              "version": "0.7.1",
+              "from": "ms@0.7.1",
+              "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+            }
+          }
+        },
+        "depd": {
+          "version": "1.1.0",
+          "from": "depd@~1.1.0",
+          "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz"
+        },
+        "http-errors": {
+          "version": "1.4.0",
+          "from": "http-errors@~1.4.0",
+          "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz",
+          "dependencies": {
+            "inherits": {
+              "version": "2.0.1",
+              "from": "inherits@2.0.1",
+              "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+            },
+            "statuses": {
+              "version": "1.3.0",
+              "from": "statuses@>= 1.2.1 < 2",
+              "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz"
+            }
+          }
+        },
+        "iconv-lite": {
+          "version": "0.4.13",
+          "from": "iconv-lite@0.4.13",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz"
+        },
+        "on-finished": {
+          "version": "2.3.0",
+          "from": "on-finished@~2.3.0",
+          "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+          "dependencies": {
+            "ee-first": {
+              "version": "1.1.1",
+              "from": "ee-first@1.1.1",
+              "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
+            }
+          }
+        },
+        "qs": {
+          "version": "6.1.0",
+          "from": "qs@6.1.0",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz"
+        },
+        "raw-body": {
+          "version": "2.1.6",
+          "from": "raw-body@~2.1.6",
+          "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.6.tgz",
+          "dependencies": {
+            "unpipe": {
+              "version": "1.0.0",
+              "from": "unpipe@1.0.0",
+              "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
+            }
+          }
+        },
+        "type-is": {
+          "version": "1.6.13",
+          "from": "type-is@~1.6.12",
+          "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.13.tgz",
+          "dependencies": {
+            "media-typer": {
+              "version": "0.3.0",
+              "from": "media-typer@0.3.0",
+              "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
+            },
+            "mime-types": {
+              "version": "2.1.11",
+              "from": "mime-types@~2.1.11",
+              "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz",
+              "dependencies": {
+                "mime-db": {
+                  "version": "1.23.0",
+                  "from": "mime-db@~1.23.0",
+                  "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "bufferedstream": {
+      "version": "1.6.0",
+      "from": "bufferedstream@1.6.0",
+      "resolved": "https://registry.npmjs.org/bufferedstream/-/bufferedstream-1.6.0.tgz"
+    },
+    "connect-redis": {
+      "version": "2.3.0",
+      "from": "connect-redis@2.3.0",
+      "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-2.3.0.tgz",
+      "dependencies": {
+        "debug": {
+          "version": "1.0.4",
+          "from": "debug@^1.0.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz",
+          "dependencies": {
+            "ms": {
+              "version": "0.6.2",
+              "from": "ms@0.6.2",
+              "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz"
+            }
+          }
+        },
+        "redis": {
+          "version": "0.12.1",
+          "from": "redis@^0.12.1",
+          "resolved": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz"
+        }
+      }
+    },
+    "contentful": {
+      "version": "3.3.14",
+      "from": "contentful@^3.3.14",
+      "resolved": "https://registry.npmjs.org/contentful/-/contentful-3.3.14.tgz",
+      "dependencies": {
+        "babel-runtime": {
+          "version": "6.9.2",
+          "from": "babel-runtime@^6.3.19",
+          "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.9.2.tgz",
+          "dependencies": {
+            "core-js": {
+              "version": "2.4.0",
+              "from": "core-js@^2.4.0",
+              "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.0.tgz"
+            },
+            "regenerator-runtime": {
+              "version": "0.9.5",
+              "from": "regenerator-runtime@^0.9.5",
+              "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.9.5.tgz"
+            }
+          }
+        },
+        "contentful-sdk-core": {
+          "version": "2.2.4",
+          "from": "contentful-sdk-core@^2.2.1",
+          "resolved": "https://registry.npmjs.org/contentful-sdk-core/-/contentful-sdk-core-2.2.4.tgz",
+          "dependencies": {
+            "follow-redirects": {
+              "version": "0.0.7",
+              "from": "follow-redirects@0.0.7",
+              "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz",
+              "dependencies": {
+                "debug": {
+                  "version": "2.2.0",
+                  "from": "debug@^2.2.0",
+                  "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+                  "dependencies": {
+                    "ms": {
+                      "version": "0.7.1",
+                      "from": "ms@0.7.1",
+                      "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+                    }
+                  }
+                },
+                "stream-consume": {
+                  "version": "0.1.0",
+                  "from": "stream-consume@^0.1.0",
+                  "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.0.tgz"
+                }
+              }
+            },
+            "qs": {
+              "version": "6.2.0",
+              "from": "qs@^6.1.0",
+              "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.0.tgz"
+            }
+          }
+        },
+        "json-stringify-safe": {
+          "version": "5.0.1",
+          "from": "json-stringify-safe@^5.0.1",
+          "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
+        }
+      }
+    },
+    "cookie": {
+      "version": "0.2.4",
+      "from": "cookie@^0.2.3",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.2.4.tgz"
+    },
+    "cookie-parser": {
+      "version": "1.3.5",
+      "from": "cookie-parser@1.3.5",
+      "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.3.5.tgz",
+      "dependencies": {
+        "cookie": {
+          "version": "0.1.3",
+          "from": "cookie@0.1.3",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz"
+        },
+        "cookie-signature": {
+          "version": "1.0.6",
+          "from": "cookie-signature@1.0.6",
+          "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
+        }
+      }
+    },
+    "csurf": {
+      "version": "1.9.0",
+      "from": "csurf@^1.8.3",
+      "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.9.0.tgz",
+      "dependencies": {
+        "cookie": {
+          "version": "0.3.1",
+          "from": "cookie@0.3.1",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz"
+        },
+        "cookie-signature": {
+          "version": "1.0.6",
+          "from": "cookie-signature@1.0.6",
+          "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
+        },
+        "csrf": {
+          "version": "3.0.3",
+          "from": "csrf@~3.0.3",
+          "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.3.tgz",
+          "dependencies": {
+            "base64-url": {
+              "version": "1.2.2",
+              "from": "base64-url@1.2.2",
+              "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-1.2.2.tgz"
+            },
+            "rndm": {
+              "version": "1.2.0",
+              "from": "rndm@1.2.0",
+              "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz"
+            },
+            "tsscmp": {
+              "version": "1.0.5",
+              "from": "tsscmp@1.0.5",
+              "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz"
+            },
+            "uid-safe": {
+              "version": "2.1.1",
+              "from": "uid-safe@2.1.1",
+              "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.1.tgz",
+              "dependencies": {
+                "random-bytes": {
+                  "version": "1.0.0",
+                  "from": "random-bytes@~1.0.0",
+                  "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz"
+                }
+              }
+            }
+          }
+        },
+        "http-errors": {
+          "version": "1.5.0",
+          "from": "http-errors@~1.5.0",
+          "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.0.tgz",
+          "dependencies": {
+            "inherits": {
+              "version": "2.0.1",
+              "from": "inherits@2.0.1",
+              "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+            },
+            "setprototypeof": {
+              "version": "1.0.1",
+              "from": "setprototypeof@1.0.1",
+              "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.1.tgz"
+            },
+            "statuses": {
+              "version": "1.3.0",
+              "from": "statuses@>= 1.3.0 < 2",
+              "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz"
+            }
+          }
+        }
+      }
+    },
+    "dateformat": {
+      "version": "1.0.4-1.2.3",
+      "from": "dateformat@1.0.4-1.2.3",
+      "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.4-1.2.3.tgz"
+    },
+    "express": {
+      "version": "4.13.0",
+      "from": "express@4.13.0",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.13.0.tgz",
+      "dependencies": {
+        "accepts": {
+          "version": "1.2.13",
+          "from": "accepts@~1.2.9",
+          "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz",
+          "dependencies": {
+            "mime-types": {
+              "version": "2.1.11",
+              "from": "mime-types@~2.1.11",
+              "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz",
+              "dependencies": {
+                "mime-db": {
+                  "version": "1.23.0",
+                  "from": "mime-db@~1.23.0",
+                  "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz"
+                }
+              }
+            },
+            "negotiator": {
+              "version": "0.5.3",
+              "from": "negotiator@0.5.3",
+              "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz"
+            }
+          }
+        },
+        "array-flatten": {
+          "version": "1.1.0",
+          "from": "array-flatten@1.1.0",
+          "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.0.tgz"
+        },
+        "content-disposition": {
+          "version": "0.5.0",
+          "from": "content-disposition@0.5.0",
+          "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz"
+        },
+        "content-type": {
+          "version": "1.0.2",
+          "from": "content-type@~1.0.1",
+          "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz"
+        },
+        "cookie": {
+          "version": "0.1.3",
+          "from": "cookie@0.1.3",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz"
+        },
+        "cookie-signature": {
+          "version": "1.0.6",
+          "from": "cookie-signature@1.0.6",
+          "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
+        },
+        "debug": {
+          "version": "2.2.0",
+          "from": "debug@~2.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+          "dependencies": {
+            "ms": {
+              "version": "0.7.1",
+              "from": "ms@0.7.1",
+              "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+            }
+          }
+        },
+        "depd": {
+          "version": "1.0.1",
+          "from": "depd@~1.0.1",
+          "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
+        },
+        "escape-html": {
+          "version": "1.0.2",
+          "from": "escape-html@1.0.2",
+          "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz"
+        },
+        "etag": {
+          "version": "1.7.0",
+          "from": "etag@~1.7.0",
+          "resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz"
+        },
+        "finalhandler": {
+          "version": "0.4.0",
+          "from": "finalhandler@0.4.0",
+          "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz",
+          "dependencies": {
+            "unpipe": {
+              "version": "1.0.0",
+              "from": "unpipe@~1.0.0",
+              "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
+            }
+          }
+        },
+        "fresh": {
+          "version": "0.3.0",
+          "from": "fresh@0.3.0",
+          "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.3.0.tgz"
+        },
+        "merge-descriptors": {
+          "version": "1.0.0",
+          "from": "merge-descriptors@1.0.0",
+          "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz"
+        },
+        "methods": {
+          "version": "1.1.2",
+          "from": "methods@~1.1.1",
+          "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
+        },
+        "on-finished": {
+          "version": "2.3.0",
+          "from": "on-finished@~2.3.0",
+          "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+          "dependencies": {
+            "ee-first": {
+              "version": "1.1.1",
+              "from": "ee-first@1.1.1",
+              "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
+            }
+          }
+        },
+        "parseurl": {
+          "version": "1.3.1",
+          "from": "parseurl@~1.3.0",
+          "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz"
+        },
+        "path-to-regexp": {
+          "version": "0.1.6",
+          "from": "path-to-regexp@0.1.6",
+          "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.6.tgz"
+        },
+        "proxy-addr": {
+          "version": "1.0.10",
+          "from": "proxy-addr@~1.0.8",
+          "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz",
+          "dependencies": {
+            "forwarded": {
+              "version": "0.1.0",
+              "from": "forwarded@~0.1.0",
+              "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz"
+            },
+            "ipaddr.js": {
+              "version": "1.0.5",
+              "from": "ipaddr.js@1.0.5",
+              "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz"
+            }
+          }
+        },
+        "qs": {
+          "version": "2.4.2",
+          "from": "qs@2.4.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-2.4.2.tgz"
+        },
+        "range-parser": {
+          "version": "1.0.3",
+          "from": "range-parser@~1.0.2",
+          "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz"
+        },
+        "send": {
+          "version": "0.13.0",
+          "from": "send@0.13.0",
+          "resolved": "https://registry.npmjs.org/send/-/send-0.13.0.tgz",
+          "dependencies": {
+            "destroy": {
+              "version": "1.0.3",
+              "from": "destroy@1.0.3",
+              "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz"
+            },
+            "http-errors": {
+              "version": "1.3.1",
+              "from": "http-errors@~1.3.1",
+              "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz",
+              "dependencies": {
+                "inherits": {
+                  "version": "2.0.1",
+                  "from": "inherits@~2.0.1",
+                  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                }
+              }
+            },
+            "mime": {
+              "version": "1.3.4",
+              "from": "mime@1.3.4",
+              "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz"
+            },
+            "ms": {
+              "version": "0.7.1",
+              "from": "ms@0.7.1",
+              "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+            },
+            "statuses": {
+              "version": "1.2.1",
+              "from": "statuses@~1.2.1",
+              "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
+            }
+          }
+        },
+        "serve-static": {
+          "version": "1.10.3",
+          "from": "serve-static@~1.10.0",
+          "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.10.3.tgz",
+          "dependencies": {
+            "escape-html": {
+              "version": "1.0.3",
+              "from": "escape-html@~1.0.3",
+              "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz"
+            },
+            "send": {
+              "version": "0.13.2",
+              "from": "send@0.13.2",
+              "resolved": "https://registry.npmjs.org/send/-/send-0.13.2.tgz",
+              "dependencies": {
+                "depd": {
+                  "version": "1.1.0",
+                  "from": "depd@~1.1.0",
+                  "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz"
+                },
+                "destroy": {
+                  "version": "1.0.4",
+                  "from": "destroy@~1.0.4",
+                  "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz"
+                },
+                "http-errors": {
+                  "version": "1.3.1",
+                  "from": "http-errors@~1.3.1",
+                  "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz",
+                  "dependencies": {
+                    "inherits": {
+                      "version": "2.0.1",
+                      "from": "inherits@~2.0.1",
+                      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                    }
+                  }
+                },
+                "mime": {
+                  "version": "1.3.4",
+                  "from": "mime@1.3.4",
+                  "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz"
+                },
+                "ms": {
+                  "version": "0.7.1",
+                  "from": "ms@0.7.1",
+                  "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+                },
+                "statuses": {
+                  "version": "1.2.1",
+                  "from": "statuses@~1.2.1",
+                  "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
+                }
+              }
+            }
+          }
+        },
+        "type-is": {
+          "version": "1.6.13",
+          "from": "type-is@~1.6.3",
+          "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.13.tgz",
+          "dependencies": {
+            "media-typer": {
+              "version": "0.3.0",
+              "from": "media-typer@0.3.0",
+              "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
+            },
+            "mime-types": {
+              "version": "2.1.11",
+              "from": "mime-types@~2.1.11",
+              "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz",
+              "dependencies": {
+                "mime-db": {
+                  "version": "1.23.0",
+                  "from": "mime-db@~1.23.0",
+                  "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz"
+                }
+              }
+            }
+          }
+        },
+        "vary": {
+          "version": "1.0.1",
+          "from": "vary@~1.0.0",
+          "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz"
+        },
+        "utils-merge": {
+          "version": "1.0.0",
+          "from": "utils-merge@1.0.0",
+          "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz"
+        }
+      }
+    },
+    "express-session": {
+      "version": "1.11.3",
+      "from": "express-session@1.11.3",
+      "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.11.3.tgz",
+      "dependencies": {
+        "cookie": {
+          "version": "0.1.3",
+          "from": "cookie@0.1.3",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz"
+        },
+        "cookie-signature": {
+          "version": "1.0.6",
+          "from": "cookie-signature@1.0.6",
+          "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
+        },
+        "crc": {
+          "version": "3.3.0",
+          "from": "crc@3.3.0",
+          "resolved": "https://registry.npmjs.org/crc/-/crc-3.3.0.tgz"
+        },
+        "debug": {
+          "version": "2.2.0",
+          "from": "debug@~2.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+          "dependencies": {
+            "ms": {
+              "version": "0.7.1",
+              "from": "ms@0.7.1",
+              "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+            }
+          }
+        },
+        "depd": {
+          "version": "1.0.1",
+          "from": "depd@~1.0.1",
+          "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
+        },
+        "on-headers": {
+          "version": "1.0.1",
+          "from": "on-headers@~1.0.0",
+          "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz"
+        },
+        "parseurl": {
+          "version": "1.3.1",
+          "from": "parseurl@~1.3.0",
+          "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz"
+        },
+        "uid-safe": {
+          "version": "2.0.0",
+          "from": "uid-safe@~2.0.0",
+          "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.0.0.tgz",
+          "dependencies": {
+            "base64-url": {
+              "version": "1.2.1",
+              "from": "base64-url@1.2.1",
+              "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-1.2.1.tgz"
+            }
+          }
+        },
+        "utils-merge": {
+          "version": "1.0.0",
+          "from": "utils-merge@1.0.0",
+          "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz"
+        }
+      }
+    },
+    "grunt": {
+      "version": "0.4.5",
+      "from": "grunt@^0.4.5",
+      "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz",
+      "dependencies": {
+        "async": {
+          "version": "0.1.22",
+          "from": "async@~0.1.22",
+          "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz"
+        },
+        "coffee-script": {
+          "version": "1.3.3",
+          "from": "coffee-script@~1.3.3",
+          "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz"
+        },
+        "colors": {
+          "version": "0.6.2",
+          "from": "colors@~0.6.2",
+          "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz"
+        },
+        "dateformat": {
+          "version": "1.0.2-1.2.3",
+          "from": "dateformat@1.0.2-1.2.3",
+          "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz"
+        },
+        "eventemitter2": {
+          "version": "0.4.14",
+          "from": "eventemitter2@~0.4.13",
+          "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz"
+        },
+        "findup-sync": {
+          "version": "0.1.3",
+          "from": "findup-sync@~0.1.2",
+          "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz",
+          "dependencies": {
+            "glob": {
+              "version": "3.2.11",
+              "from": "glob@~3.2.9",
+              "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
+              "dependencies": {
+                "inherits": {
+                  "version": "2.0.1",
+                  "from": "inherits@2",
+                  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                },
+                "minimatch": {
+                  "version": "0.3.0",
+                  "from": "minimatch@0.3",
+                  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz",
+                  "dependencies": {
+                    "lru-cache": {
+                      "version": "2.7.3",
+                      "from": "lru-cache@2",
+                      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
+                    },
+                    "sigmund": {
+                      "version": "1.0.1",
+                      "from": "sigmund@~1.0.0",
+                      "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
+                    }
+                  }
+                }
+              }
+            },
+            "lodash": {
+              "version": "2.4.2",
+              "from": "lodash@~2.4.1",
+              "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
+            }
+          }
+        },
+        "glob": {
+          "version": "3.1.21",
+          "from": "glob@~3.1.21",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz",
+          "dependencies": {
+            "graceful-fs": {
+              "version": "1.2.3",
+              "from": "graceful-fs@~1.2.0",
+              "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz"
+            },
+            "inherits": {
+              "version": "1.0.2",
+              "from": "inherits@1",
+              "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz"
+            }
+          }
+        },
+        "hooker": {
+          "version": "0.2.3",
+          "from": "hooker@~0.2.3",
+          "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz"
+        },
+        "iconv-lite": {
+          "version": "0.2.11",
+          "from": "iconv-lite@~0.2.11",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz"
+        },
+        "minimatch": {
+          "version": "0.2.14",
+          "from": "minimatch@~0.2.12",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
+          "dependencies": {
+            "lru-cache": {
+              "version": "2.7.3",
+              "from": "lru-cache@2",
+              "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
+            },
+            "sigmund": {
+              "version": "1.0.1",
+              "from": "sigmund@~1.0.0",
+              "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
+            }
+          }
+        },
+        "nopt": {
+          "version": "1.0.10",
+          "from": "nopt@~1.0.10",
+          "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+          "dependencies": {
+            "abbrev": {
+              "version": "1.0.7",
+              "from": "abbrev@1",
+              "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz"
+            }
+          }
+        },
+        "rimraf": {
+          "version": "2.2.8",
+          "from": "rimraf@~2.2.8",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz"
+        },
+        "lodash": {
+          "version": "0.9.2",
+          "from": "lodash@~0.9.2",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz"
+        },
+        "underscore.string": {
+          "version": "2.2.1",
+          "from": "underscore.string@~2.2.1",
+          "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz"
+        },
+        "which": {
+          "version": "1.0.9",
+          "from": "which@~1.0.5",
+          "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz"
+        },
+        "js-yaml": {
+          "version": "2.0.5",
+          "from": "js-yaml@~2.0.5",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz",
+          "dependencies": {
+            "argparse": {
+              "version": "0.1.16",
+              "from": "argparse@~ 0.1.11",
+              "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz",
+              "dependencies": {
+                "underscore": {
+                  "version": "1.7.0",
+                  "from": "underscore@~1.7.0",
+                  "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz"
+                },
+                "underscore.string": {
+                  "version": "2.4.0",
+                  "from": "underscore.string@~2.4.0",
+                  "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz"
+                }
+              }
+            },
+            "esprima": {
+              "version": "1.0.4",
+              "from": "esprima@~ 1.0.2",
+              "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz"
+            }
+          }
+        },
+        "exit": {
+          "version": "0.1.2",
+          "from": "exit@~0.1.1",
+          "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz"
+        },
+        "getobject": {
+          "version": "0.1.0",
+          "from": "getobject@~0.1.0",
+          "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz"
+        },
+        "grunt-legacy-util": {
+          "version": "0.2.0",
+          "from": "grunt-legacy-util@~0.2.0",
+          "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz"
+        },
+        "grunt-legacy-log": {
+          "version": "0.1.3",
+          "from": "grunt-legacy-log@~0.1.0",
+          "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz",
+          "dependencies": {
+            "grunt-legacy-log-utils": {
+              "version": "0.1.1",
+              "from": "grunt-legacy-log-utils@~0.1.1",
+              "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz"
+            },
+            "lodash": {
+              "version": "2.4.2",
+              "from": "lodash@~2.4.1",
+              "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
+            },
+            "underscore.string": {
+              "version": "2.3.3",
+              "from": "underscore.string@~2.3.3",
+              "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz"
+            }
+          }
+        }
+      }
+    },
+    "heapdump": {
+      "version": "0.3.7",
+      "from": "heapdump@^0.3.7",
+      "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.7.tgz"
+    },
+    "http-proxy": {
+      "version": "1.13.3",
+      "from": "http-proxy@^1.8.1",
+      "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.13.3.tgz",
+      "dependencies": {
+        "eventemitter3": {
+          "version": "1.2.0",
+          "from": "eventemitter3@1.x.x",
+          "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz"
+        },
+        "requires-port": {
+          "version": "1.0.0",
+          "from": "requires-port@1.x.x",
+          "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz"
+        }
+      }
+    },
+    "jade": {
+      "version": "1.3.1",
+      "from": "jade@~1.3.1",
+      "resolved": "https://registry.npmjs.org/jade/-/jade-1.3.1.tgz",
+      "dependencies": {
+        "commander": {
+          "version": "2.1.0",
+          "from": "commander@2.1.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz"
+        },
+        "mkdirp": {
+          "version": "0.3.5",
+          "from": "mkdirp@~0.3.5",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
+        },
+        "transformers": {
+          "version": "2.1.0",
+          "from": "transformers@2.1.0",
+          "resolved": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz",
+          "dependencies": {
+            "promise": {
+              "version": "2.0.0",
+              "from": "promise@~2.0",
+              "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz",
+              "dependencies": {
+                "is-promise": {
+                  "version": "1.0.1",
+                  "from": "is-promise@~1",
+                  "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz"
+                }
+              }
+            },
+            "css": {
+              "version": "1.0.8",
+              "from": "css@~1.0.8",
+              "resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz",
+              "dependencies": {
+                "css-parse": {
+                  "version": "1.0.4",
+                  "from": "css-parse@1.0.4",
+                  "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz"
+                },
+                "css-stringify": {
+                  "version": "1.0.5",
+                  "from": "css-stringify@1.0.5",
+                  "resolved": "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz"
+                }
+              }
+            },
+            "uglify-js": {
+              "version": "2.2.5",
+              "from": "uglify-js@~2.2.5",
+              "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz",
+              "dependencies": {
+                "source-map": {
+                  "version": "0.1.43",
+                  "from": "source-map@~0.1.7",
+                  "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
+                  "dependencies": {
+                    "amdefine": {
+                      "version": "1.0.0",
+                      "from": "amdefine@>=0.0.4",
+                      "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz"
+                    }
+                  }
+                },
+                "optimist": {
+                  "version": "0.3.7",
+                  "from": "optimist@~0.3.5",
+                  "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz",
+                  "dependencies": {
+                    "wordwrap": {
+                      "version": "0.0.3",
+                      "from": "wordwrap@~0.0.2",
+                      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz"
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+        "character-parser": {
+          "version": "1.2.0",
+          "from": "character-parser@1.2.0",
+          "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.0.tgz"
+        },
+        "monocle": {
+          "version": "1.1.51",
+          "from": "monocle@1.1.51",
+          "resolved": "https://registry.npmjs.org/monocle/-/monocle-1.1.51.tgz",
+          "dependencies": {
+            "readdirp": {
+              "version": "0.2.5",
+              "from": "readdirp@~0.2.3",
+              "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-0.2.5.tgz",
+              "dependencies": {
+                "minimatch": {
+                  "version": "3.0.0",
+                  "from": "minimatch@>=0.2.4",
+                  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz",
+                  "dependencies": {
+                    "brace-expansion": {
+                      "version": "1.1.4",
+                      "from": "brace-expansion@^1.0.0",
+                      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz",
+                      "dependencies": {
+                        "balanced-match": {
+                          "version": "0.4.1",
+                          "from": "balanced-match@^0.4.1",
+                          "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
+                        },
+                        "concat-map": {
+                          "version": "0.0.1",
+                          "from": "concat-map@0.0.1",
+                          "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+        "with": {
+          "version": "3.0.1",
+          "from": "with@~3.0.0",
+          "resolved": "https://registry.npmjs.org/with/-/with-3.0.1.tgz",
+          "dependencies": {
+            "uglify-js": {
+              "version": "2.4.24",
+              "from": "uglify-js@~2.4.12",
+              "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz",
+              "dependencies": {
+                "async": {
+                  "version": "0.2.10",
+                  "from": "async@~0.2.6",
+                  "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
+                },
+                "source-map": {
+                  "version": "0.1.34",
+                  "from": "source-map@0.1.34",
+                  "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz",
+                  "dependencies": {
+                    "amdefine": {
+                      "version": "1.0.0",
+                      "from": "amdefine@>=0.0.4",
+                      "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz"
+                    }
+                  }
+                },
+                "uglify-to-browserify": {
+                  "version": "1.0.2",
+                  "from": "uglify-to-browserify@~1.0.0",
+                  "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz"
+                },
+                "yargs": {
+                  "version": "3.5.4",
+                  "from": "yargs@~3.5.4",
+                  "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz",
+                  "dependencies": {
+                    "camelcase": {
+                      "version": "1.2.1",
+                      "from": "camelcase@^1.0.2",
+                      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz"
+                    },
+                    "decamelize": {
+                      "version": "1.2.0",
+                      "from": "decamelize@^1.0.0",
+                      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
+                    },
+                    "window-size": {
+                      "version": "0.1.0",
+                      "from": "window-size@0.1.0",
+                      "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz"
+                    },
+                    "wordwrap": {
+                      "version": "0.0.2",
+                      "from": "wordwrap@0.0.2",
+                      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz"
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+        "constantinople": {
+          "version": "2.0.1",
+          "from": "constantinople@~2.0.0",
+          "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-2.0.1.tgz",
+          "dependencies": {
+            "uglify-js": {
+              "version": "2.4.24",
+              "from": "uglify-js@~2.4.0",
+              "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz",
+              "dependencies": {
+                "async": {
+                  "version": "0.2.10",
+                  "from": "async@~0.2.6",
+                  "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
+                },
+                "source-map": {
+                  "version": "0.1.34",
+                  "from": "source-map@0.1.34",
+                  "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz",
+                  "dependencies": {
+                    "amdefine": {
+                      "version": "1.0.0",
+                      "from": "amdefine@>=0.0.4",
+                      "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz"
+                    }
+                  }
+                },
+                "uglify-to-browserify": {
+                  "version": "1.0.2",
+                  "from": "uglify-to-browserify@~1.0.0",
+                  "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz"
+                },
+                "yargs": {
+                  "version": "3.5.4",
+                  "from": "yargs@~3.5.4",
+                  "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz",
+                  "dependencies": {
+                    "camelcase": {
+                      "version": "1.2.1",
+                      "from": "camelcase@^1.0.2",
+                      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz"
+                    },
+                    "decamelize": {
+                      "version": "1.2.0",
+                      "from": "decamelize@^1.0.0",
+                      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
+                    },
+                    "window-size": {
+                      "version": "0.1.0",
+                      "from": "window-size@0.1.0",
+                      "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz"
+                    },
+                    "wordwrap": {
+                      "version": "0.0.2",
+                      "from": "wordwrap@0.0.2",
+                      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz"
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "ldapjs": {
+      "version": "1.0.0",
+      "from": "ldapjs@^1.0.0",
+      "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.0.tgz",
+      "dependencies": {
+        "asn1": {
+          "version": "0.2.3",
+          "from": "asn1@0.2.3",
+          "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz"
+        },
+        "assert-plus": {
+          "version": "0.1.5",
+          "from": "assert-plus@0.1.5",
+          "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz"
+        },
+        "bunyan": {
+          "version": "1.5.1",
+          "from": "bunyan@1.5.1",
+          "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz",
+          "dependencies": {
+            "mv": {
+              "version": "2.1.1",
+              "from": "mv@~2",
+              "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
+              "dependencies": {
+                "mkdirp": {
+                  "version": "0.5.1",
+                  "from": "mkdirp@~0.5.1",
+                  "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+                  "dependencies": {
+                    "minimist": {
+                      "version": "0.0.8",
+                      "from": "minimist@0.0.8",
+                      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
+                    }
+                  }
+                },
+                "ncp": {
+                  "version": "2.0.0",
+                  "from": "ncp@~2.0.0",
+                  "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz"
+                },
+                "rimraf": {
+                  "version": "2.4.5",
+                  "from": "rimraf@~2.4.0",
+                  "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
+                  "dependencies": {
+                    "glob": {
+                      "version": "6.0.4",
+                      "from": "glob@^6.0.1",
+                      "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
+                      "dependencies": {
+                        "inflight": {
+                          "version": "1.0.5",
+                          "from": "inflight@^1.0.4",
+                          "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz",
+                          "dependencies": {
+                            "wrappy": {
+                              "version": "1.0.2",
+                              "from": "wrappy@1",
+                              "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+                            }
+                          }
+                        },
+                        "inherits": {
+                          "version": "2.0.1",
+                          "from": "inherits@2",
+                          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                        },
+                        "minimatch": {
+                          "version": "3.0.0",
+                          "from": "minimatch@2 || 3",
+                          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz",
+                          "dependencies": {
+                            "brace-expansion": {
+                              "version": "1.1.4",
+                              "from": "brace-expansion@^1.0.0",
+                              "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz",
+                              "dependencies": {
+                                "balanced-match": {
+                                  "version": "0.4.1",
+                                  "from": "balanced-match@^0.4.1",
+                                  "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
+                                },
+                                "concat-map": {
+                                  "version": "0.0.1",
+                                  "from": "concat-map@0.0.1",
+                                  "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+                                }
+                              }
+                            }
+                          }
+                        },
+                        "path-is-absolute": {
+                          "version": "1.0.0",
+                          "from": "path-is-absolute@^1.0.0",
+                          "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz"
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            },
+            "safe-json-stringify": {
+              "version": "1.0.3",
+              "from": "safe-json-stringify@~1",
+              "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.3.tgz"
+            }
+          }
+        },
+        "dashdash": {
+          "version": "1.10.1",
+          "from": "dashdash@1.10.1",
+          "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.10.1.tgz"
+        },
+        "backoff": {
+          "version": "2.4.1",
+          "from": "backoff@2.4.1",
+          "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.4.1.tgz",
+          "dependencies": {
+            "precond": {
+              "version": "0.2.3",
+              "from": "precond@0.2",
+              "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz"
+            }
+          }
+        },
+        "ldap-filter": {
+          "version": "0.2.2",
+          "from": "ldap-filter@0.2.2",
+          "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz"
+        },
+        "once": {
+          "version": "1.3.2",
+          "from": "once@1.3.2",
+          "resolved": "https://registry.npmjs.org/once/-/once-1.3.2.tgz",
+          "dependencies": {
+            "wrappy": {
+              "version": "1.0.2",
+              "from": "wrappy@1",
+              "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+            }
+          }
+        },
+        "vasync": {
+          "version": "1.6.3",
+          "from": "vasync@1.6.3",
+          "resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.3.tgz"
+        },
+        "verror": {
+          "version": "1.6.0",
+          "from": "verror@1.6.0",
+          "resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
+          "dependencies": {
+            "extsprintf": {
+              "version": "1.2.0",
+              "from": "extsprintf@1.2.0",
+              "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz"
+            }
+          }
+        },
+        "dtrace-provider": {
+          "version": "0.6.0",
+          "from": "dtrace-provider@~0.6",
+          "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz",
+          "dependencies": {
+            "nan": {
+              "version": "2.3.5",
+              "from": "nan@^2.0.8",
+              "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz"
+            }
+          }
+        }
+      }
+    },
+    "lodash": {
+      "version": "4.13.1",
+      "from": "lodash@^4.13.1",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.13.1.tgz"
+    },
+    "logger-sharelatex": {
+      "version": "1.3.1",
+      "from": "logger-sharelatex@git+https://github.com/sharelatex/logger-sharelatex.git#v1.3.1",
+      "resolved": "git+https://github.com/sharelatex/logger-sharelatex.git#bf413ec621a000cf0e08c939de38d5e24541a08c",
+      "dependencies": {
+        "bunyan": {
+          "version": "1.5.1",
+          "from": "bunyan@1.5.1",
+          "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz",
+          "dependencies": {
+            "dtrace-provider": {
+              "version": "0.6.0",
+              "from": "dtrace-provider@~0.6",
+              "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz",
+              "dependencies": {
+                "nan": {
+                  "version": "2.3.5",
+                  "from": "nan@^2.0.8",
+                  "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz"
+                }
+              }
+            },
+            "mv": {
+              "version": "2.1.1",
+              "from": "mv@~2",
+              "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
+              "dependencies": {
+                "mkdirp": {
+                  "version": "0.5.1",
+                  "from": "mkdirp@~0.5.1",
+                  "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+                  "dependencies": {
+                    "minimist": {
+                      "version": "0.0.8",
+                      "from": "minimist@0.0.8",
+                      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
+                    }
+                  }
+                },
+                "ncp": {
+                  "version": "2.0.0",
+                  "from": "ncp@~2.0.0",
+                  "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz"
+                },
+                "rimraf": {
+                  "version": "2.4.5",
+                  "from": "rimraf@~2.4.0",
+                  "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
+                  "dependencies": {
+                    "glob": {
+                      "version": "6.0.4",
+                      "from": "glob@^6.0.1",
+                      "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
+                      "dependencies": {
+                        "inflight": {
+                          "version": "1.0.5",
+                          "from": "inflight@^1.0.4",
+                          "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz",
+                          "dependencies": {
+                            "wrappy": {
+                              "version": "1.0.2",
+                              "from": "wrappy@1",
+                              "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+                            }
+                          }
+                        },
+                        "inherits": {
+                          "version": "2.0.1",
+                          "from": "inherits@2",
+                          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                        },
+                        "minimatch": {
+                          "version": "3.0.0",
+                          "from": "minimatch@2 || 3",
+                          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz",
+                          "dependencies": {
+                            "brace-expansion": {
+                              "version": "1.1.4",
+                              "from": "brace-expansion@^1.0.0",
+                              "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz",
+                              "dependencies": {
+                                "balanced-match": {
+                                  "version": "0.4.1",
+                                  "from": "balanced-match@^0.4.1",
+                                  "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
+                                },
+                                "concat-map": {
+                                  "version": "0.0.1",
+                                  "from": "concat-map@0.0.1",
+                                  "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+                                }
+                              }
+                            }
+                          }
+                        },
+                        "once": {
+                          "version": "1.3.3",
+                          "from": "once@^1.3.0",
+                          "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz",
+                          "dependencies": {
+                            "wrappy": {
+                              "version": "1.0.2",
+                              "from": "wrappy@1",
+                              "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+                            }
+                          }
+                        },
+                        "path-is-absolute": {
+                          "version": "1.0.0",
+                          "from": "path-is-absolute@^1.0.0",
+                          "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz"
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            },
+            "safe-json-stringify": {
+              "version": "1.0.3",
+              "from": "safe-json-stringify@~1",
+              "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.3.tgz"
+            }
+          }
+        },
+        "coffee-script": {
+          "version": "1.4.0",
+          "from": "coffee-script@1.4.0",
+          "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz"
+        },
+        "raven": {
+          "version": "0.8.1",
+          "from": "raven@^0.8.0",
+          "resolved": "https://registry.npmjs.org/raven/-/raven-0.8.1.tgz",
+          "dependencies": {
+            "cookie": {
+              "version": "0.1.0",
+              "from": "cookie@0.1.0",
+              "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz"
+            },
+            "lsmod": {
+              "version": "0.0.3",
+              "from": "lsmod@~0.0.3",
+              "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-0.0.3.tgz"
+            },
+            "stack-trace": {
+              "version": "0.0.7",
+              "from": "stack-trace@0.0.7",
+              "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.7.tgz"
+            }
+          }
+        }
+      }
+    },
+    "lynx": {
+      "version": "0.1.1",
+      "from": "lynx@0.1.1",
+      "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz",
+      "dependencies": {
+        "mersenne": {
+          "version": "0.0.3",
+          "from": "mersenne@~0.0.3",
+          "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.3.tgz"
+        },
+        "statsd-parser": {
+          "version": "0.0.4",
+          "from": "statsd-parser@~0.0.4",
+          "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz"
+        }
+      }
+    },
+    "marked": {
+      "version": "0.3.5",
+      "from": "marked@^0.3.5",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.5.tgz"
+    },
+    "method-override": {
+      "version": "2.3.6",
+      "from": "method-override@^2.3.3",
+      "resolved": "https://registry.npmjs.org/method-override/-/method-override-2.3.6.tgz",
+      "dependencies": {
+        "debug": {
+          "version": "2.2.0",
+          "from": "debug@~2.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+          "dependencies": {
+            "ms": {
+              "version": "0.7.1",
+              "from": "ms@0.7.1",
+              "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+            }
+          }
+        },
+        "methods": {
+          "version": "1.1.2",
+          "from": "methods@~1.1.2",
+          "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
+        },
+        "parseurl": {
+          "version": "1.3.1",
+          "from": "parseurl@~1.3.1",
+          "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz"
+        },
+        "vary": {
+          "version": "1.1.0",
+          "from": "vary@~1.1.0",
+          "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.0.tgz"
+        }
+      }
+    },
+    "metrics-sharelatex": {
+      "version": "1.3.0",
+      "from": "metrics-sharelatex@git+https://github.com/sharelatex/metrics-sharelatex.git#v1.3.0",
+      "resolved": "git+https://github.com/sharelatex/metrics-sharelatex.git#080c4aeb696edcd5d6d86f202f2c528f0661d7a6",
+      "dependencies": {
+        "coffee-script": {
+          "version": "1.6.0",
+          "from": "coffee-script@1.6.0",
+          "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz"
+        }
+      }
+    },
+    "mimelib": {
+      "version": "0.2.14",
+      "from": "mimelib@0.2.14",
+      "resolved": "https://registry.npmjs.org/mimelib/-/mimelib-0.2.14.tgz",
+      "dependencies": {
+        "encoding": {
+          "version": "0.1.12",
+          "from": "encoding@~0.1",
+          "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
+          "dependencies": {
+            "iconv-lite": {
+              "version": "0.4.13",
+              "from": "iconv-lite@~0.4.13",
+              "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz"
+            }
+          }
+        },
+        "addressparser": {
+          "version": "0.2.1",
+          "from": "addressparser@~0.2.0",
+          "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-0.2.1.tgz"
+        }
+      }
+    },
+    "mocha": {
+      "version": "1.17.1",
+      "from": "mocha@1.17.1",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz",
+      "dependencies": {
+        "commander": {
+          "version": "2.0.0",
+          "from": "commander@2.0.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz"
+        },
+        "growl": {
+          "version": "1.7.0",
+          "from": "growl@1.7.x",
+          "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz"
+        },
+        "jade": {
+          "version": "0.26.3",
+          "from": "jade@0.26.3",
+          "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz",
+          "dependencies": {
+            "commander": {
+              "version": "0.6.1",
+              "from": "commander@0.6.1",
+              "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz"
+            },
+            "mkdirp": {
+              "version": "0.3.0",
+              "from": "mkdirp@0.3.0",
+              "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz"
+            }
+          }
+        },
+        "diff": {
+          "version": "1.0.7",
+          "from": "diff@1.0.7",
+          "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz"
+        },
+        "debug": {
+          "version": "2.2.0",
+          "from": "debug@*",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+          "dependencies": {
+            "ms": {
+              "version": "0.7.1",
+              "from": "ms@0.7.1",
+              "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+            }
+          }
+        },
+        "mkdirp": {
+          "version": "0.3.5",
+          "from": "mkdirp@0.3.5",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
+        },
+        "glob": {
+          "version": "3.2.3",
+          "from": "glob@3.2.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz",
+          "dependencies": {
+            "minimatch": {
+              "version": "0.2.14",
+              "from": "minimatch@~0.2.11",
+              "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
+              "dependencies": {
+                "lru-cache": {
+                  "version": "2.7.3",
+                  "from": "lru-cache@2",
+                  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
+                },
+                "sigmund": {
+                  "version": "1.0.1",
+                  "from": "sigmund@~1.0.0",
+                  "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
+                }
+              }
+            },
+            "graceful-fs": {
+              "version": "2.0.3",
+              "from": "graceful-fs@~2.0.0",
+              "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz"
+            },
+            "inherits": {
+              "version": "2.0.1",
+              "from": "inherits@2",
+              "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+            }
+          }
+        }
+      }
+    },
+    "mongojs": {
+      "version": "0.18.2",
+      "from": "mongojs@0.18.2",
+      "resolved": "https://registry.npmjs.org/mongojs/-/mongojs-0.18.2.tgz",
+      "dependencies": {
+        "thunky": {
+          "version": "0.1.0",
+          "from": "thunky@~0.1.0",
+          "resolved": "https://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz"
+        },
+        "readable-stream": {
+          "version": "1.1.14",
+          "from": "readable-stream@1.1.x",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+          "dependencies": {
+            "core-util-is": {
+              "version": "1.0.2",
+              "from": "core-util-is@~1.0.0",
+              "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+            },
+            "isarray": {
+              "version": "0.0.1",
+              "from": "isarray@0.0.1",
+              "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+            },
+            "string_decoder": {
+              "version": "0.10.31",
+              "from": "string_decoder@~0.10.x",
+              "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+            },
+            "inherits": {
+              "version": "2.0.1",
+              "from": "inherits@~2.0.1",
+              "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+            }
+          }
+        },
+        "mongodb": {
+          "version": "1.4.32",
+          "from": "mongodb@1.4.32",
+          "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-1.4.32.tgz",
+          "dependencies": {
+            "bson": {
+              "version": "0.2.22",
+              "from": "bson@~0.2",
+              "resolved": "https://registry.npmjs.org/bson/-/bson-0.2.22.tgz",
+              "dependencies": {
+                "nan": {
+                  "version": "1.8.4",
+                  "from": "nan@~1.8",
+                  "resolved": "https://registry.npmjs.org/nan/-/nan-1.8.4.tgz"
+                }
+              }
+            },
+            "kerberos": {
+              "version": "0.0.9",
+              "from": "kerberos@0.0.9",
+              "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.9.tgz",
+              "dependencies": {
+                "nan": {
+                  "version": "1.6.2",
+                  "from": "nan@1.6.2",
+                  "resolved": "https://registry.npmjs.org/nan/-/nan-1.6.2.tgz"
+                }
+              }
+            },
+            "readable-stream": {
+              "version": "2.1.4",
+              "from": "readable-stream@latest",
+              "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz",
+              "dependencies": {
+                "buffer-shims": {
+                  "version": "1.0.0",
+                  "from": "buffer-shims@^1.0.0",
+                  "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz"
+                },
+                "core-util-is": {
+                  "version": "1.0.2",
+                  "from": "core-util-is@~1.0.0",
+                  "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+                },
+                "inherits": {
+                  "version": "2.0.1",
+                  "from": "inherits@~2.0.1",
+                  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                },
+                "isarray": {
+                  "version": "1.0.0",
+                  "from": "isarray@~1.0.0",
+                  "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
+                },
+                "process-nextick-args": {
+                  "version": "1.0.7",
+                  "from": "process-nextick-args@~1.0.6",
+                  "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz"
+                },
+                "string_decoder": {
+                  "version": "0.10.31",
+                  "from": "string_decoder@~0.10.x",
+                  "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+                },
+                "util-deprecate": {
+                  "version": "1.0.2",
+                  "from": "util-deprecate@~1.0.1",
+                  "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "mongoose": {
+      "version": "4.1.0",
+      "from": "mongoose@4.1.0",
+      "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.1.0.tgz",
+      "dependencies": {
+        "async": {
+          "version": "0.9.0",
+          "from": "async@0.9.0",
+          "resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz"
+        },
+        "bson": {
+          "version": "0.3.2",
+          "from": "bson@~0.3",
+          "resolved": "https://registry.npmjs.org/bson/-/bson-0.3.2.tgz",
+          "dependencies": {
+            "bson-ext": {
+              "version": "0.1.13",
+              "from": "bson-ext@~0.1",
+              "resolved": "https://registry.npmjs.org/bson-ext/-/bson-ext-0.1.13.tgz",
+              "dependencies": {
+                "bindings": {
+                  "version": "1.2.1",
+                  "from": "bindings@^1.2.1",
+                  "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz"
+                },
+                "nan": {
+                  "version": "2.0.9",
+                  "from": "nan@~2.0.9",
+                  "resolved": "https://registry.npmjs.org/nan/-/nan-2.0.9.tgz"
+                }
+              }
+            }
+          }
+        },
+        "hooks-fixed": {
+          "version": "1.1.0",
+          "from": "hooks-fixed@1.1.0",
+          "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-1.1.0.tgz"
+        },
+        "kareem": {
+          "version": "1.0.1",
+          "from": "kareem@1.0.1",
+          "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.0.1.tgz"
+        },
+        "mongodb": {
+          "version": "2.0.34",
+          "from": "mongodb@2.0.34",
+          "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.0.34.tgz",
+          "dependencies": {
+            "mongodb-core": {
+              "version": "1.2.0",
+              "from": "mongodb-core@1.2.0",
+              "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-1.2.0.tgz",
+              "dependencies": {
+                "bson": {
+                  "version": "0.4.23",
+                  "from": "bson@~0.4",
+                  "resolved": "https://registry.npmjs.org/bson/-/bson-0.4.23.tgz"
+                },
+                "kerberos": {
+                  "version": "0.0.21",
+                  "from": "kerberos@~0.0",
+                  "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.21.tgz",
+                  "dependencies": {
+                    "nan": {
+                      "version": "2.3.5",
+                      "from": "nan@~2.3",
+                      "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz"
+                    }
+                  }
+                }
+              }
+            },
+            "readable-stream": {
+              "version": "1.0.31",
+              "from": "readable-stream@1.0.31",
+              "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz",
+              "dependencies": {
+                "core-util-is": {
+                  "version": "1.0.2",
+                  "from": "core-util-is@~1.0.0",
+                  "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+                },
+                "isarray": {
+                  "version": "0.0.1",
+                  "from": "isarray@0.0.1",
+                  "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+                },
+                "string_decoder": {
+                  "version": "0.10.31",
+                  "from": "string_decoder@~0.10.x",
+                  "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+                },
+                "inherits": {
+                  "version": "2.0.1",
+                  "from": "inherits@~2.0.1",
+                  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                }
+              }
+            }
+          }
+        },
+        "mpath": {
+          "version": "0.1.1",
+          "from": "mpath@0.1.1",
+          "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.1.1.tgz"
+        },
+        "mpromise": {
+          "version": "0.5.4",
+          "from": "mpromise@0.5.4",
+          "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.4.tgz"
+        },
+        "mquery": {
+          "version": "1.6.1",
+          "from": "mquery@1.6.1",
+          "resolved": "https://registry.npmjs.org/mquery/-/mquery-1.6.1.tgz",
+          "dependencies": {
+            "bluebird": {
+              "version": "2.9.26",
+              "from": "bluebird@2.9.26",
+              "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.26.tgz"
+            },
+            "debug": {
+              "version": "2.2.0",
+              "from": "debug@2.2.0",
+              "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+              "dependencies": {
+                "ms": {
+                  "version": "0.7.1",
+                  "from": "ms@0.7.1",
+                  "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+                }
+              }
+            }
+          }
+        },
+        "ms": {
+          "version": "0.1.0",
+          "from": "ms@0.1.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.1.0.tgz"
+        },
+        "muri": {
+          "version": "1.0.0",
+          "from": "muri@1.0.0",
+          "resolved": "https://registry.npmjs.org/muri/-/muri-1.0.0.tgz"
+        },
+        "regexp-clone": {
+          "version": "0.0.1",
+          "from": "regexp-clone@0.0.1",
+          "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz"
+        },
+        "sliced": {
+          "version": "0.0.5",
+          "from": "sliced@0.0.5",
+          "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz"
+        }
+      }
+    },
+    "multer": {
+      "version": "0.1.8",
+      "from": "multer@^0.1.8",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-0.1.8.tgz",
+      "dependencies": {
+        "busboy": {
+          "version": "0.2.13",
+          "from": "busboy@~0.2.9",
+          "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.13.tgz",
+          "dependencies": {
+            "dicer": {
+              "version": "0.2.5",
+              "from": "dicer@0.2.5",
+              "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
+              "dependencies": {
+                "streamsearch": {
+                  "version": "0.1.2",
+                  "from": "streamsearch@0.1.2",
+                  "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz"
+                }
+              }
+            },
+            "readable-stream": {
+              "version": "1.1.14",
+              "from": "readable-stream@1.1.x",
+              "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+              "dependencies": {
+                "core-util-is": {
+                  "version": "1.0.2",
+                  "from": "core-util-is@~1.0.0",
+                  "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+                },
+                "isarray": {
+                  "version": "0.0.1",
+                  "from": "isarray@0.0.1",
+                  "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+                },
+                "string_decoder": {
+                  "version": "0.10.31",
+                  "from": "string_decoder@~0.10.x",
+                  "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+                },
+                "inherits": {
+                  "version": "2.0.1",
+                  "from": "inherits@~2.0.1",
+                  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                }
+              }
+            }
+          }
+        },
+        "mkdirp": {
+          "version": "0.3.5",
+          "from": "mkdirp@~0.3.5",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
+        },
+        "qs": {
+          "version": "1.2.2",
+          "from": "qs@~1.2.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-1.2.2.tgz"
+        },
+        "type-is": {
+          "version": "1.5.7",
+          "from": "type-is@~1.5.2",
+          "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz",
+          "dependencies": {
+            "media-typer": {
+              "version": "0.3.0",
+              "from": "media-typer@0.3.0",
+              "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
+            },
+            "mime-types": {
+              "version": "2.0.14",
+              "from": "mime-types@~2.0.9",
+              "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz",
+              "dependencies": {
+                "mime-db": {
+                  "version": "1.12.0",
+                  "from": "mime-db@~1.12.0",
+                  "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "node-uuid": {
+      "version": "1.4.1",
+      "from": "node-uuid@1.4.1",
+      "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.1.tgz"
+    },
+    "nodemailer": {
+      "version": "2.1.0",
+      "from": "nodemailer@2.1.0",
+      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-2.1.0.tgz",
+      "dependencies": {
+        "libmime": {
+          "version": "2.0.0",
+          "from": "libmime@2.0.0",
+          "resolved": "https://registry.npmjs.org/libmime/-/libmime-2.0.0.tgz",
+          "dependencies": {
+            "iconv-lite": {
+              "version": "0.4.13",
+              "from": "iconv-lite@0.4.13",
+              "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz"
+            },
+            "libbase64": {
+              "version": "0.1.0",
+              "from": "libbase64@0.1.0",
+              "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz"
+            },
+            "libqp": {
+              "version": "1.1.0",
+              "from": "libqp@1.1.0",
+              "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz"
+            }
+          }
+        },
+        "mailcomposer": {
+          "version": "3.3.2",
+          "from": "mailcomposer@3.3.2",
+          "resolved": "https://registry.npmjs.org/mailcomposer/-/mailcomposer-3.3.2.tgz",
+          "dependencies": {
+            "buildmail": {
+              "version": "3.3.2",
+              "from": "buildmail@3.3.2",
+              "resolved": "https://registry.npmjs.org/buildmail/-/buildmail-3.3.2.tgz",
+              "dependencies": {
+                "addressparser": {
+                  "version": "1.0.0",
+                  "from": "addressparser@1.0.0",
+                  "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.0.tgz"
+                },
+                "libbase64": {
+                  "version": "0.1.0",
+                  "from": "libbase64@0.1.0",
+                  "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz"
+                },
+                "libqp": {
+                  "version": "1.1.0",
+                  "from": "libqp@1.1.0",
+                  "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz"
+                },
+                "nodemailer-fetch": {
+                  "version": "1.2.1",
+                  "from": "nodemailer-fetch@1.2.1",
+                  "resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.2.1.tgz"
+                }
+              }
+            }
+          }
+        },
+        "nodemailer-direct-transport": {
+          "version": "2.0.1",
+          "from": "nodemailer-direct-transport@2.0.1",
+          "resolved": "https://registry.npmjs.org/nodemailer-direct-transport/-/nodemailer-direct-transport-2.0.1.tgz",
+          "dependencies": {
+            "smtp-connection": {
+              "version": "2.0.1",
+              "from": "smtp-connection@2.0.1",
+              "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.0.1.tgz"
+            }
+          }
+        },
+        "nodemailer-shared": {
+          "version": "1.0.3",
+          "from": "nodemailer-shared@1.0.3",
+          "resolved": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.0.3.tgz",
+          "dependencies": {
+            "nodemailer-fetch": {
+              "version": "1.2.1",
+              "from": "nodemailer-fetch@1.2.1",
+              "resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.2.1.tgz"
+            }
+          }
+        },
+        "nodemailer-smtp-pool": {
+          "version": "2.1.0",
+          "from": "nodemailer-smtp-pool@2.1.0",
+          "resolved": "https://registry.npmjs.org/nodemailer-smtp-pool/-/nodemailer-smtp-pool-2.1.0.tgz",
+          "dependencies": {
+            "clone": {
+              "version": "1.0.2",
+              "from": "clone@1.0.2",
+              "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz"
+            },
+            "nodemailer-wellknown": {
+              "version": "0.1.7",
+              "from": "nodemailer-wellknown@0.1.7",
+              "resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.7.tgz"
+            },
+            "smtp-connection": {
+              "version": "2.0.1",
+              "from": "smtp-connection@2.0.1",
+              "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.0.1.tgz"
+            }
+          }
+        },
+        "nodemailer-smtp-transport": {
+          "version": "2.0.1",
+          "from": "nodemailer-smtp-transport@2.0.1",
+          "resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.0.1.tgz",
+          "dependencies": {
+            "clone": {
+              "version": "1.0.2",
+              "from": "clone@1.0.2",
+              "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz"
+            },
+            "nodemailer-wellknown": {
+              "version": "0.1.7",
+              "from": "nodemailer-wellknown@0.1.7",
+              "resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.7.tgz"
+            },
+            "smtp-connection": {
+              "version": "2.0.1",
+              "from": "smtp-connection@2.0.1",
+              "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.0.1.tgz"
+            }
+          }
+        }
+      }
+    },
+    "nodemailer-sendgrid-transport": {
+      "version": "0.2.0",
+      "from": "nodemailer-sendgrid-transport@^0.2.0",
+      "resolved": "https://registry.npmjs.org/nodemailer-sendgrid-transport/-/nodemailer-sendgrid-transport-0.2.0.tgz",
+      "dependencies": {
+        "sendgrid": {
+          "version": "1.9.2",
+          "from": "sendgrid@^1.8.0",
+          "resolved": "https://registry.npmjs.org/sendgrid/-/sendgrid-1.9.2.tgz",
+          "dependencies": {
+            "mime": {
+              "version": "1.3.4",
+              "from": "mime@^1.2.9",
+              "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz"
+            },
+            "lodash": {
+              "version": "3.10.1",
+              "from": "lodash@^3.0.1 || ^2.0.0",
+              "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz"
+            },
+            "smtpapi": {
+              "version": "1.2.0",
+              "from": "smtpapi@^1.2.0",
+              "resolved": "https://registry.npmjs.org/smtpapi/-/smtpapi-1.2.0.tgz"
+            }
+          }
+        }
+      }
+    },
+    "nodemailer-ses-transport": {
+      "version": "1.3.1",
+      "from": "nodemailer-ses-transport@^1.3.0",
+      "resolved": "https://registry.npmjs.org/nodemailer-ses-transport/-/nodemailer-ses-transport-1.3.1.tgz",
+      "dependencies": {
+        "aws-sdk": {
+          "version": "2.4.0",
+          "from": "aws-sdk@^2.3.11",
+          "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.4.0.tgz",
+          "dependencies": {
+            "sax": {
+              "version": "1.1.5",
+              "from": "sax@1.1.5",
+              "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.5.tgz"
+            },
+            "xml2js": {
+              "version": "0.4.15",
+              "from": "xml2js@0.4.15",
+              "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.15.tgz"
+            },
+            "xmlbuilder": {
+              "version": "2.6.2",
+              "from": "xmlbuilder@2.6.2",
+              "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.6.2.tgz",
+              "dependencies": {
+                "lodash": {
+                  "version": "3.5.0",
+                  "from": "lodash@~3.5.0",
+                  "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.5.0.tgz"
+                }
+              }
+            },
+            "jmespath": {
+              "version": "0.15.0",
+              "from": "jmespath@0.15.0",
+              "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz"
+            }
+          }
+        }
+      }
+    },
+    "optimist": {
+      "version": "0.6.1",
+      "from": "optimist@0.6.1",
+      "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+      "dependencies": {
+        "wordwrap": {
+          "version": "0.0.3",
+          "from": "wordwrap@~0.0.2",
+          "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz"
+        },
+        "minimist": {
+          "version": "0.0.10",
+          "from": "minimist@~0.0.1",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz"
+        }
+      }
+    },
+    "redback": {
+      "version": "0.4.0",
+      "from": "redback@0.4.0",
+      "resolved": "https://registry.npmjs.org/redback/-/redback-0.4.0.tgz"
+    },
+    "redis": {
+      "version": "0.10.1",
+      "from": "redis@0.10.1",
+      "resolved": "https://registry.npmjs.org/redis/-/redis-0.10.1.tgz"
+    },
+    "redis-sharelatex": {
+      "version": "0.0.9",
+      "from": "redis-sharelatex@0.0.9",
+      "resolved": "https://registry.npmjs.org/redis-sharelatex/-/redis-sharelatex-0.0.9.tgz",
+      "dependencies": {
+        "chai": {
+          "version": "1.9.1",
+          "from": "chai@1.9.1",
+          "resolved": "https://registry.npmjs.org/chai/-/chai-1.9.1.tgz",
+          "dependencies": {
+            "assertion-error": {
+              "version": "1.0.0",
+              "from": "assertion-error@1.0.0",
+              "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz"
+            },
+            "deep-eql": {
+              "version": "0.1.3",
+              "from": "deep-eql@0.1.3",
+              "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz",
+              "dependencies": {
+                "type-detect": {
+                  "version": "0.1.1",
+                  "from": "type-detect@0.1.1",
+                  "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz"
+                }
+              }
+            }
+          }
+        },
+        "coffee-script": {
+          "version": "1.8.0",
+          "from": "coffee-script@1.8.0",
+          "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.8.0.tgz",
+          "dependencies": {
+            "mkdirp": {
+              "version": "0.3.5",
+              "from": "mkdirp@~0.3.5",
+              "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
+            }
+          }
+        },
+        "grunt-contrib-coffee": {
+          "version": "0.11.1",
+          "from": "grunt-contrib-coffee@0.11.1",
+          "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.11.1.tgz",
+          "dependencies": {
+            "coffee-script": {
+              "version": "1.7.1",
+              "from": "coffee-script@~1.7.0",
+              "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz",
+              "dependencies": {
+                "mkdirp": {
+                  "version": "0.3.5",
+                  "from": "mkdirp@~0.3.5",
+                  "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
+                }
+              }
+            },
+            "chalk": {
+              "version": "0.5.1",
+              "from": "chalk@~0.5.0",
+              "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz",
+              "dependencies": {
+                "ansi-styles": {
+                  "version": "1.1.0",
+                  "from": "ansi-styles@^1.1.0",
+                  "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz"
+                },
+                "escape-string-regexp": {
+                  "version": "1.0.5",
+                  "from": "escape-string-regexp@^1.0.0",
+                  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
+                },
+                "has-ansi": {
+                  "version": "0.1.0",
+                  "from": "has-ansi@^0.1.0",
+                  "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz",
+                  "dependencies": {
+                    "ansi-regex": {
+                      "version": "0.2.1",
+                      "from": "ansi-regex@^0.2.0",
+                      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz"
+                    }
+                  }
+                },
+                "strip-ansi": {
+                  "version": "0.3.0",
+                  "from": "strip-ansi@^0.3.0",
+                  "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz",
+                  "dependencies": {
+                    "ansi-regex": {
+                      "version": "0.2.1",
+                      "from": "ansi-regex@^0.2.0",
+                      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz"
+                    }
+                  }
+                },
+                "supports-color": {
+                  "version": "0.2.0",
+                  "from": "supports-color@^0.2.0",
+                  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz"
+                }
+              }
+            },
+            "lodash": {
+              "version": "2.4.2",
+              "from": "lodash@~2.4.1",
+              "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
+            }
+          }
+        },
+        "grunt-mocha-test": {
+          "version": "0.12.0",
+          "from": "grunt-mocha-test@0.12.0",
+          "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.12.0.tgz",
+          "dependencies": {
+            "hooker": {
+              "version": "0.2.3",
+              "from": "hooker@~0.2.3",
+              "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz"
+            },
+            "fs-extra": {
+              "version": "0.11.1",
+              "from": "fs-extra@~0.11.1",
+              "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.11.1.tgz",
+              "dependencies": {
+                "ncp": {
+                  "version": "0.6.0",
+                  "from": "ncp@^0.6.0",
+                  "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.6.0.tgz"
+                },
+                "mkdirp": {
+                  "version": "0.5.1",
+                  "from": "mkdirp@^0.5.0",
+                  "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+                  "dependencies": {
+                    "minimist": {
+                      "version": "0.0.8",
+                      "from": "minimist@0.0.8",
+                      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
+                    }
+                  }
+                },
+                "jsonfile": {
+                  "version": "2.3.1",
+                  "from": "jsonfile@^2.0.0",
+                  "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.3.1.tgz"
+                },
+                "rimraf": {
+                  "version": "2.5.2",
+                  "from": "rimraf@^2.2.8",
+                  "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz",
+                  "dependencies": {
+                    "glob": {
+                      "version": "7.0.3",
+                      "from": "glob@^7.0.0",
+                      "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz",
+                      "dependencies": {
+                        "inflight": {
+                          "version": "1.0.5",
+                          "from": "inflight@^1.0.4",
+                          "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz",
+                          "dependencies": {
+                            "wrappy": {
+                              "version": "1.0.2",
+                              "from": "wrappy@1",
+                              "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+                            }
+                          }
+                        },
+                        "inherits": {
+                          "version": "2.0.1",
+                          "from": "inherits@2",
+                          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                        },
+                        "minimatch": {
+                          "version": "3.0.0",
+                          "from": "minimatch@2 || 3",
+                          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz",
+                          "dependencies": {
+                            "brace-expansion": {
+                              "version": "1.1.4",
+                              "from": "brace-expansion@^1.0.0",
+                              "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz",
+                              "dependencies": {
+                                "balanced-match": {
+                                  "version": "0.4.1",
+                                  "from": "balanced-match@^0.4.1",
+                                  "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
+                                },
+                                "concat-map": {
+                                  "version": "0.0.1",
+                                  "from": "concat-map@0.0.1",
+                                  "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+                                }
+                              }
+                            }
+                          }
+                        },
+                        "once": {
+                          "version": "1.3.3",
+                          "from": "once@^1.3.0",
+                          "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz",
+                          "dependencies": {
+                            "wrappy": {
+                              "version": "1.0.2",
+                              "from": "wrappy@1",
+                              "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+                            }
+                          }
+                        },
+                        "path-is-absolute": {
+                          "version": "1.0.0",
+                          "from": "path-is-absolute@^1.0.0",
+                          "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz"
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+        "mocha": {
+          "version": "1.21.4",
+          "from": "mocha@1.21.4",
+          "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.21.4.tgz",
+          "dependencies": {
+            "commander": {
+              "version": "2.0.0",
+              "from": "commander@2.0.0",
+              "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz"
+            },
+            "growl": {
+              "version": "1.8.1",
+              "from": "growl@1.8.x",
+              "resolved": "https://registry.npmjs.org/growl/-/growl-1.8.1.tgz"
+            },
+            "jade": {
+              "version": "0.26.3",
+              "from": "jade@0.26.3",
+              "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz",
+              "dependencies": {
+                "commander": {
+                  "version": "0.6.1",
+                  "from": "commander@0.6.1",
+                  "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz"
+                },
+                "mkdirp": {
+                  "version": "0.3.0",
+                  "from": "mkdirp@0.3.0",
+                  "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz"
+                }
+              }
+            },
+            "diff": {
+              "version": "1.0.7",
+              "from": "diff@1.0.7",
+              "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz"
+            },
+            "debug": {
+              "version": "2.2.0",
+              "from": "debug@*",
+              "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+              "dependencies": {
+                "ms": {
+                  "version": "0.7.1",
+                  "from": "ms@0.7.1",
+                  "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+                }
+              }
+            },
+            "mkdirp": {
+              "version": "0.3.5",
+              "from": "mkdirp@0.3.5",
+              "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
+            },
+            "glob": {
+              "version": "3.2.3",
+              "from": "glob@3.2.3",
+              "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz",
+              "dependencies": {
+                "minimatch": {
+                  "version": "0.2.14",
+                  "from": "minimatch@~0.2.11",
+                  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
+                  "dependencies": {
+                    "lru-cache": {
+                      "version": "2.7.3",
+                      "from": "lru-cache@2",
+                      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
+                    },
+                    "sigmund": {
+                      "version": "1.0.1",
+                      "from": "sigmund@~1.0.0",
+                      "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
+                    }
+                  }
+                },
+                "graceful-fs": {
+                  "version": "2.0.3",
+                  "from": "graceful-fs@~2.0.0",
+                  "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz"
+                },
+                "inherits": {
+                  "version": "2.0.1",
+                  "from": "inherits@2",
+                  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                }
+              }
+            }
+          }
+        },
+        "redis": {
+          "version": "0.12.1",
+          "from": "redis@0.12.1",
+          "resolved": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz"
+        },
+        "redis-sentinel": {
+          "version": "0.1.1",
+          "from": "redis-sentinel@0.1.1",
+          "resolved": "https://registry.npmjs.org/redis-sentinel/-/redis-sentinel-0.1.1.tgz",
+          "dependencies": {
+            "redis": {
+              "version": "0.11.0",
+              "from": "redis@0.11.x",
+              "resolved": "https://registry.npmjs.org/redis/-/redis-0.11.0.tgz"
+            },
+            "q": {
+              "version": "0.9.2",
+              "from": "q@0.9.2",
+              "resolved": "https://registry.npmjs.org/q/-/q-0.9.2.tgz"
+            }
+          }
+        },
+        "sandboxed-module": {
+          "version": "1.0.1",
+          "from": "sandboxed-module@1.0.1",
+          "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-1.0.1.tgz",
+          "dependencies": {
+            "require-like": {
+              "version": "0.1.2",
+              "from": "require-like@0.1.2",
+              "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz"
+            },
+            "stack-trace": {
+              "version": "0.0.9",
+              "from": "stack-trace@0.0.9",
+              "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz"
+            }
+          }
+        },
+        "sinon": {
+          "version": "1.10.3",
+          "from": "sinon@1.10.3",
+          "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.10.3.tgz",
+          "dependencies": {
+            "formatio": {
+              "version": "1.0.2",
+              "from": "formatio@~1.0",
+              "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.0.2.tgz",
+              "dependencies": {
+                "samsam": {
+                  "version": "1.1.3",
+                  "from": "samsam@~1.1",
+                  "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.3.tgz"
+                }
+              }
+            },
+            "util": {
+              "version": "0.10.3",
+              "from": "util@>=0.10.3 <1",
+              "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+              "dependencies": {
+                "inherits": {
+                  "version": "2.0.1",
+                  "from": "inherits@2.0.1",
+                  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                }
+              }
+            }
+          }
+        },
+        "underscore": {
+          "version": "1.7.0",
+          "from": "underscore@1.7.0",
+          "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz"
+        }
+      }
+    },
+    "request": {
+      "version": "2.72.0",
+      "from": "request@^2.69.0",
+      "resolved": "https://registry.npmjs.org/request/-/request-2.72.0.tgz",
+      "dependencies": {
+        "aws-sign2": {
+          "version": "0.6.0",
+          "from": "aws-sign2@~0.6.0",
+          "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz"
+        },
+        "aws4": {
+          "version": "1.4.1",
+          "from": "aws4@^1.2.1",
+          "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.4.1.tgz"
+        },
+        "bl": {
+          "version": "1.1.2",
+          "from": "bl@~1.1.2",
+          "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz",
+          "dependencies": {
+            "readable-stream": {
+              "version": "2.0.6",
+              "from": "readable-stream@~2.0.5",
+              "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+              "dependencies": {
+                "core-util-is": {
+                  "version": "1.0.2",
+                  "from": "core-util-is@~1.0.0",
+                  "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+                },
+                "inherits": {
+                  "version": "2.0.1",
+                  "from": "inherits@~2.0.1",
+                  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                },
+                "isarray": {
+                  "version": "1.0.0",
+                  "from": "isarray@~1.0.0",
+                  "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
+                },
+                "process-nextick-args": {
+                  "version": "1.0.7",
+                  "from": "process-nextick-args@~1.0.6",
+                  "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz"
+                },
+                "string_decoder": {
+                  "version": "0.10.31",
+                  "from": "string_decoder@~0.10.x",
+                  "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+                },
+                "util-deprecate": {
+                  "version": "1.0.2",
+                  "from": "util-deprecate@~1.0.1",
+                  "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
+                }
+              }
+            }
+          }
+        },
+        "caseless": {
+          "version": "0.11.0",
+          "from": "caseless@~0.11.0",
+          "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz"
+        },
+        "combined-stream": {
+          "version": "1.0.5",
+          "from": "combined-stream@~1.0.5",
+          "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
+          "dependencies": {
+            "delayed-stream": {
+              "version": "1.0.0",
+              "from": "delayed-stream@~1.0.0",
+              "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
+            }
+          }
+        },
+        "extend": {
+          "version": "3.0.0",
+          "from": "extend@~3.0.0",
+          "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz"
+        },
+        "forever-agent": {
+          "version": "0.6.1",
+          "from": "forever-agent@~0.6.1",
+          "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz"
+        },
+        "form-data": {
+          "version": "1.0.0-rc4",
+          "from": "form-data@~1.0.0-rc3",
+          "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz",
+          "dependencies": {
+            "async": {
+              "version": "1.5.2",
+              "from": "async@^1.5.2",
+              "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
+            }
+          }
+        },
+        "har-validator": {
+          "version": "2.0.6",
+          "from": "har-validator@~2.0.6",
+          "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz",
+          "dependencies": {
+            "chalk": {
+              "version": "1.1.3",
+              "from": "chalk@^1.1.1",
+              "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+              "dependencies": {
+                "ansi-styles": {
+                  "version": "2.2.1",
+                  "from": "ansi-styles@^2.2.1",
+                  "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz"
+                },
+                "escape-string-regexp": {
+                  "version": "1.0.5",
+                  "from": "escape-string-regexp@^1.0.2",
+                  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
+                },
+                "has-ansi": {
+                  "version": "2.0.0",
+                  "from": "has-ansi@^2.0.0",
+                  "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+                  "dependencies": {
+                    "ansi-regex": {
+                      "version": "2.0.0",
+                      "from": "ansi-regex@^2.0.0",
+                      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
+                    }
+                  }
+                },
+                "strip-ansi": {
+                  "version": "3.0.1",
+                  "from": "strip-ansi@^3.0.0",
+                  "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+                  "dependencies": {
+                    "ansi-regex": {
+                      "version": "2.0.0",
+                      "from": "ansi-regex@^2.0.0",
+                      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
+                    }
+                  }
+                },
+                "supports-color": {
+                  "version": "2.0.0",
+                  "from": "supports-color@^2.0.0",
+                  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
+                }
+              }
+            },
+            "commander": {
+              "version": "2.9.0",
+              "from": "commander@^2.9.0",
+              "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
+              "dependencies": {
+                "graceful-readlink": {
+                  "version": "1.0.1",
+                  "from": "graceful-readlink@>= 1.0.0",
+                  "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz"
+                }
+              }
+            },
+            "is-my-json-valid": {
+              "version": "2.13.1",
+              "from": "is-my-json-valid@^2.12.4",
+              "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz",
+              "dependencies": {
+                "generate-function": {
+                  "version": "2.0.0",
+                  "from": "generate-function@^2.0.0",
+                  "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz"
+                },
+                "generate-object-property": {
+                  "version": "1.2.0",
+                  "from": "generate-object-property@^1.1.0",
+                  "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
+                  "dependencies": {
+                    "is-property": {
+                      "version": "1.0.2",
+                      "from": "is-property@^1.0.0",
+                      "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz"
+                    }
+                  }
+                },
+                "jsonpointer": {
+                  "version": "2.0.0",
+                  "from": "jsonpointer@2.0.0",
+                  "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz"
+                },
+                "xtend": {
+                  "version": "4.0.1",
+                  "from": "xtend@^4.0.0",
+                  "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
+                }
+              }
+            },
+            "pinkie-promise": {
+              "version": "2.0.1",
+              "from": "pinkie-promise@^2.0.0",
+              "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+              "dependencies": {
+                "pinkie": {
+                  "version": "2.0.4",
+                  "from": "pinkie@^2.0.0",
+                  "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz"
+                }
+              }
+            }
+          }
+        },
+        "hawk": {
+          "version": "3.1.3",
+          "from": "hawk@~3.1.3",
+          "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
+          "dependencies": {
+            "hoek": {
+              "version": "2.16.3",
+              "from": "hoek@2.x.x",
+              "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz"
+            },
+            "boom": {
+              "version": "2.10.1",
+              "from": "boom@2.x.x",
+              "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz"
+            },
+            "cryptiles": {
+              "version": "2.0.5",
+              "from": "cryptiles@2.x.x",
+              "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz"
+            },
+            "sntp": {
+              "version": "1.0.9",
+              "from": "sntp@1.x.x",
+              "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz"
+            }
+          }
+        },
+        "http-signature": {
+          "version": "1.1.1",
+          "from": "http-signature@~1.1.0",
+          "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
+          "dependencies": {
+            "assert-plus": {
+              "version": "0.2.0",
+              "from": "assert-plus@^0.2.0",
+              "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz"
+            },
+            "jsprim": {
+              "version": "1.2.2",
+              "from": "jsprim@^1.2.2",
+              "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz",
+              "dependencies": {
+                "extsprintf": {
+                  "version": "1.0.2",
+                  "from": "extsprintf@1.0.2",
+                  "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz"
+                },
+                "json-schema": {
+                  "version": "0.2.2",
+                  "from": "json-schema@0.2.2",
+                  "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz"
+                },
+                "verror": {
+                  "version": "1.3.6",
+                  "from": "verror@1.3.6",
+                  "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz"
+                }
+              }
+            },
+            "sshpk": {
+              "version": "1.8.3",
+              "from": "sshpk@^1.7.0",
+              "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.8.3.tgz",
+              "dependencies": {
+                "asn1": {
+                  "version": "0.2.3",
+                  "from": "asn1@~0.2.3",
+                  "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz"
+                },
+                "assert-plus": {
+                  "version": "1.0.0",
+                  "from": "assert-plus@^1.0.0",
+                  "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz"
+                },
+                "dashdash": {
+                  "version": "1.14.0",
+                  "from": "dashdash@^1.12.0",
+                  "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.0.tgz"
+                },
+                "getpass": {
+                  "version": "0.1.6",
+                  "from": "getpass@^0.1.1",
+                  "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz"
+                },
+                "jsbn": {
+                  "version": "0.1.0",
+                  "from": "jsbn@~0.1.0",
+                  "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz"
+                },
+                "tweetnacl": {
+                  "version": "0.13.3",
+                  "from": "tweetnacl@~0.13.0",
+                  "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.3.tgz"
+                },
+                "jodid25519": {
+                  "version": "1.0.2",
+                  "from": "jodid25519@^1.0.0",
+                  "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz"
+                },
+                "ecc-jsbn": {
+                  "version": "0.1.1",
+                  "from": "ecc-jsbn@~0.1.1",
+                  "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz"
+                }
+              }
+            }
+          }
+        },
+        "is-typedarray": {
+          "version": "1.0.0",
+          "from": "is-typedarray@~1.0.0",
+          "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz"
+        },
+        "isstream": {
+          "version": "0.1.2",
+          "from": "isstream@~0.1.2",
+          "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz"
+        },
+        "json-stringify-safe": {
+          "version": "5.0.1",
+          "from": "json-stringify-safe@~5.0.1",
+          "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
+        },
+        "mime-types": {
+          "version": "2.1.11",
+          "from": "mime-types@~2.1.7",
+          "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz",
+          "dependencies": {
+            "mime-db": {
+              "version": "1.23.0",
+              "from": "mime-db@~1.23.0",
+              "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz"
+            }
+          }
+        },
+        "node-uuid": {
+          "version": "1.4.7",
+          "from": "node-uuid@~1.4.7",
+          "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz"
+        },
+        "oauth-sign": {
+          "version": "0.8.2",
+          "from": "oauth-sign@~0.8.1",
+          "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz"
+        },
+        "qs": {
+          "version": "6.1.0",
+          "from": "qs@~6.1.0",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz"
+        },
+        "stringstream": {
+          "version": "0.0.5",
+          "from": "stringstream@~0.0.4",
+          "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz"
+        },
+        "tough-cookie": {
+          "version": "2.2.2",
+          "from": "tough-cookie@~2.2.0",
+          "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz"
+        },
+        "tunnel-agent": {
+          "version": "0.4.3",
+          "from": "tunnel-agent@~0.4.1",
+          "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz"
+        }
+      }
+    },
+    "requests": {
+      "version": "0.1.7",
+      "from": "requests@^0.1.7",
+      "resolved": "https://registry.npmjs.org/requests/-/requests-0.1.7.tgz",
+      "dependencies": {
+        "axo": {
+          "version": "0.0.1",
+          "from": "axo@0.0.x",
+          "resolved": "https://registry.npmjs.org/axo/-/axo-0.0.1.tgz"
+        },
+        "eventemitter3": {
+          "version": "1.1.1",
+          "from": "eventemitter3@1.1.x",
+          "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.1.1.tgz"
+        },
+        "extendible": {
+          "version": "0.1.1",
+          "from": "extendible@0.1.x",
+          "resolved": "https://registry.npmjs.org/extendible/-/extendible-0.1.1.tgz"
+        },
+        "hang": {
+          "version": "1.0.0",
+          "from": "hang@1.0.x",
+          "resolved": "https://registry.npmjs.org/hang/-/hang-1.0.0.tgz"
+        },
+        "loads": {
+          "version": "0.0.4",
+          "from": "loads@0.0.x",
+          "resolved": "https://registry.npmjs.org/loads/-/loads-0.0.4.tgz",
+          "dependencies": {
+            "failure": {
+              "version": "1.1.1",
+              "from": "failure@1.1.x",
+              "resolved": "https://registry.npmjs.org/failure/-/failure-1.1.1.tgz"
+            },
+            "one-time": {
+              "version": "0.0.4",
+              "from": "one-time@0.0.x",
+              "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz"
+            },
+            "xhr-response": {
+              "version": "1.0.1",
+              "from": "xhr-response@1.0.x",
+              "resolved": "https://registry.npmjs.org/xhr-response/-/xhr-response-1.0.1.tgz"
+            },
+            "xhr-status": {
+              "version": "1.0.0",
+              "from": "xhr-status@1.0.x",
+              "resolved": "https://registry.npmjs.org/xhr-status/-/xhr-status-1.0.0.tgz"
+            }
+          }
+        },
+        "xhr-send": {
+          "version": "1.0.0",
+          "from": "xhr-send@1.0.x",
+          "resolved": "https://registry.npmjs.org/xhr-send/-/xhr-send-1.0.0.tgz"
+        }
+      }
+    },
+    "rimraf": {
+      "version": "2.2.6",
+      "from": "rimraf@2.2.6",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz"
+    },
+    "sanitizer": {
+      "version": "0.1.1",
+      "from": "sanitizer@0.1.1",
+      "resolved": "https://registry.npmjs.org/sanitizer/-/sanitizer-0.1.1.tgz"
+    },
+    "settings-sharelatex": {
+      "version": "1.0.0",
+      "from": "settings-sharelatex@git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
+      "resolved": "git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559",
+      "dependencies": {
+        "coffee-script": {
+          "version": "1.6.0",
+          "from": "coffee-script@1.6.0",
+          "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz"
+        }
+      }
+    },
+    "sixpack-client": {
+      "version": "1.0.0",
+      "from": "sixpack-client@^1.0.0",
+      "resolved": "https://registry.npmjs.org/sixpack-client/-/sixpack-client-1.0.0.tgz"
+    },
+    "temp": {
+      "version": "0.8.3",
+      "from": "temp@^0.8.3",
+      "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz",
+      "dependencies": {
+        "os-tmpdir": {
+          "version": "1.0.1",
+          "from": "os-tmpdir@^1.0.0",
+          "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.1.tgz"
+        }
+      }
+    },
+    "underscore": {
+      "version": "1.6.0",
+      "from": "underscore@1.6.0",
+      "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz"
+    },
+    "v8-profiler": {
+      "version": "5.6.5",
+      "from": "v8-profiler@^5.2.3",
+      "resolved": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.6.5.tgz",
+      "dependencies": {
+        "nan": {
+          "version": "2.3.5",
+          "from": "nan@^2.3.2",
+          "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz"
+        },
+        "node-pre-gyp": {
+          "version": "0.6.28",
+          "from": "node-pre-gyp@^0.6.5",
+          "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.28.tgz",
+          "dependencies": {
+            "mkdirp": {
+              "version": "0.5.1",
+              "from": "mkdirp@~0.5.0",
+              "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+              "dependencies": {
+                "minimist": {
+                  "version": "0.0.8",
+                  "from": "minimist@0.0.8",
+                  "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
+                }
+              }
+            },
+            "nopt": {
+              "version": "3.0.6",
+              "from": "nopt@~3.0.1",
+              "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+              "dependencies": {
+                "abbrev": {
+                  "version": "1.0.7",
+                  "from": "abbrev@1",
+                  "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz"
+                }
+              }
+            },
+            "npmlog": {
+              "version": "2.0.4",
+              "from": "npmlog@~2.0.0",
+              "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-2.0.4.tgz",
+              "dependencies": {
+                "ansi": {
+                  "version": "0.3.1",
+                  "from": "ansi@~0.3.1",
+                  "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz"
+                },
+                "are-we-there-yet": {
+                  "version": "1.1.2",
+                  "from": "are-we-there-yet@~1.1.2",
+                  "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz",
+                  "dependencies": {
+                    "delegates": {
+                      "version": "1.0.0",
+                      "from": "delegates@^1.0.0",
+                      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz"
+                    },
+                    "readable-stream": {
+                      "version": "2.1.4",
+                      "from": "readable-stream@^2.0.0 || ^1.1.13",
+                      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz",
+                      "dependencies": {
+                        "buffer-shims": {
+                          "version": "1.0.0",
+                          "from": "buffer-shims@^1.0.0",
+                          "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz"
+                        },
+                        "core-util-is": {
+                          "version": "1.0.2",
+                          "from": "core-util-is@~1.0.0",
+                          "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+                        },
+                        "inherits": {
+                          "version": "2.0.1",
+                          "from": "inherits@~2.0.1",
+                          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                        },
+                        "isarray": {
+                          "version": "1.0.0",
+                          "from": "isarray@~1.0.0",
+                          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
+                        },
+                        "process-nextick-args": {
+                          "version": "1.0.7",
+                          "from": "process-nextick-args@~1.0.6",
+                          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz"
+                        },
+                        "string_decoder": {
+                          "version": "0.10.31",
+                          "from": "string_decoder@~0.10.x",
+                          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+                        },
+                        "util-deprecate": {
+                          "version": "1.0.2",
+                          "from": "util-deprecate@~1.0.1",
+                          "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
+                        }
+                      }
+                    }
+                  }
+                },
+                "gauge": {
+                  "version": "1.2.7",
+                  "from": "gauge@~1.2.5",
+                  "resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz",
+                  "dependencies": {
+                    "has-unicode": {
+                      "version": "2.0.0",
+                      "from": "has-unicode@^2.0.0",
+                      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.0.tgz"
+                    },
+                    "lodash.pad": {
+                      "version": "4.4.0",
+                      "from": "lodash.pad@^4.1.0",
+                      "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.4.0.tgz",
+                      "dependencies": {
+                        "lodash._baseslice": {
+                          "version": "4.0.0",
+                          "from": "lodash._baseslice@~4.0.0",
+                          "resolved": "https://registry.npmjs.org/lodash._baseslice/-/lodash._baseslice-4.0.0.tgz"
+                        },
+                        "lodash._basetostring": {
+                          "version": "4.12.0",
+                          "from": "lodash._basetostring@~4.12.0",
+                          "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz"
+                        },
+                        "lodash.tostring": {
+                          "version": "4.1.3",
+                          "from": "lodash.tostring@^4.0.0",
+                          "resolved": "https://registry.npmjs.org/lodash.tostring/-/lodash.tostring-4.1.3.tgz"
+                        }
+                      }
+                    },
+                    "lodash.padend": {
+                      "version": "4.5.0",
+                      "from": "lodash.padend@^4.1.0",
+                      "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.5.0.tgz",
+                      "dependencies": {
+                        "lodash._baseslice": {
+                          "version": "4.0.0",
+                          "from": "lodash._baseslice@~4.0.0",
+                          "resolved": "https://registry.npmjs.org/lodash._baseslice/-/lodash._baseslice-4.0.0.tgz"
+                        },
+                        "lodash._basetostring": {
+                          "version": "4.12.0",
+                          "from": "lodash._basetostring@~4.12.0",
+                          "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz"
+                        },
+                        "lodash.tostring": {
+                          "version": "4.1.3",
+                          "from": "lodash.tostring@^4.0.0",
+                          "resolved": "https://registry.npmjs.org/lodash.tostring/-/lodash.tostring-4.1.3.tgz"
+                        }
+                      }
+                    },
+                    "lodash.padstart": {
+                      "version": "4.5.0",
+                      "from": "lodash.padstart@^4.1.0",
+                      "resolved": "https://registry.npmjs.org/lodash.padstart/-/lodash.padstart-4.5.0.tgz",
+                      "dependencies": {
+                        "lodash._baseslice": {
+                          "version": "4.0.0",
+                          "from": "lodash._baseslice@~4.0.0",
+                          "resolved": "https://registry.npmjs.org/lodash._baseslice/-/lodash._baseslice-4.0.0.tgz"
+                        },
+                        "lodash._basetostring": {
+                          "version": "4.12.0",
+                          "from": "lodash._basetostring@~4.12.0",
+                          "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz"
+                        },
+                        "lodash.tostring": {
+                          "version": "4.1.3",
+                          "from": "lodash.tostring@^4.0.0",
+                          "resolved": "https://registry.npmjs.org/lodash.tostring/-/lodash.tostring-4.1.3.tgz"
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            },
+            "rc": {
+              "version": "1.1.6",
+              "from": "rc@~1.1.0",
+              "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz",
+              "dependencies": {
+                "deep-extend": {
+                  "version": "0.4.1",
+                  "from": "deep-extend@~0.4.0",
+                  "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz"
+                },
+                "ini": {
+                  "version": "1.3.4",
+                  "from": "ini@~1.3.0",
+                  "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz"
+                },
+                "minimist": {
+                  "version": "1.2.0",
+                  "from": "minimist@^1.2.0",
+                  "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+                },
+                "strip-json-comments": {
+                  "version": "1.0.4",
+                  "from": "strip-json-comments@~1.0.4",
+                  "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz"
+                }
+              }
+            },
+            "rimraf": {
+              "version": "2.5.2",
+              "from": "rimraf@~2.5.0",
+              "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz",
+              "dependencies": {
+                "glob": {
+                  "version": "7.0.3",
+                  "from": "glob@^7.0.0",
+                  "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz",
+                  "dependencies": {
+                    "inflight": {
+                      "version": "1.0.5",
+                      "from": "inflight@^1.0.4",
+                      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz",
+                      "dependencies": {
+                        "wrappy": {
+                          "version": "1.0.2",
+                          "from": "wrappy@1",
+                          "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+                        }
+                      }
+                    },
+                    "inherits": {
+                      "version": "2.0.1",
+                      "from": "inherits@2",
+                      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                    },
+                    "minimatch": {
+                      "version": "3.0.0",
+                      "from": "minimatch@2 || 3",
+                      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz",
+                      "dependencies": {
+                        "brace-expansion": {
+                          "version": "1.1.4",
+                          "from": "brace-expansion@^1.0.0",
+                          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz",
+                          "dependencies": {
+                            "balanced-match": {
+                              "version": "0.4.1",
+                              "from": "balanced-match@^0.4.1",
+                              "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
+                            },
+                            "concat-map": {
+                              "version": "0.0.1",
+                              "from": "concat-map@0.0.1",
+                              "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+                            }
+                          }
+                        }
+                      }
+                    },
+                    "once": {
+                      "version": "1.3.3",
+                      "from": "once@^1.3.0",
+                      "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz",
+                      "dependencies": {
+                        "wrappy": {
+                          "version": "1.0.2",
+                          "from": "wrappy@1",
+                          "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+                        }
+                      }
+                    },
+                    "path-is-absolute": {
+                      "version": "1.0.0",
+                      "from": "path-is-absolute@^1.0.0",
+                      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz"
+                    }
+                  }
+                }
+              }
+            },
+            "semver": {
+              "version": "5.1.0",
+              "from": "semver@~5.1.0",
+              "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz"
+            },
+            "tar": {
+              "version": "2.2.1",
+              "from": "tar@~2.2.0",
+              "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
+              "dependencies": {
+                "block-stream": {
+                  "version": "0.0.9",
+                  "from": "block-stream@*",
+                  "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz"
+                },
+                "fstream": {
+                  "version": "1.0.9",
+                  "from": "fstream@^1.0.2",
+                  "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.9.tgz",
+                  "dependencies": {
+                    "graceful-fs": {
+                      "version": "4.1.4",
+                      "from": "graceful-fs@^4.1.2",
+                      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.4.tgz"
+                    }
+                  }
+                },
+                "inherits": {
+                  "version": "2.0.1",
+                  "from": "inherits@~2.0.1",
+                  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                }
+              }
+            },
+            "tar-pack": {
+              "version": "3.1.3",
+              "from": "tar-pack@~3.1.0",
+              "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.1.3.tgz",
+              "dependencies": {
+                "debug": {
+                  "version": "2.2.0",
+                  "from": "debug@~2.2.0",
+                  "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+                  "dependencies": {
+                    "ms": {
+                      "version": "0.7.1",
+                      "from": "ms@0.7.1",
+                      "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+                    }
+                  }
+                },
+                "fstream": {
+                  "version": "1.0.9",
+                  "from": "fstream@~1.0.8",
+                  "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.9.tgz",
+                  "dependencies": {
+                    "graceful-fs": {
+                      "version": "4.1.4",
+                      "from": "graceful-fs@^4.1.2",
+                      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.4.tgz"
+                    },
+                    "inherits": {
+                      "version": "2.0.1",
+                      "from": "inherits@~2.0.0",
+                      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                    }
+                  }
+                },
+                "fstream-ignore": {
+                  "version": "1.0.5",
+                  "from": "fstream-ignore@~1.0.3",
+                  "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz",
+                  "dependencies": {
+                    "inherits": {
+                      "version": "2.0.1",
+                      "from": "inherits@2",
+                      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                    },
+                    "minimatch": {
+                      "version": "3.0.0",
+                      "from": "minimatch@^3.0.0",
+                      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz",
+                      "dependencies": {
+                        "brace-expansion": {
+                          "version": "1.1.4",
+                          "from": "brace-expansion@^1.0.0",
+                          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz",
+                          "dependencies": {
+                            "balanced-match": {
+                              "version": "0.4.1",
+                              "from": "balanced-match@^0.4.1",
+                              "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
+                            },
+                            "concat-map": {
+                              "version": "0.0.1",
+                              "from": "concat-map@0.0.1",
+                              "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+                            }
+                          }
+                        }
+                      }
+                    }
+                  }
+                },
+                "once": {
+                  "version": "1.3.3",
+                  "from": "once@~1.3.3",
+                  "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz",
+                  "dependencies": {
+                    "wrappy": {
+                      "version": "1.0.2",
+                      "from": "wrappy@1",
+                      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+                    }
+                  }
+                },
+                "readable-stream": {
+                  "version": "2.0.6",
+                  "from": "readable-stream@~2.0.4",
+                  "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+                  "dependencies": {
+                    "core-util-is": {
+                      "version": "1.0.2",
+                      "from": "core-util-is@~1.0.0",
+                      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+                    },
+                    "inherits": {
+                      "version": "2.0.1",
+                      "from": "inherits@~2.0.1",
+                      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+                    },
+                    "isarray": {
+                      "version": "1.0.0",
+                      "from": "isarray@~1.0.0",
+                      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
+                    },
+                    "process-nextick-args": {
+                      "version": "1.0.7",
+                      "from": "process-nextick-args@~1.0.6",
+                      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz"
+                    },
+                    "string_decoder": {
+                      "version": "0.10.31",
+                      "from": "string_decoder@~0.10.x",
+                      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+                    },
+                    "util-deprecate": {
+                      "version": "1.0.2",
+                      "from": "util-deprecate@~1.0.1",
+                      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
+                    }
+                  }
+                },
+                "uid-number": {
+                  "version": "0.0.6",
+                  "from": "uid-number@~0.0.6",
+                  "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "xml2js": {
+      "version": "0.2.0",
+      "from": "xml2js@0.2.0",
+      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.0.tgz",
+      "dependencies": {
+        "sax": {
+          "version": "1.2.1",
+          "from": "sax@>=0.1.1",
+          "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz"
+        }
+      }
+    }
+  }
+}

+ 12 - 7
services/web/package.json

@@ -18,6 +18,7 @@
     "body-parser": "^1.13.1",
     "bufferedstream": "1.6.0",
     "connect-redis": "2.3.0",
+    "contentful": "^3.3.14",
     "cookie": "^0.2.3",
     "cookie-parser": "1.3.5",
     "csurf": "^1.8.3",
@@ -30,8 +31,9 @@
     "jade": "~1.3.1",
     "ldapjs": "^0.7.1",
     "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master",
+    "lodash": "^4.13.1",
     "lynx": "0.1.1",
-    "marked": "^0.3.3",
+    "marked": "^0.3.5",
     "method-override": "^2.3.3",
     "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.3.0",
     "mimelib": "0.2.14",
@@ -54,32 +56,35 @@
     "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
     "sixpack-client": "^1.0.0",
     "temp": "^0.8.3",
-    "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master",
     "underscore": "1.6.0",
-    "underscore.string": "^3.0.2",
     "v8-profiler": "^5.2.3",
     "xml2js": "0.2.0"
   },
   "devDependencies": {
     "bunyan": "0.22.1",
+    "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master",
     "chai": "",
     "chai-spies": "",
     "grunt": "0.4.1",
     "grunt-available-tasks": "0.4.1",
     "grunt-bunyan": "0.5.0",
-    "grunt-concurrent": "0.4.3",
     "grunt-contrib-clean": "0.5.0",
     "grunt-contrib-coffee": "0.10.0",
     "grunt-contrib-less": "0.9.0",
     "grunt-contrib-requirejs": "0.4.1",
-    "grunt-execute": "0.1.5",
+    "grunt-contrib-watch": "^1.0.0",
+    "grunt-env": "0.4.4",
+    "clean-css": "^3.4.18",
+    "grunt-exec": "^0.4.7",
+    "grunt-execute": "^0.2.2",
     "grunt-file-append": "0.0.6",
     "grunt-git-rev-parse": "^0.1.4",
     "grunt-mocha-test": "0.9.0",
+    "grunt-newer": "^1.2.0",
+    "grunt-parallel": "^0.5.1",
     "grunt-sed": "^0.1.1",
     "sandboxed-module": "0.2.0",
     "sinon": "",
-    "timekeeper": "",
-    "grunt-env": "0.4.4"
+    "timekeeper": ""
   }
 }

+ 12 - 1
services/web/public/coffee/ide.coffee

@@ -42,7 +42,7 @@ define [
 	ReferencesManager
 ) ->
 
-	App.controller "IdeController", ($scope, $timeout, ide, localStorage) ->
+	App.controller "IdeController", ($scope, $timeout, ide, localStorage, event_tracking) ->
 		# Don't freak out if we're already in an apply callback
 		$scope.$originalApply = $scope.$apply
 		$scope.$apply = (fn = () ->) ->
@@ -69,6 +69,17 @@ define [
 
 		$scope.chat = {}
 
+		# Tracking code.
+		$scope.$watch "ui.view", (newView, oldView) ->
+			if newView? and newView != "editor" and newView != "pdf"
+				event_tracking.sendCountlyOnce "ide-open-view-#{ newView }-once" 
+
+		$scope.$watch "ui.chatOpen", (isOpen) ->
+			event_tracking.sendCountlyOnce "ide-open-chat-once" if isOpen
+
+		$scope.$watch "ui.leftMenuShown", (isOpen) ->
+			event_tracking.sendCountlyOnce "ide-open-left-menu-once" if isOpen
+		# End of tracking code.
 
 		window._ide = ide
 

+ 4 - 2
services/web/public/coffee/ide/editor/directives/aceEditor.coffee

@@ -74,15 +74,17 @@ define [
 				editor.commands.removeCommand "foldall"
 				
 				# For European keyboards, the / is above 7 so needs Shift pressing.
-				# This comes through as Ctrl-Shift-/ which is mapped to toggleBlockComment.
+				# This comes through as Command-Shift-/ on OS X, which is mapped to 
+				# toggleBlockComment.
 				# This doesn't do anything for LaTeX, so remap this to togglecomment to
 				# work for European keyboards as normal.
+				# On Windows, the key combo comes as Ctrl-Shift-7.
 				editor.commands.removeCommand "toggleBlockComment"
 				editor.commands.removeCommand "togglecomment"
 				
 				editor.commands.addCommand {
 					name: "togglecomment",
-					bindKey: { win: "Ctrl-/|Ctrl-Shift-/", mac: "Command-/|Command-Shift-/" },
+					bindKey: { win: "Ctrl-/|Ctrl-Shift-7", mac: "Command-/|Command-Shift-/" },
 					exec: (editor) -> editor.toggleCommentLines(),
 					multiSelectAction: "forEachLine",
 					scrollIntoView: "selectionPart"

+ 2 - 0
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee

@@ -134,6 +134,8 @@ define [
 						editor.completer.showPopup(editor)
 						editor.completer.cancelContextMenu()
 						$(editor.completer.popup?.container).css({'font-size': @$scope.fontSize + 'px'})
+						if editor.completer?.completions?.filtered?.length == 0
+							editor.completer.detach()
 					bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space"
 				}
 

+ 3 - 1
services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee

@@ -2,8 +2,10 @@ define [
 	"base"
 	"ace/ace"
 ], (App) ->
-	App.controller "HotkeysController", ($scope, $modal) ->
+	App.controller "HotkeysController", ($scope, $modal, event_tracking) ->
 		$scope.openHotkeysModal = ->
+			event_tracking.sendCountly "ide-open-hotkeys-modal"
+
 			$modal.open {
 				templateUrl: "hotkeysModalTemplate"
 				controller:  "HotkeysModalController"

+ 20 - 0
services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee

@@ -0,0 +1,20 @@
+define [
+	"libs/latex-log-parser"
+	"ide/human-readable-logs/HumanReadableLogsRules"
+], (LogParser, ruleset) ->
+	parse : (rawLog, options) ->
+		parsedLogEntries = LogParser.parse(rawLog, options)
+
+		_getRule = (logMessage) ->
+			return rule for rule in ruleset when rule.regexToMatch.test logMessage
+
+		for entry in parsedLogEntries.all
+			ruleDetails = _getRule entry.message
+
+			if (ruleDetails?)
+				entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1) if ruleDetails.regexToMatch?
+				
+				entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint?
+				entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL?
+	
+		return parsedLogEntries

+ 91 - 0
services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee

@@ -0,0 +1,91 @@
+define -> [
+		regexToMatch: /Misplaced alignment tab character \&/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/Misplaced_alignment_tab_character_%26"
+		humanReadableHint: """
+			You have placed an alignment tab character '&' in the wrong place. If you want to align something, you must write it inside an align environment such as \\begin{align} \u2026 \\end{align}, \\begin{tabular} \u2026 \\end{tabular}, etc. If you want to write an ampersand '&' in text, you must write \\& instead.
+		"""
+	,
+		regexToMatch: /Extra alignment tab has been changed to \\cr/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr"
+		humanReadableHint: """
+			You have written too many alignment tabs in a table, causing one of them to be turned into a line break. Make sure you have specified the correct number of columns in your <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Tables\">table</a>.
+		"""
+	,
+		regexToMatch: /Display math should end with \$\$/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/Display_math_should_end_with_$$"
+		humanReadableHint: """
+			You have forgotten a $ sign at the end of 'display math' mode. When writing in display math mode, you must always math write inside $$ \u2026 $$. Check that the number of $s match around each math expression.
+		"""
+	,
+		regexToMatch: /Missing [{$] inserted./
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/Missing_$_inserted"
+		humanReadableHint: """
+			Check that your $'s match around math expressions. If they do, then you've probably used a symbol in normal text that needs to be in math mode. Symbols such as subscripts ( _ ), integrals ( \\int ), Greek letters ( \\alpha, \\beta, \\delta ), and modifiers (\\vec{x}, \\tilde{x} ) must be written in math mode. See the full list <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors/Missing_$_inserted \">here</a>.If you intended to use mathematics mode, then use $ \u2026 $ for 'inline math mode', $$ \u2026 $$ for 'display math mode' or alternatively \begin{math} \u2026 \end{math}.
+		"""
+	,
+		regexToMatch: /(undefined )?[rR]eference(s)?.+(undefined)?/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/There_were_undefined_references"
+		humanReadableHint: """
+			You have referenced something which has not yet been labelled. If you have labelled it already, make sure that what is written inside \\ref{...} is the same as what is written inside \\label{...}.
+		"""
+	,
+		regexToMatch: /Citation .+ on page .+ undefined on input line .+/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/Citation_XXX_on_page_XXX_undefined_on_input_line_XXX"
+		humanReadableHint: """
+			You have cited something which is not included in your bibliography. Make sure that the citation (\\cite{...}) has a corresponding key in your bibliography, and that both are spelled the same way.
+		"""
+	,
+		regexToMatch: /(Label .+)? multiply[ -]defined( labels)?/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/There_were_multiply-defined_labels"
+		humanReadableHint: """
+			You have used the same label more than once. Check that each \\label{...} labels only one item.
+		"""
+	,
+		regexToMatch: /`!?h' float specifier changed to `!?ht'/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/%60!h%27_float_specifier_changed_to_%60!ht%27"
+		humanReadableHint: """
+			The float specifier 'h' is too strict of a demand for LaTeX to place your float in a nice way here. Try relaxing it by using 'ht', or even 'htbp' if necessary. If you want to try keep the float here anyway, check out the <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Positioning_of_Figures\">float package</a>.
+		"""
+	,
+		regexToMatch: /No positions in optional float specifier/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/No_positions_in_optional_float_specifier"
+		humanReadableHint: """
+			You have forgotten to include a float specifier, which tells LaTeX where to position your figure. To fix this, either insert a float specifier inside the square brackets (e.g. \begin{figure}[h]), or remove the square brackets (e.g. \begin{figure}). Find out more about float specifiers <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Positioning_of_Figures\">here</a>.
+		"""
+	,
+		regexToMatch: /Undefined control sequence/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/Undefined_control_sequence"
+		humanReadableHint: """
+			The compiler is having trouble understanding a command you have used. Check that the command is spelled correctly. If the command is part of a package, make sure you have included the package in your preamble using \\usepackage{...}.
+		"""
+	,
+		regexToMatch: /File .+ not found/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/File_XXX_not_found_on_input_line_XXX"
+		humanReadableHint: """
+			The compiler cannot find the file you want to include. Make sure that you have <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Including_images_in_ShareLaTeX\">uploaded the file</a> and <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors/File_XXX_not_found_on_input_line_XXX.\">specified the file location correctly</a>.
+		"""
+	,
+		regexToMatch: /LaTeX Error: Unknown graphics extension: \..+/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.XXX"
+		humanReadableHint: """
+			The compiler does not recognise the file type of one of your images. Make sure you are using a <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.gif.\">supported image format</a> for your choice of compiler, and check that there are no periods (.) in the name of your image.
+		"""
+	,
+		regexToMatch: /LaTeX Error: Unknown float option `H'/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60H%27"
+		humanReadableHint: """
+			The compiler isn't recognizing the float option 'H'. Include \\usepackage{float} in your preamble to fix this.
+		"""
+	,
+		regexToMatch: /LaTeX Error: Unknown float option `q'/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60q%27"
+		humanReadableHint: """
+			You have used a float specifier which the compiler does not understand. You can learn more about the different float options available for placing figures <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Positioning_of_Figures\">here</a>.
+		"""
+	,
+		regexToMatch: /LaTeX Error: \\math.+ allowed only in math mode/
+		extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_%5Cmathrm_allowed_only_in_math_mode"
+		humanReadableHint: """
+			You have used a font command which is only available in math mode. To use this command, you must be in maths mode (E.g. $ \u2026 $ or \\begin{math} \u2026 \\end{math}). If you want to use it outside of math mode, use the text version instead: \\textrm, \\textit, etc.
+		"""
+]

+ 150 - 38
services/web/public/coffee/ide/pdf/controllers/PdfController.coffee

@@ -1,15 +1,67 @@
 define [
 	"base"
-	"libs/latex-log-parser"
+	"ace/ace"
+	"ide/human-readable-logs/HumanReadableLogs"
 	"libs/bib-log-parser"
-], (App, LogParser, BibLogParser) ->
-	App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, localStorage) ->
+	"services/log-hints-feedback"
+], (App, Ace, HumanReadableLogs, BibLogParser) ->
+	App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, logHintsFeedback, localStorage) ->
 
+		# enable per-user containers by default
+		perUserCompile = true
 		autoCompile = true
 
 		# pdf.view = uncompiled | pdf | errors
 		$scope.pdf.view = if $scope?.pdf?.url then 'pdf' else 'uncompiled'
 		$scope.shouldShowLogs = false
+		$scope.wikiEnabled = window.wikiEnabled;
+
+		# view logic to check whether the files dropdown should "drop up" or "drop down"
+		$scope.shouldDropUp = false
+
+		logsContainerEl	= document.querySelector ".pdf-logs"
+		filesDropdownEl	= logsContainerEl?.querySelector ".files-dropdown"
+
+		# get the top coordinate of the files dropdown as a ratio (to the logs container height)
+		# logs container supports scrollable content, so it's possible that ratio > 1.
+		getFilesDropdownTopCoordAsRatio = () ->
+			 filesDropdownEl?.getBoundingClientRect().top / logsContainerEl?.getBoundingClientRect().height
+
+		$scope.$watch "shouldShowLogs", (shouldShow) ->
+			if shouldShow
+				$scope.$applyAsync () -> 
+					$scope.shouldDropUp = getFilesDropdownTopCoordAsRatio() > 0.65
+
+		# log hints tracking
+		$scope.logHintsNegFeedbackValues = logHintsFeedback.feedbackOpts
+		
+		$scope.trackLogHintsLearnMore = () ->
+			event_tracking.sendCountly "logs-hints-learn-more"
+
+		trackLogHintsFeedback = (isPositive, hintId) ->
+			event_tracking.send "log-hints", (if isPositive then "feedback-positive" else "feedback-negative"), hintId
+			event_tracking.sendCountly (if isPositive then "log-hints-feedback-positive" else "log-hints-feedback-negative"), { hintId }
+
+		$scope.trackLogHintsNegFeedbackDetails = (hintId, feedbackOpt, feedbackOtherVal) ->
+			logHintsFeedback.submitFeedback hintId, feedbackOpt, feedbackOtherVal
+
+		$scope.trackLogHintsPositiveFeedback = (hintId) -> trackLogHintsFeedback true, hintId
+		$scope.trackLogHintsNegativeFeedback = (hintId) -> trackLogHintsFeedback false, hintId
+
+		if ace.require("ace/lib/useragent").isMac
+			$scope.modifierKey = "Cmd"
+		else
+			$scope.modifierKey = "Ctrl"
+
+		# utility for making a query string from a hash, could use jquery $.param
+		createQueryString = (args) ->
+			qs_args = ("#{k}=#{v}" for k, v of args)
+			if qs_args.length then "?" + qs_args.join("&") else ""
+
+		$scope.stripHTMLFromString = (htmlStr) ->
+   			tmp = document.createElement("DIV")
+   			tmp.innerHTML = htmlStr
+   			return tmp.textContent || tmp.innerText || ""
 
 		$scope.$on "project:joined", () ->
 			return if !autoCompile
@@ -28,13 +80,14 @@ define [
 
 		sendCompileRequest = (options = {}) ->
 			url = "/project/#{$scope.project_id}/compile"
+			params = {}
 			if options.isAutoCompile
-				url += "?auto_compile=true"
+				params["auto_compile"]=true
 			return $http.post url, {
 				rootDoc_id: options.rootDocOverride_id or null
 				draft: $scope.draft
 				_csrf: window.csrfToken
-			}
+			}, {params: params}
 
 		parseCompileResponse = (response) ->		
 
@@ -42,20 +95,36 @@ define [
 			$scope.pdf.error      = false
 			$scope.pdf.timedout   = false
 			$scope.pdf.failure    = false
-			$scope.pdf.projectTooLarge = false
 			$scope.pdf.url        = null
 			$scope.pdf.clsiMaintenance = false
 			$scope.pdf.tooRecentlyCompiled = false
 			$scope.pdf.renderingError = false
+			$scope.pdf.projectTooLarge = false
+			$scope.pdf.compileTerminated = false
 
 			# make a cache to look up files by name
 			fileByPath = {}
-			for file in response.outputFiles
-				fileByPath[file.path] = file
+			if response?.outputFiles?
+				for file in response?.outputFiles
+					fileByPath[file.path] = file
+
+			# prepare query string
+			qs = {}
+			# add a query string parameter for the compile group
+			if response.compileGroup?
+				ide.compileGroup = qs.compileGroup = response.compileGroup
+			# add a query string parameter for the clsi server id
+			if response.clsiServerId?
+				ide.clsiServerId = qs.clsiserverid = response.clsiServerId
 
 			if response.status == "timedout"
 				$scope.pdf.view = 'errors'
 				$scope.pdf.timedout = true
+				fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
+			else if response.status == "terminated"
+				$scope.pdf.view = 'errors'
+				$scope.pdf.compileTerminated = true
+				fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
 			else if response.status == "autocompile-backoff"
 				$scope.pdf.view = 'uncompiled'
 			else if response.status == "project-too-large"
@@ -72,12 +141,13 @@ define [
 			else if response.status == "too-recently-compiled"
 				$scope.pdf.view = 'errors'
 				$scope.pdf.tooRecentlyCompiled = true
+			else if response.status == "validation-problems"
+				$scope.pdf.view = "validation-problems"
+				$scope.pdf.validation = response.validationProblems
 			else if response.status == "success"
 				$scope.pdf.view = 'pdf'
 				$scope.shouldShowLogs = false
 
-				# prepare query string
-				qs = {}
 				# define the base url. if the pdf file has a build number, pass it to the clsi in the url
 				if fileByPath['output.pdf']?.url?
 					$scope.pdf.url = fileByPath['output.pdf'].url
@@ -89,17 +159,11 @@ define [
 				# check if we need to bust cache (build id is unique so don't need it in that case)
 				if not fileByPath['output.pdf']?.build?
 					qs.cache_bust = "#{Date.now()}"
-				# add a query string parameter for the compile group
-				if response.compileGroup?
-					$scope.pdf.compileGroup = response.compileGroup
-					qs.compileGroup = "#{$scope.pdf.compileGroup}"
-				if response.clsiServerId?
-					qs.clsiserverid = response.clsiServerId
-					ide.clsiServerId = response.clsiServerId
 				# convert the qs hash into a query string and append it
-				qs_args = ("#{k}=#{v}" for k, v of qs)
-				$scope.pdf.qs = if qs_args.length then "?" + qs_args.join("&") else ""
-				$scope.pdf.url += $scope.pdf.qs
+				$scope.pdf.url += createQueryString qs
+				# Save all downloads as files
+				qs.popupDownload = true
+				$scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs)
 
 				fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
 
@@ -108,17 +172,18 @@ define [
 
 			if !response.outputFiles?
 				return
+
+			# prepare list of output files for download dropdown
+			qs = {}
+			if response.clsiServerId?
+				qs.clsiserverid = response.clsiServerId
 			for file in response.outputFiles
 				if IGNORE_FILES.indexOf(file.path) == -1
-					# Turn 'output.blg' into 'blg file'.
-					if file.path.match(/^output\./)
-						file.name = "#{file.path.replace(/^output\./, "")} file"
-					else
-						file.name = file.path
-					file.url = "/project/#{project_id}/output/#{file.path}"
-					if response.clsiServerId?
-						file.url = file.url + "?clsiserverid=#{response.clsiServerId}"
-					$scope.pdf.outputFiles.push file
+					$scope.pdf.outputFiles.push {
+						# Turn 'output.blg' into 'blg file'.
+						name: if file.path.match(/^output\./) then "#{file.path.replace(/^output\./, "")} file" else file.path
+						url: "/project/#{project_id}/output/#{file.path}" + createQueryString qs
+					}
 
 
 		fetchLogs = (logFile, blgFile) ->
@@ -127,14 +192,17 @@ define [
 				opts =
 					method:"GET"
 					params:
-						build:file.build
+						compileGroup:ide.compileGroup
 						clsiserverid:ide.clsiServerId
-				if file.url?  # FIXME clean this up when we have file.urls out consistently
+				if file?.url?  # FIXME clean this up when we have file.urls out consistently
 					opts.url = file.url
 				else if file?.build?
 					opts.url = "/project/#{$scope.project_id}/build/#{file.build}/output/#{name}"
 				else
 					opts.url = "/project/#{$scope.project_id}/output/#{name}"
+				# check if we need to bust cache (build id is unique so don't need it in that case)
+				if not file?.build?
+					opts.params.cache_bust = "#{Date.now()}"
 				return $http(opts)
 
 			# accumulate the log entries
@@ -150,7 +218,7 @@ define [
 			# use the parsers for each file type
 			processLog = (log) ->
 				$scope.pdf.rawLog = log
-				{errors, warnings, typesetting} = LogParser.parse(log, ignoreDuplicates: true)
+				{errors, warnings, typesetting} = HumanReadableLogs.parse(log, ignoreDuplicates: true)
 				all = [].concat errors, warnings, typesetting
 				accumulateResults {all, errors, warnings}
 
@@ -204,7 +272,7 @@ define [
 			return null
 
 		normalizeFilePath = (path) ->
-			path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "")
+			path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}(-[0-9a-f]{24})?\/(\.\/)?/, "")
 			path = path.replace(/^\/compile\//, "")
 
 			rootDocDirname = ide.fileTreeManager.getRootDocDirname()
@@ -215,6 +283,9 @@ define [
 
 		$scope.recompile = (options = {}) ->
 			return if $scope.pdf.compiling
+
+			event_tracking.sendCountlySampled "editor-recompile-sampled", options
+
 			$scope.pdf.compiling = true
 
 			ide.$scope.$broadcast("flush-changes")
@@ -234,6 +305,21 @@ define [
 
 		# This needs to be public.
 		ide.$scope.recompile = $scope.recompile
+		# This method is a simply wrapper and exists only for tracking purposes.
+		ide.$scope.recompileViaKey = () ->
+			$scope.recompile { keyShortcut: true }
+
+		$scope.stop = () ->
+			return if !$scope.pdf.compiling
+
+			$http {
+				url: "/project/#{$scope.project_id}/compile/stop"
+				method: "POST"
+				params:
+					clsiserverid:ide.clsiServerId
+				headers:
+					"X-Csrf-Token": window.csrfToken
+			}
 
 		$scope.clearCache = () ->
 			$http {
@@ -247,6 +333,7 @@ define [
 
 		$scope.toggleLogs = () ->
 			$scope.shouldShowLogs = !$scope.shouldShowLogs
+			event_tracking.sendCountlyOnce "ide-open-logs-once" if $scope.shouldShowLogs
 
 		$scope.showPdf = () ->
 			$scope.pdf.view = "pdf"
@@ -254,6 +341,7 @@ define [
 
 		$scope.toggleRawLog = () ->
 			$scope.pdf.showRawLog = !$scope.pdf.showRawLog
+			event_tracking.sendCountly "logs-view-raw" if $scope.pdf.showRawLog
 
 		$scope.openClearCacheModal = () ->
 			modalInstance = $modal.open(
@@ -287,10 +375,16 @@ define [
 
 		$scope.startFreeTrial = (source) ->
 			ga?('send', 'event', 'subscription-funnel', 'compile-timeout', source)
+
+			event_tracking.sendCountly "subscription-start-trial", { source }
+
 			window.open("/user/subscription/new?planCode=student_free_trial_7_days")
 			$scope.startedFreeTrial = true
 
 	App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->
+		# enable per-user containers by default
+		perUserCompile = true
+
 		synctex =
 			syncToPdf: (cursorPosition) ->
 				deferred = $q.defer()
@@ -339,18 +433,35 @@ define [
 					deferred.reject()
 					return deferred.promise
 
+				# FIXME: this actually works better if it's halfway across the
+				# page (or the visible part of the page). Synctex doesn't
+				# always find the right place in the file when the point is at
+				# the edge of the page, it sometimes returns the start of the
+				# next paragraph instead.
+				h = position.offset.left
+
+				# Compute the vertical position to pass to synctex, which
+				# works with coordinates increasing from the top of the page
+				# down.  This matches the browser's DOM coordinate of the
+				# click point, but the pdf position is measured from the
+				# bottom of the page so we need to invert it.
+				if options.fromPdfPosition and position.pageSize?.height?
+					v = (position.pageSize.height - position.offset.top) or 0 # measure from pdf point (inverted)
+				else
+					v = position.offset.top or 0 # measure from html click position
+
 				# It's not clear exactly where we should sync to if it wasn't directly
 				# clicked on, but a little bit down from the very top seems best.
 				if options.includeVisualOffset
-					position.offset.top = position.offset.top + 80
+					v += 72 # use the same value as in pdfViewer highlighting visual offset
 
 				$http({
 						url: "/project/#{ide.project_id}/sync/pdf",
 						method: "GET",
 						params: {
 							page: position.page + 1
-							h: position.offset.left.toFixed(2)
-							v: position.offset.top.toFixed(2)
+							h: h.toFixed(2)
+							v: v.toFixed(2)
 							clsiserverid:ide.clsiServerId
 						}
 					})
@@ -380,14 +491,15 @@ define [
 
 		$scope.syncToCode = () ->
 			synctex
-				.syncToCode($scope.pdf.position, includeVisualOffset: true)
+				.syncToCode($scope.pdf.position, includeVisualOffset: true, fromPdfPosition: true)
 				.then (data) ->
 					{doc, line} = data
 					ide.editorManager.openDoc(doc, gotoLine: line)
 	]
 
-	App.controller "PdfLogEntryController", ["$scope", "ide", ($scope, ide) ->
+	App.controller "PdfLogEntryController", ["$scope", "ide", "event_tracking", ($scope, ide, event_tracking) ->
 		$scope.openInEditor = (entry) ->
+			event_tracking.sendCountlyOnce "logs-jump-to-location-once"
 			entity = ide.fileTreeManager.findEntityByPath(entry.file)
 			return if !entity? or entity.type != "doc"
 			if entry.line?

+ 12 - 66
services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee

@@ -1,23 +1,14 @@
 define [
 	"base"
 	"ide/pdfng/directives/pdfViewer"
-	"text!libs/pdfListView/TextLayer.css"
-	"text!libs/pdfListView/AnnotationsLayer.css"
-	"text!libs/pdfListView/HighlightsLayer.css"
 ], (
 	App
 	pdfViewer
-	textLayerCss
-	annotationsLayerCss
-	highlightsLayerCss
+
 ) ->
 	if PDFJS?
 		PDFJS.workerSrc = window.pdfJsWorkerPath
 
-	style = $("<style/>")
-	style.text(textLayerCss + "\n" + annotationsLayerCss + "\n" + highlightsLayerCss)
-	$("body").append(style)
-
 	App.directive "pdfng", ["$timeout", "localStorage", ($timeout, localStorage) ->
 		return {
 			scope: {
@@ -27,15 +18,6 @@ define [
 					"dblClickCallback": "="
 			}
 			link: (scope, element, attrs) ->
-				# pdfListView = new PDFListView element.find(".pdfjs-viewer")[0],
-				#		textLayerBuilder: TextLayerBuilder
-				#		annotationsLayerBuilder: AnnotationsLayerBuilder
-				#		highlightsLayerBuilder: HighlightsLayerBuilder
-				#		ondblclick: (e) -> onDoubleClick(e)
-				#		# logLevel: PDFListView.Logger.DEBUG
-				# pdfListView.listView.pageWidthOffset = 20
-				# pdfListView.listView.pageHeightOffset = 20
-
 				scope.loading = false
 				scope.pleaseJumpTo = null
 				scope.scale = null
@@ -50,9 +32,11 @@ define [
 						scope.scale = { scaleMode: 'scale_mode_fit_width' }
 
 					if (position = localStorage("pdf.position.#{attrs.key}"))
-						scope.position = { page: +position.page, offset: { "top": +position.offset.top, "left": +position.offset.left } }
-
-					#scope.position = pdfListView.getPdfPosition(true)
+						scope.position =
+							page: +position.page,
+							offset:
+								"top": +position.offset.top
+								"left": +position.offset.left
 
 					scope.$on "$destroy", () =>
 						localStorage "pdf.scale", scope.scale
@@ -78,56 +62,18 @@ define [
 				scope.$watch "pdfSrc", (url) ->
 					if url
 						scope.loading = true
-						# console.log 'pdfSrc =', url
+						scope.loaded = false
+						scope.progress = 1
 						initializePosition()
 						flashControls()
-						# pdfListView
-						#		.loadPdf(url, onProgress)
-						#		.then () ->
-						#			scope.$apply () ->
-						#				scope.loading = false
-						#				delete scope.progress
-						#				initializePosition()
-						#				flashControls()
-				
+
 				scope.$on "loaded", () ->
+					scope.loaded = true
 					scope.progress = 100
-					scope.$apply()
 					$timeout () ->
 						scope.loading = false
 						delete scope.progress
-					, 250
-
-				#scope.$watch "highlights", (areas) ->
-					# console.log 'got HIGHLIGHTS in pdfJS', areas
-					
-					# return if !areas?
-					# highlights = for area in areas or []
-					# 	{
-					# 		page: area.page
-					# 		highlight:
-					# 			left: area.h
-					# 			top: area.v
-					# 			height: area.height
-					# 			width: area.width
-					# 	}
-
-					# if highlights.length > 0
-					# 	first = highlights[0]
-					# 	position = {
-					# 		page: first.page
-					# 		offset:
-					# 			left: first.highlight.left
-					# 			top: first.highlight.top - 80
-					# 	}
-					# 	console.log 'position is', position, 'in highlights'
-					# 	scope.pleaseJumpTo = position
-					# pdfListView.clearHighlights()
-					# pdfListView.setHighlights(highlights, true)
-
-					# setTimeout () =>
-					#		pdfListView.clearHighlights()
-					# , 1000
+					, 500
 
 				scope.fitToHeight = () ->
 					scale = angular.copy (scope.scale)
@@ -155,10 +101,10 @@ define [
 					for event in attrs.resizeOn.split(",")
 						scope.$on event, (e) ->
 							#console.log 'got a resize event', event, e
-							#
 
 				scope.$on 'progress', (event, progress) ->
 					scope.$apply () ->
+						return if scope.loaded
 						scope.progress = Math.floor(progress.loaded/progress.total*100)
 						scope.progress = 100 if scope.progress > 100
 						scope.progress = 0 if scope.progress < 0

+ 2 - 2
services/web/public/coffee/ide/pdfng/directives/pdfPage.coffee

@@ -3,7 +3,7 @@ define [
 ], (App) ->
 	# App = angular.module 'pdfPage', ['pdfHighlights']
 
-	App.directive 'pdfPage', ['$timeout', 'pdfHighlights', ($timeout, pdfHighlights) ->
+	App.directive 'pdfPage', ['$timeout', 'pdfHighlights', 'pdfSpinner', ($timeout, pdfHighlights, pdfSpinner) ->
 		{
 			require: '^pdfViewer',
 			template: '''
@@ -82,7 +82,7 @@ define [
 					if scope.timeoutHandler
 						$timeout.cancel(scope.timeoutHandler)
 						highlightsLayer.clearHighlights()
-						scope.timeoutHandler
+						scope.timeoutHandler = null
 
 					# console.log 'got highlight watch in pdfPage', scope.page
 					pageHighlights = (h for h in highlights when h.page == scope.page.pageNum)

+ 126 - 78
services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee

@@ -8,11 +8,13 @@ define [
 		class PDFRenderer
 			JOB_QUEUE_INTERVAL: 25
 			PAGE_LOAD_TIMEOUT: 60*1000
-			PAGE_RENDER_TIMEOUT: 60*1000
+			INDICATOR_DELAY1: 100  # time to delay before showing the indicator
+			INDICATOR_DELAY2: 250  # time until the indicator starts animating
 
 			constructor: (@url, @options) ->
 				# PDFJS.disableFontFace = true  # avoids repaints, uses worker more
-				# PDFJS.disableAutoFetch = true # enable this to prevent loading whole file
+				if @options.disableAutoFetch
+					PDFJS.disableAutoFetch = true # prevent loading whole file
 				# PDFJS.disableStream
 				# PDFJS.disableRange
 				@scale = @options.scale || 1
@@ -28,15 +30,22 @@ define [
 				@errorCallback = @options.errorCallback
 				@pageSizeChangeCallback = @options.pageSizeChangeCallback
 				@pdfjs.promise.catch (exception) =>
-					# console.log 'ERROR in get document', exception
+					# error getting document
 					@errorCallback(exception)
 
 			resetState: () ->
-				@complete = []
-				@timeout = []
-				@pageLoad = []
-				@renderTask = []
 				@renderQueue = []
+				clearTimeout @queueTimer if @queueTimer?
+				# clear any existing timers, render tasks
+				for timer in @spinTimer or []
+					clearTimeout timer
+				for page in @pageState or []
+					page?.loadTask?.cancel()
+					page?.renderTask?.cancel()
+				# initialise/reset the state
+				@pageState = []
+				@spinTimer = []     # timers for starting the spinners (to avoid jitter)
+				@spinTimerDone = [] # array of pages where the spinner has activated
 				@jobs = 0
 
 			getNumPages: () ->
@@ -45,7 +54,6 @@ define [
 
 			getPage: (pageNum) ->
 				@document.then (pdfDocument) ->
-					# console.log 'got pdf document, now getting Page', pageNum
 					pdfDocument.getPage(pageNum)
 
 			getPdfViewport: (pageNum, scale) ->
@@ -80,14 +88,14 @@ define [
 				@resetState()
 
 			triggerRenderQueue: (interval = @JOB_QUEUE_INTERVAL) ->
+				if @queueTimer?
+					clearTimeout @queueTimer
 				@queueTimer = setTimeout () =>
 					@queueTimer = null
 					@processRenderQueue()
 				, interval
 
-			removeCompletedJob: (taskRef, pagenum) ->
-				# may need to clean up deferred object here
-				delete taskRef[pagenum]
+			removeCompletedJob: (pagenum) ->
 				@jobs = @jobs - 1
 				@triggerRenderQueue(0)
 
@@ -109,69 +117,129 @@ define [
 				@renderQueue.push current
 				@processRenderQueue()
 
+			getPageDetails: (page) ->
+				return [page.element.canvas, page.pagenum]
+
+			# handle the loading indicators for each page
+
+			startIndicators: () ->
+				# make an array of the pages in the queue
+				@queuedPages = []
+				@queuedPages[page.pagenum] = true for page in @renderQueue
+				# clear any unfinished spinner timers on pages that aren't in the queue any more
+				for pagenum of @spinTimer when not @queuedPages[pagenum]
+					clearTimeout @spinTimer[pagenum]
+					delete @spinTimer[pagenum]
+				# add indicators for any new pages in the current queue
+				for page in @renderQueue when not @spinTimer[page.pagenum] and not @spinTimerDone[page.pagenum]
+					@startIndicator page
+
+			startIndicator: (page) ->
+				[canvas, pagenum] = @getPageDetails page
+				canvas.addClass('pdfng-loading')
+				@spinTimer[pagenum] = setTimeout () =>
+					for queuedPage in @renderQueue
+						if pagenum == queuedPage.pagenum
+							@spinner.add(canvas, {static:true})
+							@spinTimerDone[pagenum] = true
+							break
+					delete @spinTimer[pagenum]
+				, @INDICATOR_DELAY1
+
+			updateIndicator: (page) ->
+				[canvas, pagenum] = @getPageDetails page
+				# did the spinner insert itself already?
+				if @spinTimerDone[pagenum]
+					@spinTimer[pagenum] = setTimeout () =>
+						@spinner.start(canvas)
+						delete @spinTimer[pagenum]
+					, @INDICATOR_DELAY2
+				else
+					# stop the existing spin timer
+					clearTimeout @spinTimer[pagenum]
+					# start a new one which will also start spinning
+					@spinTimer[pagenum] = setTimeout () =>
+						@spinner.add(canvas, {static:true})
+						@spinTimerDone[pagenum] = true
+						@spinTimer[pagenum] = setTimeout () =>
+							@spinner.start(canvas)
+							delete @spinTimer[pagenum]
+						, @INDICATOR_DELAY2
+					, @INDICATOR_DELAY1
+
+			clearIndicator: (page) ->
+				[canvas, pagenum] = @getPageDetails page
+				@spinner.stop(canvas)
+				clearTimeout @spinTimer[pagenum]
+				delete @spinTimer[pagenum]
+				@spinTimerDone[pagenum] = true
+
+			# handle the queue of pages to be rendered
+
 			processRenderQueue: () ->
 				return if @shuttingDown
+				# mark all pages in the queue as loading
+				@startIndicators()
+				# bail out if there is already a render job running
 				return if @jobs > 0
-				current = @renderQueue.shift()
-				return unless current?
-				[element, pagenum] = [current.element, current.pagenum]
-				# if task is underway or complete, go to the next entry in the
-				# render queue
-				# console.log 'processing renderq', pagenum, @renderTask[pagenum], @complete[pagenum]
-				if @pageLoad[pagenum] or @renderTask[pagenum] or @complete[pagenum]
-					@processRenderQueue()
-					return
+				# take the first page in the queue
+				page = @renderQueue.shift()
+				# check if it is in action already
+				while page? and @pageState[page.pagenum]?
+					page = @renderQueue.shift()
+				return unless page?
+				[element, pagenum] = [page.element, page.pagenum]
 				@jobs = @jobs + 1
 
-				element.canvas.addClass('pdfng-loading')
-				spinTimer = setTimeout () =>
-					@spinner.add(element.canvas)
-				, 100
-
-				completeRef = @complete
-				renderTaskRef = @renderTask
-				# console.log 'started page load', pagenum
+				# update the spinner to make it spinning (signifies loading has begun)
+				@updateIndicator page
 
 				timedOut = false
-				timer = $timeout () =>
-					Raven?.captureMessage?('pdfng page load timed out after ' + @PAGE_LOAD_TIMEOUT + 'ms (1% sample)') if Math.random() < 0.01
-					# console.log 'page load timed out', pagenum
+				timer = $timeout () => # page load timed out
+					return if loadTask.cancelled # return from cancelled page load
+					Raven?.captureMessage?('pdfng page load timed out after ' + @PAGE_LOAD_TIMEOUT + 'ms')
 					timedOut = true
-					clearTimeout(spinTimer)
-					@spinner.stop(element.canvas)
+					@clearIndicator page
 					# @jobs = @jobs - 1
 					# @triggerRenderQueue(0)
 					@errorCallback?('timeout')
 				, @PAGE_LOAD_TIMEOUT
 
-				@pageLoad[pagenum] = @getPage(pagenum)
+				loadTask = @getPage(pagenum)
 
-				@pageLoad[pagenum].then (pageObject) =>
-					# console.log 'in page load success', pagenum
+				loadTask.cancel = () ->
+					@cancelled = true
+
+				@pageState[pagenum] = pageState = { loadTask: loadTask }
+
+				loadTask.then (pageObject) =>
+					# page load success
 					$timeout.cancel(timer)
-					clearTimeout(spinTimer)
-					@renderTask[pagenum] = @doRender element, pagenum, pageObject
-					@renderTask[pagenum].then () =>
-						# complete
-						# console.log 'render task success', pagenum
-						completeRef[pagenum] = true
-						@removeCompletedJob renderTaskRef, pagenum
+					return if loadTask.cancelled # return from cancelled page load
+					pageState.renderTask = @doRender element, pagenum, pageObject
+					pageState.renderTask.then () =>
+						# render task success
+						@clearIndicator page
+						pageState.complete = true
+						delete pageState.renderTask
+						@removeCompletedJob pagenum
 					, () =>
-						# console.log 'render task failed', pagenum
-						# rejected
-						@removeCompletedJob renderTaskRef, pagenum
-				.catch (error) ->
-					# console.log 'in page load error', pagenum, 'timedOut=', timedOut
+						# render task failed
+						# could display an error icon
+						pageState.complete = false
+						delete pageState.renderTask
+						@removeCompletedJob pagenum
+				.catch (error) =>
+					# page load error
 					$timeout.cancel(timer)
-					clearTimeout(spinTimer)
-					# console.log 'ERROR', error
+					@clearIndicator page
 
 			doRender: (element, pagenum, page) ->
 				self = this
 				scale = @scale
 
 				if (not scale?)
-					# console.log 'scale is undefined, returning'
+					# scale is undefined, returning
 					return
 
 				canvas = $('<canvas class="pdf-canvas pdfng-rendering"></canvas>')
@@ -220,28 +288,15 @@ define [
 					navigateFn: @navigateFn
 				})
 
-				element.canvas.replaceWith(canvas)
-
-				# console.log 'staring page render', pagenum
-
 				result = page.render {
 					canvasContext: ctx
 					viewport: viewport
 					transform: [pixelRatio, 0, 0, pixelRatio, 0, 0]
 				}
 
-				timedOut = false
-
-				timer = $timeout () =>
-					Raven?.captureMessage?('pdfng page render timed out after ' + @PAGE_RENDER_TIMEOUT + 'ms (1% sample)') if Math.random() < 0.01
-					# console.log 'page render timed out', pagenum
-					timedOut = true
-					result.cancel()
-				, @PAGE_RENDER_TIMEOUT
-
 				result.then () ->
-					# console.log 'page rendered', pagenum
-					$timeout.cancel(timer)
+					# page render success
+					element.canvas.replaceWith(canvas)
 					canvas.removeClass('pdfng-rendering')
 					page.getTextContent().then (textContent) ->
 						textLayer.setTextContent textContent
@@ -252,24 +307,17 @@ define [
 					, (error) ->
 						self.errorCallback?(error)
 				.catch (error) ->
-					# console.log 'page render failed', pagenum, error
-					$timeout.cancel(timer)
-					if timedOut
-						# console.log 'calling ERROR callback - was timeout'
-						self.errorCallback?('timeout')
-					else if error != 'cancelled'
-						# console.log 'calling ERROR callback'
+					# page render failed
+					if error is 'cancelled'
+						return # do nothing when cancelled
+					else
 						self.errorCallback?(error)
 
 				return result
 
 			destroy: () ->
-				# console.log 'in pdf renderer destroy', @renderQueue
 				@shuttingDown = true
-				clearTimeout @queueTimer if @queueTimer?
-				@renderQueue = []
-				for task in @renderTask
-					task.cancel() if task?
+				@resetState()
 				@pdfjs.then (document) ->
 					document.cleanup()
 					document.destroy()

+ 5 - 4
services/web/public/coffee/ide/pdfng/directives/pdfSpinner.coffee

@@ -8,14 +8,15 @@ define [
 			constructor: () ->
 				# handler for spinners
 
-			add: (element) ->
-				h = element.height()
-				w = element.width()
+			add: (element, options) ->
 				size = 64
-				spinner = $('<div class="pdfng-spinner" style="position: absolute; top: 50%; left:50%; transform: translateX(-50%) translateY(-50%);"><i class="fa fa-spinner fa-spin" style="color: #999"></i></div>')
+				spinner = $('<div class="pdfng-spinner" style="position: absolute; top: 50%; left:50%; transform: translateX(-50%) translateY(-50%);"><i class="fa fa-spinner' + (if options?.static then '' else ' fa-spin') + '" style="color: #999"></i></div>')
 				spinner.css({'font-size' : size + 'px'})
 				element.append(spinner)
 
+			start: (element) ->
+				element.find('.fa-spinner').addClass('fa-spin')
+
 			stop: (element) ->
 				element.find('.fa-spinner').removeClass('fa-spin')
 

+ 41 - 20
services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee

@@ -27,8 +27,15 @@ define [
 			$scope.document.destroy() if $scope.document?
 			$scope.loadCount = if $scope.loadCount? then $scope.loadCount + 1 else 1
 			# TODO need a proper url manipulation library to add to query string
-			$scope.document = new PDFRenderer($scope.pdfSrc + '&pdfng=true' , {
+			url = $scope.pdfSrc
+			# add 'pdfng=true' to show that we are using the angular pdfjs viewer
+			queryStringExists = url.match(/\?/)
+			url = url + (if not queryStringExists then '?' else '&') + 'pdfng=true'
+			# for isolated compiles, load the pdf on-demand because nobody will overwrite it
+			onDemandLoading = true
+			$scope.document = new PDFRenderer(url, {
 				scale: 1,
+				disableAutoFetch: if onDemandLoading then true else undefined
 				navigateFn: (ref) ->
 					# this function captures clicks on the annotation links
 					$scope.navigateTo = ref
@@ -38,7 +45,7 @@ define [
 				loadedCallback: () ->
 					$scope.$emit 'loaded'
 				errorCallback: (error) ->
-					Raven?.captureMessage?('pdfng error ' + error + ' (1% sample)') if Math.random() < 0.01
+					Raven?.captureMessage?('pdfng error ' + error)
 					$scope.$emit 'pdf:error', error
 				pageSizeChangeCallback: (pageNum, deltaH) ->
 					$scope.$broadcast 'pdf:page:size-change', pageNum, deltaH
@@ -57,6 +64,7 @@ define [
 						result.pdfViewport.width
 					]
 					# console.log 'resolved q.all, page size is', result
+					$scope.$emit 'loaded'
 					$scope.numPages = result.numPages
 				.catch (error) ->
 					$scope.$emit 'pdf:error', error
@@ -187,7 +195,8 @@ define [
 			# console.log 'converted to offset = ', pdfOffset
 			newPosition = {
 				"page": topPageIdx,
-				"offset" : { "top" : pdfOffset[1], "left": 0	}
+				"offset" : { "top" : pdfOffset[1], "left": 0}
+				"pageSize": { "height": viewport.viewBox[3], "width": viewport.viewBox[2] }
 			}
 			return newPosition
 
@@ -208,6 +217,8 @@ define [
 			return $scope.document.getPdfViewport(page.pageNum).then (viewport) ->
 				page.viewport = viewport
 				pageOffset = viewport.convertToViewportPoint(offset.left, offset.top)
+				# if the passed-in position doesn't have the page height/width add them now
+				position.pageSize ?= {"height": viewport.viewBox[3], "width": viewport.viewBox[2]}
 				# console.log 'addition offset =', pageOffset
 				# console.log 'total', pageTop + pageOffset[1]
 				Math.round(pageTop + pageOffset[1] + currentScroll) ## 10 is margin
@@ -246,8 +257,8 @@ define [
 					# console.log 'layoutReady was resolved'
 
 				renderVisiblePages = () ->
-					pages = getVisiblePages()
-					# pages = getExtraPages visiblePages
+					visiblePages = getVisiblePages()
+					pages = getExtraPages visiblePages
 					scope.document.renderPages(pages)
 
 				getVisiblePages = () ->
@@ -264,20 +275,21 @@ define [
 
 				getExtraPages = (visiblePages) ->
 					extra = []
-					firstVisiblePage = visiblePages[0].pageNum
-					firstVisiblePageIdx = firstVisiblePage - 1
-					len = visiblePages.length
-					lastVisiblePage = visiblePages[len-1].pageNum
-					lastVisiblePageIdx = lastVisiblePage - 1
-					# first page after
-					if lastVisiblePageIdx + 1 < scope.pages.length
-						extra.push scope.pages[lastVisiblePageIdx + 1]
-					# page before
-					if firstVisiblePageIdx > 0
-						extra.push scope.pages[firstVisiblePageIdx - 1]
-					# second page after
-					if lastVisiblePageIdx + 2 < scope.pages.length
-						extra.push scope.pages[lastVisiblePageIdx + 2]
+					if visiblePages.length > 0
+						firstVisiblePage = visiblePages[0].pageNum
+						firstVisiblePageIdx = firstVisiblePage - 1
+						len = visiblePages.length
+						lastVisiblePage = visiblePages[len-1].pageNum
+						lastVisiblePageIdx = lastVisiblePage - 1
+						# first page after
+						if lastVisiblePageIdx + 1 < scope.pages.length
+							extra.push scope.pages[lastVisiblePageIdx + 1]
+						# page before
+						if firstVisiblePageIdx > 0
+							extra.push scope.pages[firstVisiblePageIdx - 1]
+						# second page after
+						if lastVisiblePageIdx + 2 < scope.pages.length
+							extra.push scope.pages[lastVisiblePageIdx + 2]
 					return visiblePages.concat extra
 
 				rescaleTimer = null
@@ -513,8 +525,17 @@ define [
 
 					first = highlights[0]
 
-					pageNum = scope.pages[first.page].pageNum
+					# switching between split and full pdf views can cause
+					# highlights to appear before rendering
+					if !scope.pages
+						return # ignore highlight scroll if still rendering
+
+					pageNum = scope.pages[first.page]?.pageNum
+
+					if !pageNum?
+						return # ignore highlight scroll if page not found
 
+					# use a visual offset of 72pt to match the offset in PdfController syncToCode
 					scope.document.getPdfViewport(pageNum).then (viewport) ->
 						position = {
 							page: first.page

+ 25 - 1
services/web/public/coffee/ide/settings/services/settings.coffee

@@ -1,19 +1,43 @@
 define [
 	"base"
 ], (App) ->
-	App.factory "settings", ["ide", (ide) ->
+	App.factory "settings", ["ide", "event_tracking", (ide, event_tracking) ->
 		return {
 			saveSettings: (data) ->
+				# Tracking code.
+				for key in Object.keys(data)
+					changedSetting = key
+					changedSettingVal = data[key]
+					event_tracking.sendCountly "setting-changed", { changedSetting, changedSettingVal }
+				# End of tracking code.
+				
 				data._csrf = window.csrfToken
 				ide.$http.post "/user/settings", data
 
+
 			saveProjectSettings: (data) ->
+				# Tracking code.
+				for key in Object.keys(data)
+					changedSetting = key
+					changedSettingVal = data[key]
+					event_tracking.sendCountly "project-setting-changed", { changedSetting, changedSettingVal}
+				# End of tracking code.
+				
 				data._csrf = window.csrfToken
 				ide.$http.post "/project/#{ide.project_id}/settings", data
+
 				
 			saveProjectAdminSettings: (data) ->
+				# Tracking code.
+				for key in Object.keys(data)
+					changedSetting = key
+					changedSettingVal = data[key]
+					event_tracking.sendCountly "project-admin-setting-changed", { changedSetting, changedSettingVal }
+				# End of tracking code.
+				
 				data._csrf = window.csrfToken
 				ide.$http.post "/project/#{ide.project_id}/settings/admin", data
+
 				
 		}
 	]

+ 3 - 1
services/web/public/coffee/ide/share/controllers/ShareController.coffee

@@ -1,8 +1,10 @@
 define [
 	"base"
 ], (App) ->
-	App.controller "ShareController", ["$scope", "$modal", ($scope, $modal) ->
+	App.controller "ShareController", ["$scope", "$modal", "event_tracking", ($scope, $modal, event_tracking) ->
 		$scope.openShareProjectModal = () ->
+			event_tracking.sendCountlyOnce "ide-open-share-modal-once"
+
 			$modal.open(
 				templateUrl: "shareProjectModalTemplate"
 				controller:  "ShareProjectModalController"

+ 2 - 2
services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee

@@ -4,7 +4,7 @@ define [
 	App.controller 'WordCountModalController', ($scope, $modalInstance, ide, $http) ->
 		$scope.status = 
 			loading:true
-			
+
 		opts =
 			url:"/project/#{ide.project_id}/wordcount"
 			method:"GET"
@@ -18,4 +18,4 @@ define [
 				$scope.status.error = true
 
 		$scope.cancel = () ->
-			$modalInstance.dismiss('cancel')
+			$modalInstance.dismiss('cancel')

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

@@ -16,3 +16,4 @@ define [
 	"libs/angular-sixpack"
 	"libs/ng-tags-input-3.0.0"
 ], () ->
+

+ 3 - 1
services/web/public/coffee/main/account-upgrade.coffee

@@ -1,11 +1,13 @@
 define [
 	"base"
 ], (App) ->
-	App.controller "FreeTrialModalController", ($scope, abTestManager, sixpack)->
+	App.controller "FreeTrialModalController", ($scope, abTestManager, sixpack, event_tracking)->
 
 		$scope.buttonClass = "btn-primary"
 
 		$scope.startFreeTrial = (source, couponCode) ->
+			event_tracking.sendCountly "subscription-start-trial", { source }
+
 			w = window.open()
 			sixpack.convert "track-changes-discount", ->
 				sixpack.participate 'in-editor-free-trial-plan', ['student', 'collaborator'], (planName, rawResponse)->

+ 29 - 3
services/web/public/coffee/main/contact-us.coffee

@@ -1,9 +1,8 @@
 define [
 	"base"
 	"libs/platform"
+	"services/algolia-search"
 ], (App, platform) ->
-
-
 	App.controller 'ContactModal', ($scope, $modal) ->
 		$scope.contactUsModal = () ->
 			modalInstance = $modal.open(
@@ -11,10 +10,25 @@ define [
 				controller: "SupportModalController"
 			)
 
-	App.controller 'SupportModalController', ($scope, $modalInstance) ->
+	App.controller 'SupportModalController', ($scope, $modalInstance, algoliaSearch, event_tracking) ->
 		$scope.form = {}
 		$scope.sent = false
 		$scope.sending = false
+		$scope.suggestions = [];
+
+		_handleSearchResults = (success, results) ->
+			suggestions = for hit in results.hits
+				page_underscored = hit.pageName.replace(/\s/g,'_')
+
+				suggestion = 
+					url :"/learn/kb/#{page_underscored}"
+					name : hit._highlightResult.pageName.value
+
+			event_tracking.sendCountly "contact-form-suggestions-shown" if results.hits.length
+
+			$scope.$applyAsync () -> 
+				$scope.suggestions = suggestions
+
 		$scope.contactUs = ->
 			if !$scope.form.email?
 				console.log "email not set"
@@ -36,6 +50,18 @@ define [
 				$scope.sent = true
 				$scope.$apply()
 
+		$scope.$watch "form.subject", (newVal, oldVal) ->
+			if newVal and newVal != oldVal and newVal.length > 3
+				algoliaSearch.searchKB newVal, _handleSearchResults, { 
+					hitsPerPage: 3
+					typoTolerance: 'strict'
+				}
+			else
+				$scope.suggestions = [];
+
+		$scope.clickSuggestionLink = (url) ->
+			event_tracking.sendCountly "contact-form-suggestions-clicked", { url }
+
 		$scope.close = () ->
 			$modalInstance.close()
 

+ 46 - 2
services/web/public/coffee/main/event.coffee

@@ -1,17 +1,61 @@
 define [
 	"base"
+	"modules/localStorage"
 ], (App) ->
+	CACHE_KEY = "countlyEvents"
+
+	App.factory "event_tracking", (localStorage) ->
+		_getEventCache = () -> 
+			eventCache = localStorage CACHE_KEY
+
+			# Initialize as an empy object if the event cache is still empty.
+			if !eventCache?
+				eventCache = {}
+				localStorage CACHE_KEY, eventCache 
+
+			return eventCache
+
+		_eventInCache = (key) ->
+			curCache = _getEventCache()
+			curCache[key] || false
+
+		_addEventToCache = (key) ->
+			curCache = _getEventCache()
+			curCache[key] = true
+
+			localStorage CACHE_KEY, curCache
 
-	App.factory "event_tracking", ->
 		return {
 			send: (category, action, label, value)->
 				ga('send', 'event', category, action, label, value)
+
+			sendCountly: (key, segmentation) ->
+				eventData = { key }
+				eventData.segmentation = segmentation if segmentation?				
+				Countly?.q.push([ "add_event", eventData ])
+
+			sendCountlySampled: (key, segmentation) ->
+				@sendCountly key, segmentation if Math.random() < .01 
+
+			sendCountlyOnce: (key, segmentation) ->
+				if ! _eventInCache(key)
+					_addEventToCache(key)
+					@sendCountly key, segmentation
 		}
 
+	# App.directive "countlyTrack", () ->
+	# 	return {
+	# 		restrict: "A"
+	# 		scope: false,
+	# 		link: (scope, el, attrs) ->
+	# 			eventKey  = attrs.countlyTrack
+	# 			if (eventKey?)
+	# 				el.on "click", () ->
+	# 					console.log eventKey
+	# 	}
 
 	#header
 	$('.navbar a').on "click", (e)->
 		href = $(e.target).attr("href")
 		if href?
 			ga('send', 'event', 'navigation', 'top menu bar', href)
-

+ 3 - 9
services/web/public/coffee/main/learn.coffee

@@ -1,15 +1,9 @@
 define [
 	"base"
+	"services/algolia-search"
 ], (App) ->
-	
-	App.factory "algoliawiki", ->
-		if window.sharelatex?.algolia? and window.sharelatex.algolia?.indexes?.wiki?
-			client = new AlgoliaSearch(window.sharelatex.algolia?.app_id, window.sharelatex.algolia?.api_key)
-			index = client.initIndex(window.sharelatex.algolia?.indexes?.wiki)
-			return index
 
-	App.controller "SearchWikiController", ($scope, algoliawiki, _, $modal) ->
-		algolia = algoliawiki
+	App.controller "SearchWikiController", ($scope, algoliaSearch, _, $modal) ->
 		$scope.hits = []
 
 		$scope.clearSearchText = ->
@@ -54,7 +48,7 @@ define [
 				updateHits []
 				return
 				
-			algolia.search query, (err, response)->
+			algoliaSearch.searchWiki query, (err, response)->
 				if response.hits.length == 0
 					updateHits []
 				else

+ 19 - 1
services/web/public/coffee/main/new-subscription.coffee

@@ -2,7 +2,7 @@ define [
 	"base"
 ], (App)->
 
-	App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack)->
+	App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking)->
 		throw new Error("Recurly API Library Missing.")  if typeof recurly is "undefined"
 	
 		$scope.currencyCode = MultiCurrencyPricing.currencyCode
@@ -51,6 +51,8 @@ define [
 			.done()
 
 		pricing.on "change", =>
+			event_tracking.sendCountly "subscription-form", { plan : pricing.items.plan.code }
+
 			$scope.planName = pricing.items.plan.name
 			$scope.price = pricing.price
 			$scope.trialLength = pricing.items.plan.trial?.length
@@ -115,9 +117,25 @@ define [
 						currencyCode:pricing.items.currency
 						plan_code:pricing.items.plan.code
 						coupon_code:pricing.items?.coupon?.code || ""
+						isPaypal: $scope.paymentMethod == 'paypal'
+						address:
+							address1:    $scope.data.address1
+							address2:    $scope.data.address2
+							country:     $scope.data.country
+							state:       $scope.data.state
+							postal_code: $scope.data.postal_code
+				
+				event_tracking.sendCountly "subscription-form-submitted", { 
+					currencyCode	: postData.subscriptionDetails.currencyCode,
+					plan_code		: postData.subscriptionDetails.plan_code,
+					coupon_code		: postData.subscriptionDetails.coupon_code,
+					isPaypal		: postData.subscriptionDetails.isPaypal
+				}
+
 				$http.post("/user/subscription/create", postData)
 					.success (data, status, headers)->
 						sixpack.convert "in-editor-free-trial-plan", pricing.items.plan.code, (err)->
+							event_tracking.sendCountly "subscription-submission-success"
 							window.location.href = "/user/subscription/thank-you"
 					.error (data, status, headers)->
 						$scope.processing = false

+ 3 - 4
services/web/public/coffee/main/project-list/left-hand-menu-promo-controller.coffee

@@ -3,8 +3,7 @@ define [
 ], (App) ->
 
 	App.controller 'LeftHandMenuPromoController', ($scope) ->
-		
-		$scope.showDatajoy = Math.random() < 0.5
+
 		$scope.hasProjects = window.data.projects.length > 0
-		$scope.userHasSubscription = window.userHasSubscription
-		$scope.randomView = _.shuffle(["default", "dropbox", "github"])[0]
+		$scope.userHasNoSubscription = window.userHasNoSubscription
+		$scope.randomView = _.shuffle(["default", "dropbox", "github"])[0]

+ 3 - 9
services/web/public/coffee/main/templates.coffee

@@ -1,15 +1,9 @@
 define [
 	"base"
+	"services/algolia-search"
 ], (App) ->
-	
-	App.factory "algoliawiki", ->
-		if window.sharelatex?.algolia? and window.sharelatex.algolia?.indexes?.wiki?
-			client = new AlgoliaSearch(window.sharelatex.algolia?.app_id, window.sharelatex.algolia?.api_key)
-			index = client.initIndex(window.sharelatex.algolia?.indexes?.wiki)
-			return index
 
-	App.controller "SearchWikiController", ($scope, algoliawiki, _) ->
-		algolia = algoliawiki
+	App.controller "SearchWikiController", ($scope, algoliaSearch, _) ->
 		$scope.hits = []
 
 		$scope.clearSearchText = ->
@@ -54,7 +48,7 @@ define [
 				updateHits []
 				return
 
-			algolia.search query, (err, response)->
+			algoliaSearch.searchWiki query, (err, response)->
 				if response.hits.length == 0
 					updateHits []
 				else

+ 14 - 0
services/web/public/coffee/services/algolia-search.coffee

@@ -0,0 +1,14 @@
+define [
+	"base"
+], (App) ->
+	App.factory "algoliaSearch", ->
+		if window.sharelatex?.algolia? and window.sharelatex.algolia?.indexes?.wiki?
+			client = new AlgoliaSearch(window.sharelatex.algolia?.app_id, window.sharelatex.algolia?.api_key)
+			wikiIdx = client.initIndex(window.sharelatex.algolia?.indexes?.wiki)
+			kbIdx = client.initIndex(window.sharelatex.algolia?.indexes?.kb)
+
+		service =
+			searchWiki: wikiIdx.search.bind(wikiIdx)
+			searchKB: kbIdx.search.bind(kbIdx)
+		
+		return service

+ 58 - 0
services/web/public/coffee/services/log-hints-feedback.coffee

@@ -0,0 +1,58 @@
+define [
+	"base"
+], (App) ->
+	App.factory "logHintsFeedback", ($http, $q) ->
+		hintsFeedbackFormAPIHash = "rl4xgvr1v5t64a"
+		idStampVal = "OPkEWEFHUFAm7hKlraQMhiOXQabafWo8NipRvLT397w="
+		hintFieldAPIId = "3"
+		reasonFieldAPIId = "1"
+		reasonOtherFieldAPIId = "1_other_other"
+		submitEndpoint = "https://sharelatex.wufoo.eu/forms/#{ hintsFeedbackFormAPIHash }/#public"
+
+		feedbackOpts =
+			DIDNT_UNDERSTAND: "didnt_understand"
+			NOT_APPLICABLE: "not_applicable"
+			INCORRECT: "incorrect"
+			OTHER: "other"
+
+		createRequest = (hintId, feedbackOpt, feedbackOtherVal = "") ->
+			formData = new FormData()
+
+			formData.append "Field#{ hintFieldAPIId }", hintId
+			formData.append "Field#{ reasonFieldAPIId }", feedbackOpt
+			formData.append "idstamp", idStampVal
+
+			# Allow user to specify "other" without any extra details; an empty string 
+			# would trigger an error submitting.
+			if feedbackOpt == feedbackOpts.OTHER and feedbackOtherVal == ""
+				formData.append "Field#{ reasonOtherFieldAPIId }", "#{ feedbackOpts.OTHER } empty"
+			else
+				formData.append "Field#{ reasonOtherFieldAPIId }", feedbackOtherVal
+
+			req =
+				method: 'POST'
+				url: submitEndpoint
+				# This will effectively disable Angular's default serialization mechanisms,
+				# forcing the XHR to be done with whatever data we provide (in this case,
+				# form data). Without this, Angular will forcefully try to serialize data
+				# to JSON.
+				transformRequest: angular.identity
+				data: formData
+				headers : 
+					# This will tell Angular to use the browser-provided value, which is
+					# computed according to the data being sent (in this case, multipart
+					# form + browser-specific multipart boundary). Without this, Angular
+					# will set JSON.
+					"Content-Type": undefined
+
+			return req
+
+		submitFeedback = (hintId, feedbackOpt, feedbackOtherVal = "") ->
+			submitRequest = createRequest hintId, feedbackOpt, feedbackOtherVal
+			$http(submitRequest)
+
+		service =
+			feedbackOpts: feedbackOpts
+			submitFeedback: submitFeedback
+		
+		return service

BIN=BIN
services/web/public/img/about/brian_gough.jpg


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


BIN=BIN
services/web/public/img/about/henry_oswald.jpg


BIN=BIN
services/web/public/img/about/james_allen.jpg


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


BIN=BIN
services/web/public/img/about/michael.jpg


BIN=BIN
services/web/public/img/about/paulo_reis.jpg


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


BIN=BIN
services/web/public/img/crests/cambridge.png


BIN=BIN
services/web/public/img/crests/durham.png


BIN=BIN
services/web/public/img/crests/icl.png


BIN=BIN
services/web/public/img/crests/liverpool.jpg


BIN=BIN
services/web/public/img/crests/mit.gif


BIN=BIN
services/web/public/img/crests/nasa.png


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio