Ver código fonte

Merge branch 'master' into sk-session-revocation

Shane Kilkelly 10 anos atrás
pai
commit
fc6cf75ad5
27 arquivos alterados com 245 adições e 77 exclusões
  1. 23 13
      services/web/Gruntfile.coffee
  2. 8 0
      services/web/README.md
  3. 1 1
      services/web/app/coffee/Features/Blog/BlogController.coffee
  4. 19 2
      services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
  5. 4 8
      services/web/app/coffee/Features/Compile/CompileController.coffee
  6. 1 1
      services/web/app/coffee/Features/Compile/CompileManager.coffee
  7. 4 0
      services/web/app/coffee/Features/Email/EmailBuilder.coffee
  8. 5 5
      services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
  9. 5 3
      services/web/app/coffee/Features/Project/ProjectDeleter.coffee
  10. 3 3
      services/web/app/views/layout/footer.jade
  11. 2 2
      services/web/app/views/project/editor/pdf.jade
  12. 4 0
      services/web/config/settings.defaults.coffee
  13. 0 1
      services/web/package.json
  14. 2 1
      services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee
  15. 8 11
      services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
  16. 2 1
      services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee
  17. 9 1
      services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee
  18. 1 5
      services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee
  19. BIN
      services/web/public/img/sprite.png
  20. 105 0
      services/web/public/stylesheets/app/sprites.less
  21. 3 8
      services/web/public/stylesheets/app/wiki.less
  22. 5 0
      services/web/public/stylesheets/components/footer.less
  23. 1 0
      services/web/public/stylesheets/style.less
  24. 19 4
      services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee
  25. 1 5
      services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee
  26. 1 1
      services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee
  27. 9 1
      services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee

+ 23 - 13
services/web/Gruntfile.coffee

@@ -17,8 +17,9 @@ module.exports = (grunt) ->
 	grunt.loadNpmTasks 'grunt-contrib-watch'
 	grunt.loadNpmTasks 'grunt-contrib-watch'
 	grunt.loadNpmTasks 'grunt-parallel'
 	grunt.loadNpmTasks 'grunt-parallel'
 	grunt.loadNpmTasks 'grunt-exec'
 	grunt.loadNpmTasks 'grunt-exec'
-	grunt.loadNpmTasks 'grunt-contrib-imagemin'
 	grunt.loadNpmTasks 'grunt-contrib-cssmin'
 	grunt.loadNpmTasks 'grunt-contrib-cssmin'
+	# grunt.loadNpmTasks 'grunt-contrib-imagemin'
+	# grunt.loadNpmTasks 'grunt-sprity'
 
 
 	config =
 	config =
 
 
@@ -47,18 +48,26 @@ module.exports = (grunt) ->
 					stream:true
 					stream:true
 
 
 
 
-		imagemin:
-			dynamic:                       
-				files: [{
-					expand: true
-					cwd: 'public/img/'
-					src: ['**/*.{png,jpg,gif}']
-					dest: 'public/img/'
-				}]
-			options:
-				interlaced:false
-				optimizationLevel: 7
-
+		# 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:
 		coffee:
@@ -218,6 +227,7 @@ module.exports = (grunt) ->
 				pattern: "@@RELEASE@@"
 				pattern: "@@RELEASE@@"
 				replacement: process.env.BUILD_NUMBER || "(unknown build)"
 				replacement: process.env.BUILD_NUMBER || "(unknown build)"
 
 
+
 			
 			
 
 
 		availabletasks:
 		availabletasks:

+ 8 - 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 
 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.
 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 
 The rest of the ShareLaTeX stack, along with information about contributing can be found in the 
 [sharelatex/sharelatex](https://github.com/sharelatex/sharelatex) repository.
 [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
 Unit test status
 ----------------
 ----------------
 
 

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

@@ -20,7 +20,7 @@ module.exports = BlogController =
 
 
 		logger.log url:url, "proxying request to blog api"
 		logger.log url:url, "proxying request to blog api"
 		request.get blogUrl, (err, r, data)->
 		request.get blogUrl, (err, r, data)->
-			if r?.statusCode == 404
+			if r?.statusCode == 404 or r?.statusCode == 403
 				return ErrorController.notFound(req, res, next)
 				return ErrorController.notFound(req, res, next)
 			if err?
 			if err?
 				return res.send 500
 				return res.send 500

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

@@ -30,12 +30,17 @@ module.exports = CollaboratorsHandler =
 	getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
 	getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
 		CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
 		CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
 			return callback(error) if error?
 			return callback(error) if error?
+			result = []
 			async.mapLimit members, 3,
 			async.mapLimit members, 3,
 				(member, cb) ->
 				(member, cb) ->
 					UserGetter.getUser member.id, (error, user) ->
 					UserGetter.getUser member.id, (error, user) ->
 						return cb(error) if error?
 						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) ->) ->
 	getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) ->
 		# In future if the schema changes and getting all member ids is more expensive (multiple documents)
 		# 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"
 				logger.error err: err, "problem removing user from project collaberators"
 			callback(err)
 			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) ->) ->
 	addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) ->
 		emails = mimelib.parseAddresses(unparsed_email)
 		emails = mimelib.parseAddresses(unparsed_email)
 		email = emails[0]?.address?.toLowerCase()
 		email = emails[0]?.address?.toLowerCase()

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

@@ -29,8 +29,6 @@ module.exports = CompileController =
 				options.compiler = req.body.compiler
 				options.compiler = req.body.compiler
 			if req.body?.draft
 			if req.body?.draft
 				options.draft = req.body.draft
 				options.draft = req.body.draft
-			if req.query?.isolated is "true"
-				options.isolated = true
 			logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
 			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) ->
 			CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
 				return next(error) if error?
 				return next(error) if error?
@@ -44,17 +42,15 @@ module.exports = CompileController =
 				}
 				}
 
 
 	_compileAsUser: (req, callback) ->
 	_compileAsUser: (req, callback) ->
-		# callback with user_id if isolated flag is set on request, undefined otherwise
-		isolated = req.query?.isolated is "true"
-		if isolated
+		# callback with user_id if per-user, undefined otherwise
+		if not Settings.disablePerUserCompiles
 			AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
 			AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
 		else
 		else
 			callback() # do a per-project compile, not per-user
 			callback() # do a per-project compile, not per-user
 
 
 	_downloadAsUser: (req, callback) ->
 	_downloadAsUser: (req, callback) ->
-		# callback with user_id if isolated flag or user_id param is set on request, undefined otherwise
-		isolated = req.query?.isolated is "true" or req.params.user_id?
-		if isolated
+		# callback with user_id if per-user, undefined otherwise
+		if not Settings.disablePerUserCompiles
 			AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
 			AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
 		else
 		else
 			callback() # do a per-project compile, not per-user
 			callback() # do a per-project compile, not per-user

+ 1 - 1
services/web/app/coffee/Features/Compile/CompileManager.coffee

@@ -38,7 +38,7 @@ module.exports = CompileManager =
 							for key, value of limits
 							for key, value of limits
 								options[key] = value
 								options[key] = value
 							# only pass user_id down to clsi if this is a per-user compile
 							# only pass user_id down to clsi if this is a per-user compile
-							compileAsUser = if options.isolated then user_id else undefined
+							compileAsUser = if Settings.disablePerUserCompiles then undefined else user_id
 							ClsiManager.sendRequest project_id, compileAsUser, options, (error, status, outputFiles, clsiServerId, validationProblems) ->
 							ClsiManager.sendRequest project_id, compileAsUser, options, (error, status, outputFiles, clsiServerId, validationProblems) ->
 								return callback(error) if error?
 								return callback(error) if error?
 								logger.log files: outputFiles, "output files"
 								logger.log files: outputFiles, "output files"

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

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

+ 5 - 5
services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee

@@ -6,7 +6,7 @@ oneSecond = 1000
 
 
 makeRequest = (opts, callback)->
 makeRequest = (opts, callback)->
 	if !settings.apis.notifications?.url?
 	if !settings.apis.notifications?.url?
-		return callback()
+		return callback(null, statusCode:200)
 	else
 	else
 		request(opts, callback)
 		request(opts, callback)
 
 
@@ -18,7 +18,7 @@ module.exports =
 			json: true
 			json: true
 			timeout: oneSecond
 			timeout: oneSecond
 			method: "GET"
 			method: "GET"
-		request opts, (err, res, unreadNotifications)->
+		makeRequest opts, (err, res, unreadNotifications)->
 			statusCode =  if res? then res.statusCode else 500
 			statusCode =  if res? then res.statusCode else 500
 			if err? or statusCode != 200
 			if err? or statusCode != 200
 				e = new Error("something went wrong getting notifications, #{err}, #{statusCode}")
 				e = new Error("something went wrong getting notifications, #{err}, #{statusCode}")
@@ -40,7 +40,7 @@ module.exports =
 				templateKey:templateKey
 				templateKey:templateKey
 			}
 			}
 		logger.log opts:opts, "creating notification for user"
 		logger.log opts:opts, "creating notification for user"
-		request opts, callback
+		makeRequest opts, callback
 
 
 	markAsReadWithKey: (user_id, key, callback)->
 	markAsReadWithKey: (user_id, key, callback)->
 		opts = 
 		opts = 
@@ -51,7 +51,7 @@ module.exports =
 				key:key
 				key:key
 			}
 			}
 		logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api"
 		logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api"
-		request opts, callback
+		makeRequest opts, callback
 	
 	
 
 
 	markAsRead: (user_id, notification_id, callback)->
 	markAsRead: (user_id, notification_id, callback)->
@@ -60,4 +60,4 @@ module.exports =
 			uri: "#{settings.apis.notifications?.url}/user/#{user_id}/notification/#{notification_id}"
 			uri: "#{settings.apis.notifications?.url}/user/#{user_id}/notification/#{notification_id}"
 			timeout:oneSecond
 			timeout:oneSecond
 		logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api"
 		logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api"
-		request opts, callback
+		makeRequest opts, callback

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

@@ -24,9 +24,11 @@ module.exports = ProjectDeleter =
 		update = {deletedByExternalDataSource: false}
 		update = {deletedByExternalDataSource: false}
 		Project.update conditions, update, {}, callback
 		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) ->) ->
 	deleteProject: (project_id, callback = (error) ->) ->
 		# archiveProject takes care of the clean-up
 		# archiveProject takes care of the clean-up

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

@@ -15,7 +15,7 @@ footer.site-footer
 							aria-expanded="false",
 							aria-expanded="false",
 							tooltip="#{translate('language')}"
 							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")
 						ul.dropdown-menu(role="menu")
 							li.dropdown-header #{translate("language")}
 							li.dropdown-header #{translate("language")}
@@ -23,9 +23,9 @@ footer.site-footer
 								if !subdomainDetails.hide
 								if !subdomainDetails.hide
 									li.lngOption
 									li.lngOption
 										a.menu-indent(href=subdomainDetails.url+currentUrl)
 										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)}
 											|  #{translate(subdomainDetails.lngCode)}
-										
+										//- img(src="/img/flags/24/.png")
 				each item in nav.left_footer
 				each item in nav.left_footer
 					li
 					li
 						if item.url
 						if item.url

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

@@ -128,12 +128,12 @@ div.full-size.pdf(ng-controller="PdfController")
 						)
 						)
 							label.card-hint-feedback-label #{translate("log_hint_feedback_label")}
 							label.card-hint-feedback-label #{translate("log_hint_feedback_label")}
 							a.card-hint-feedback-positive(
 							a.card-hint-feedback-positive(
-								ng-click="feedbackSent = true;"
+								ng-click="trackLogHintsPositiveFeedback(entry.ruleId); feedbackSent = true;"
 								href
 								href
 							) #{translate("answer_yes")}
 							) #{translate("answer_yes")}
 							span  / 
 							span  / 
 							a.card-hint-feedback-negative(
 							a.card-hint-feedback-negative(
-								ng-click="feedbackSent = true;"
+								ng-click="trackLogHintsNegativeFeedback(entry.ruleId); feedbackSent = true;"
 								href
 								href
 							) #{translate("answer_no")}
 							) #{translate("answer_no")}
 						.card-hint-feedback(ng-show="feedbackSent")
 						.card-hint-feedback(ng-show="feedbackSent")

+ 4 - 0
services/web/config/settings.defaults.coffee

@@ -262,6 +262,10 @@ module.exports = settings =
 	# Should we allow access to any page without logging in? This includes
 	# Should we allow access to any page without logging in? This includes
 	# public projects, /learn, /templates, about pages, etc.
 	# public projects, /learn, /templates, about pages, etc.
 	allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false
 	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.
 	# Maximum size of text documents in the real-time editing system.
 	max_doc_length: 2 * 1024 * 1024 # 2mb
 	max_doc_length: 2 * 1024 * 1024 # 2mb

+ 0 - 1
services/web/package.json

@@ -70,7 +70,6 @@
     "grunt-contrib-clean": "0.5.0",
     "grunt-contrib-clean": "0.5.0",
     "grunt-contrib-coffee": "0.10.0",
     "grunt-contrib-coffee": "0.10.0",
     "grunt-contrib-cssmin": "^1.0.1",
     "grunt-contrib-cssmin": "^1.0.1",
-    "grunt-contrib-imagemin": "^1.0.1",
     "grunt-contrib-less": "0.9.0",
     "grunt-contrib-less": "0.9.0",
     "grunt-contrib-requirejs": "0.4.1",
     "grunt-contrib-requirejs": "0.4.1",
     "grunt-contrib-watch": "^1.0.0",
     "grunt-contrib-watch": "^1.0.0",

+ 2 - 1
services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee

@@ -12,7 +12,8 @@ define [
 			ruleDetails = _getRule entry.message
 			ruleDetails = _getRule entry.message
 
 
 			if (ruleDetails?)
 			if (ruleDetails?)
-				entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() if ruleDetails.regexToMatch?
+				entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1) if ruleDetails.regexToMatch?
+				
 				entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint?
 				entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint?
 				entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL?
 				entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL?
 	
 	

+ 8 - 11
services/web/public/coffee/ide/pdf/controllers/PdfController.coffee

@@ -15,6 +15,13 @@ define [
 		$scope.shouldShowLogs = false
 		$scope.shouldShowLogs = false
 		$scope.wikiEnabled = window.wikiEnabled;
 		$scope.wikiEnabled = window.wikiEnabled;
 
 
+		# log hints tracking
+		trackLogHintsFeedback = (isPositive, hintId) ->
+			event_tracking.send 'log-hints', (if isPositive then 'feedback-positive' else 'feedback-negative'), hintId
+
+		$scope.trackLogHintsPositiveFeedback = (hintId) -> trackLogHintsFeedback true, hintId
+		$scope.trackLogHintsNegativeFeedback = (hintId) -> trackLogHintsFeedback false, hintId
+
 		if ace.require("ace/lib/useragent").isMac
 		if ace.require("ace/lib/useragent").isMac
 			$scope.modifierKey = "Cmd"
 			$scope.modifierKey = "Cmd"
 		else
 		else
@@ -50,8 +57,6 @@ define [
 			params = {}
 			params = {}
 			if options.isAutoCompile
 			if options.isAutoCompile
 				params["auto_compile"]=true
 				params["auto_compile"]=true
-			if perUserCompile # send ?isolated=true for per-user compiles
-				params["isolated"] = true
 			return $http.post url, {
 			return $http.post url, {
 				rootDoc_id: options.rootDocOverride_id or null
 				rootDoc_id: options.rootDocOverride_id or null
 				draft: $scope.draft
 				draft: $scope.draft
@@ -125,9 +130,6 @@ define [
 				# convert the qs hash into a query string and append it
 				# convert the qs hash into a query string and append it
 				$scope.pdf.qs = createQueryString qs
 				$scope.pdf.qs = createQueryString qs
 				$scope.pdf.url += $scope.pdf.qs
 				$scope.pdf.url += $scope.pdf.qs
-				# special case for the download url
-				if perUserCompile
-					qs.isolated = true
 				# Save all downloads as files
 				# Save all downloads as files
 				qs.popupDownload = true
 				qs.popupDownload = true
 				$scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs)
 				$scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs)
@@ -147,8 +149,6 @@ define [
 					else
 					else
 						file.name = file.path
 						file.name = file.path
 					qs = {}
 					qs = {}
-					if perUserCompile
-						qs.isolated = true
 					if response.clsiServerId?
 					if response.clsiServerId?
 						qs.clsiserverid = response.clsiServerId
 						qs.clsiserverid = response.clsiServerId
 					file.url = "/project/#{project_id}/output/#{file.path}" +	createQueryString qs
 					file.url = "/project/#{project_id}/output/#{file.path}" +	createQueryString qs
@@ -237,7 +237,7 @@ define [
 			return null
 			return null
 
 
 		normalizeFilePath = (path) ->
 		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\//, "")
 			path = path.replace(/^\/compile\//, "")
 
 
 			rootDocDirname = ide.fileTreeManager.getRootDocDirname()
 			rootDocDirname = ide.fileTreeManager.getRootDocDirname()
@@ -274,7 +274,6 @@ define [
 				method: "DELETE"
 				method: "DELETE"
 				params:
 				params:
 					clsiserverid:ide.clsiServerId
 					clsiserverid:ide.clsiServerId
-					isolated: perUserCompile
 				headers:
 				headers:
 					"X-Csrf-Token": window.csrfToken
 					"X-Csrf-Token": window.csrfToken
 			}
 			}
@@ -361,7 +360,6 @@ define [
 							line: row + 1
 							line: row + 1
 							column: column
 							column: column
 							clsiserverid:ide.clsiServerId
 							clsiserverid:ide.clsiServerId
-							isolated: perUserCompile
 						}
 						}
 					})
 					})
 					.success (data) ->
 					.success (data) ->
@@ -407,7 +405,6 @@ define [
 							h: h.toFixed(2)
 							h: h.toFixed(2)
 							v: v.toFixed(2)
 							v: v.toFixed(2)
 							clsiserverid:ide.clsiServerId
 							clsiserverid:ide.clsiServerId
-							isolated: perUserCompile
 						}
 						}
 					})
 					})
 					.success (data) ->
 					.success (data) ->

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

@@ -12,7 +12,8 @@ define [
 
 
 			constructor: (@url, @options) ->
 			constructor: (@url, @options) ->
 				# PDFJS.disableFontFace = true  # avoids repaints, uses worker more
 				# 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.disableStream
 				# PDFJS.disableRange
 				# PDFJS.disableRange
 				@scale = @options.scale || 1
 				@scale = @options.scale || 1

+ 9 - 1
services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee

@@ -27,13 +27,21 @@ define [
 			$scope.document.destroy() if $scope.document?
 			$scope.document.destroy() if $scope.document?
 			$scope.loadCount = if $scope.loadCount? then $scope.loadCount + 1 else 1
 			$scope.loadCount = if $scope.loadCount? then $scope.loadCount + 1 else 1
 			# TODO need a proper url manipulation library to add to query string
 			# 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 = window.location?.search?.match(/isolated=true/)?
+			$scope.document = new PDFRenderer(url, {
 				scale: 1,
 				scale: 1,
+				disableAutoFetch: if onDemandLoading then true else undefined
 				navigateFn: (ref) ->
 				navigateFn: (ref) ->
 					# this function captures clicks on the annotation links
 					# this function captures clicks on the annotation links
 					$scope.navigateTo = ref
 					$scope.navigateTo = ref
 					$scope.$apply()
 					$scope.$apply()
 				progressCallback: (progress) ->
 				progressCallback: (progress) ->
+					return if onDemandLoading is true # don't show progress for on-demand page loading
 					$scope.$emit 'progress', progress
 					$scope.$emit 'progress', progress
 				loadedCallback: () ->
 				loadedCallback: () ->
 					$scope.$emit 'loaded'
 					$scope.$emit 'loaded'

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

@@ -5,15 +5,11 @@ define [
 		$scope.status = 
 		$scope.status = 
 			loading:true
 			loading:true
 
 
-		# enable per-user containers by default
-		perUserCompile = true
-
 		opts =
 		opts =
 			url:"/project/#{ide.project_id}/wordcount"
 			url:"/project/#{ide.project_id}/wordcount"
 			method:"GET"
 			method:"GET"
 			params:
 			params:
 				clsiserverid:ide.clsiServerId
 				clsiserverid:ide.clsiServerId
-				isolated: perUserCompile
 		$http opts
 		$http opts
 			.success (data) ->
 			.success (data) ->
 				$scope.status.loading = false
 				$scope.status.loading = false
@@ -22,4 +18,4 @@ define [
 				$scope.status.error = true
 				$scope.status.error = true
 
 
 		$scope.cancel = () ->
 		$scope.cancel = () ->
-			$modalInstance.dismiss('cancel')
+			$modalInstance.dismiss('cancel')

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


+ 105 - 0
services/web/public/stylesheets/app/sprites.less

@@ -0,0 +1,105 @@
+
+.sprite-icon {
+  background-image: url('/img/sprite.png');
+}
+
+.sprite-icon-ko {
+  background-position: -0px -0px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-cn {
+  background-position: -0px -24px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-da {
+  background-position: -0px -48px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-de {
+  background-position: -0px -72px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-en {
+  background-position: -0px -96px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-es {
+  background-position: -0px -120px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-fi {
+  background-position: -0px -144px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-fr {
+  background-position: -0px -168px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-it {
+  background-position: -0px -192px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-ja {
+  background-position: -0px -216px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-cs {
+  background-position: -0px -240px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-nl {
+  background-position: -0px -264px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-no {
+  background-position: -0px -288px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-pl {
+  background-position: -0px -312px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-pt {
+  background-position: -0px -336px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-ru {
+  background-position: -0px -360px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-sv {
+  background-position: -0px -384px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-tr {
+  background-position: -0px -408px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-uk {
+  background-position: -0px -432px;
+  width: 24px;
+  height: 24px;
+}
+.sprite-icon-zh-CN {
+  background-position: -0px -456px;
+  width: 24px;
+  height: 24px;
+}

+ 3 - 8
services/web/public/stylesheets/app/wiki.less

@@ -41,11 +41,6 @@
 	.example {
 	.example {
 		max-width: 100%;
 		max-width: 100%;
 
 
-		& > div {
-			display: block !important;
-			width: auto !important;
-		}
-
 		.code {
 		.code {
 			pre {
 			pre {
 				background-color: @gray-lightest;
 				background-color: @gray-lightest;
@@ -60,9 +55,9 @@
 			padding-top: 10px;
 			padding-top: 10px;
 			
 			
 			img {
 			img {
-				width: auto !important;
-				height: auto !important;
-				max-width: 100% !important;
+				width: auto;
+				height: auto;
+				max-width: 100%;
 				box-shadow: 0 1px 3px @gray-light;
 				box-shadow: 0 1px 3px @gray-light;
 				border-radius: 6px;
 				border-radius: 6px;
 			}
 			}

+ 5 - 0
services/web/public/stylesheets/components/footer.less

@@ -23,3 +23,8 @@ footer.site-footer {
 		}
 		}
 	}
 	}
 }
 }
+
+.sprite-icon-lang {
+	display: inline-block;
+	vertical-align: middle;
+}

+ 1 - 0
services/web/public/stylesheets/style.less

@@ -73,3 +73,4 @@
 @import "app/wiki.less";
 @import "app/wiki.less";
 @import "app/translations.less";
 @import "app/translations.less";
 @import "app/contact-us.less";
 @import "app/contact-us.less";
+@import "app/sprites.less";

+ 19 - 4
services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee

@@ -77,17 +77,19 @@ describe "CollaboratorsHandler", ->
 				{ id: "read-only-ref-2", privilegeLevel: "readOnly" }
 				{ id: "read-only-ref-2", privilegeLevel: "readOnly" }
 				{ id: "read-write-ref-1", privilegeLevel: "readAndWrite" }
 				{ id: "read-write-ref-1", privilegeLevel: "readAndWrite" }
 				{ id: "read-write-ref-2", privilegeLevel: "readAndWrite" }
 				{ id: "read-write-ref-2", privilegeLevel: "readAndWrite" }
+				{ id: "doesnt-exist", privilegeLevel: "readAndWrite" }
 			])
 			])
 			@UserGetter.getUser = sinon.stub()
 			@UserGetter.getUser = sinon.stub()
 			@UserGetter.getUser.withArgs("read-only-ref-1").yields(null, { _id: "read-only-ref-1" })
 			@UserGetter.getUser.withArgs("read-only-ref-1").yields(null, { _id: "read-only-ref-1" })
 			@UserGetter.getUser.withArgs("read-only-ref-2").yields(null, { _id: "read-only-ref-2" })
 			@UserGetter.getUser.withArgs("read-only-ref-2").yields(null, { _id: "read-only-ref-2" })
 			@UserGetter.getUser.withArgs("read-write-ref-1").yields(null, { _id: "read-write-ref-1" })
 			@UserGetter.getUser.withArgs("read-write-ref-1").yields(null, { _id: "read-write-ref-1" })
 			@UserGetter.getUser.withArgs("read-write-ref-2").yields(null, { _id: "read-write-ref-2" })
 			@UserGetter.getUser.withArgs("read-write-ref-2").yields(null, { _id: "read-write-ref-2" })
+			@UserGetter.getUser.withArgs("doesnt-exist").yields(null, null)
 			@CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback
 			@CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback
 		
 		
 		it "should return an array of members with their privilege levels", ->
 		it "should return an array of members with their privilege levels", ->
 			@callback
 			@callback
-				.calledWith(undefined, [
+				.calledWith(null, [
 					{ user: { _id: "read-only-ref-1" }, privilegeLevel: "readOnly" }
 					{ user: { _id: "read-only-ref-1" }, privilegeLevel: "readOnly" }
 					{ user: { _id: "read-only-ref-2" }, privilegeLevel: "readOnly" }
 					{ user: { _id: "read-only-ref-2" }, privilegeLevel: "readOnly" }
 					{ user: { _id: "read-write-ref-1" }, privilegeLevel: "readAndWrite" }
 					{ user: { _id: "read-write-ref-1" }, privilegeLevel: "readAndWrite" }
@@ -274,6 +276,19 @@ describe "CollaboratorsHandler", ->
 			it "should not add any users to the proejct", ->
 			it "should not add any users to the proejct", ->
 				@CollaboratorHandler.addUserIdToProject.called.should.equal false
 				@CollaboratorHandler.addUserIdToProject.called.should.equal false
 
 
-
-
-
+	describe "removeUserFromAllProjects", ->
+		beforeEach (done) ->
+			@CollaboratorHandler.getProjectsUserIsCollaboratorOf = sinon.stub()
+			@CollaboratorHandler.getProjectsUserIsCollaboratorOf.withArgs(@user_id, { _id: 1 }).yields(
+				null,
+				[ { _id: "read-and-write-0" }, { _id: "read-and-write-1" }, null ],
+				[ { _id: "read-only-0" }, { _id: "read-only-1" }, null ]
+			)
+			@CollaboratorHandler.removeUserFromProject = sinon.stub().yields()
+			@CollaboratorHandler.removeUserFromAllProjets @user_id, done
+		
+		it "should remove the user from each project", ->
+			for project_id in ["read-and-write-0", "read-and-write-1", "read-only-0", "read-only-1"]
+				@CollaboratorHandler.removeUserFromProject
+					.calledWith(project_id, @user_id)
+					.should.equal true

+ 1 - 5
services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee

@@ -139,7 +139,7 @@ describe "CompileController", ->
 					.should.equal true
 					.should.equal true
 
 
 			it "should proxy the PDF from the CLSI", ->
 			it "should proxy the PDF from the CLSI", ->
-				@CompileController.proxyToClsi.calledWith(@project_id, "/project/#{@project_id}/output/output.pdf", @req, @res, @next).should.equal true
+				@CompileController.proxyToClsi.calledWith(@project_id, "/project/#{@project_id}/user/#{@user_id}/output/output.pdf", @req, @res, @next).should.equal true
 
 
 		describe "when the pdf is not going to be used in pdfjs viewer", ->
 		describe "when the pdf is not going to be used in pdfjs viewer", ->
 
 
@@ -338,8 +338,6 @@ describe "CompileController", ->
 			@req =
 			@req =
 				params:
 				params:
 					project_id:@project_id
 					project_id:@project_id
-				query:
-					isolated: "true"
 			@CompileManager.compile.callsArgWith(3)
 			@CompileManager.compile.callsArgWith(3)
 			@CompileController.proxyToClsi = sinon.stub()
 			@CompileController.proxyToClsi = sinon.stub()
 			@res = 
 			@res = 
@@ -362,8 +360,6 @@ describe "CompileController", ->
 			@CompileManager.wordCount = sinon.stub().callsArgWith(3, null, {content:"body"})
 			@CompileManager.wordCount = sinon.stub().callsArgWith(3, null, {content:"body"})
 			@req.params =
 			@req.params =
 				Project_id: @project_id
 				Project_id: @project_id
-			@req.query =
-				isolated: "true"
 			@res.send = sinon.stub()
 			@res.send = sinon.stub()
 			@res.contentType = sinon.stub()
 			@res.contentType = sinon.stub()
 			@CompileController.wordCount @req, @res, @next
 			@CompileController.wordCount @req, @res, @next

+ 1 - 1
services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee

@@ -71,7 +71,7 @@ describe "CompileManager", ->
 
 
 			it "should run the compile with the compile limits", ->
 			it "should run the compile with the compile limits", ->
 				@ClsiManager.sendRequest
 				@ClsiManager.sendRequest
-					.calledWith(@project_id, undefined, {
+					.calledWith(@project_id, @user_id, {
 						timeout: @limits.timeout
 						timeout: @limits.timeout
 					})
 					})
 					.should.equal true
 					.should.equal true

+ 9 - 1
services/web/test/UnitTests/coffee/Project/ProjectDeleterTests.coffee

@@ -27,13 +27,15 @@ describe 'ProjectDeleter', ->
 			removeProjectFromAllTags: sinon.stub().callsArgWith(2)
 			removeProjectFromAllTags: sinon.stub().callsArgWith(2)
 		@ProjectGetter =
 		@ProjectGetter =
 			getProject:sinon.stub()
 			getProject:sinon.stub()
+		@CollaboratorsHandler =
+			removeUserFromAllProjets: sinon.stub().yields()
 		@deleter = SandboxedModule.require modulePath, requires:
 		@deleter = SandboxedModule.require modulePath, requires:
 			"../Editor/EditorController": @editorController
 			"../Editor/EditorController": @editorController
 			'../../models/Project':{Project:@Project}
 			'../../models/Project':{Project:@Project}
 			'../DocumentUpdater/DocumentUpdaterHandler': @documentUpdaterHandler
 			'../DocumentUpdater/DocumentUpdaterHandler': @documentUpdaterHandler
 			"../Tags/TagsHandler":@TagsHandler
 			"../Tags/TagsHandler":@TagsHandler
 			"../FileStore/FileStoreHandler": @FileStoreHandler = {}
 			"../FileStore/FileStoreHandler": @FileStoreHandler = {}
-			"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {}
+			"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler
 			"./ProjectGetter": @ProjectGetter
 			"./ProjectGetter": @ProjectGetter
 			'logger-sharelatex':
 			'logger-sharelatex':
 				log:->
 				log:->
@@ -74,6 +76,12 @@ describe 'ProjectDeleter', ->
 				@Project.remove.calledWith(owner_ref:user_id).should.equal true
 				@Project.remove.calledWith(owner_ref:user_id).should.equal true
 				done()
 				done()
 
 
+		it "should remove all the projects the user is a collaborator of", (done)->
+			user_id = 1234
+			@deleter.deleteUsersProjects user_id, =>
+				@CollaboratorsHandler.removeUserFromAllProjets.calledWith(user_id).should.equal true
+				done()
+
 	describe "deleteProject", ->
 	describe "deleteProject", ->
 		beforeEach (done) ->
 		beforeEach (done) ->
 			@project_id = "mock-project-id-123"
 			@project_id = "mock-project-id-123"