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

Merge branch 'master' into support-cached-pdfs

Brian Gough 11 лет назад
Родитель
Сommit
0315954b47
31 измененных файлов с 241 добавлено и 167 удалено
  1. 0 1
      services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
  2. 1 1
      services/web/app/coffee/Features/Chat/ChatController.coffee
  3. 2 2
      services/web/app/coffee/Features/Compile/CompileManager.coffee
  4. 7 6
      services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
  5. 5 0
      services/web/app/coffee/Features/Project/ProjectLocator.coffee
  6. 0 1
      services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee
  7. 3 1
      services/web/app/coffee/Features/User/UserRegistrationHandler.coffee
  8. 30 20
      services/web/app/coffee/Features/Wiki/WikiController.coffee
  9. 5 0
      services/web/app/coffee/infrastructure/ExpressLocals.coffee
  10. 7 4
      services/web/app/views/layout.jade
  11. 2 2
      services/web/app/views/project/editor/left-menu.jade
  12. 5 0
      services/web/app/views/project/editor/track-changes.jade
  13. 1 1
      services/web/app/views/project/list/side-bar.jade
  14. 3 2
      services/web/app/views/referal/bonus.jade
  15. 79 76
      services/web/app/views/user/settings.jade
  16. 16 0
      services/web/config/settings.defaults.coffee
  17. 2 2
      services/web/package.json
  18. 1 1
      services/web/public/coffee/ide.coffee
  19. 1 1
      services/web/public/coffee/ide/chat/controllers/ChatButtonController.coffee
  20. 1 1
      services/web/public/coffee/ide/chat/controllers/ChatController.coffee
  21. 2 2
      services/web/public/coffee/ide/chat/directives/mathjax.coffee
  22. 5 5
      services/web/public/coffee/ide/chat/services/chatMessages.coffee
  23. 2 6
      services/web/public/coffee/ide/file-tree/FileTreeManager.coffee
  24. 1 1
      services/web/public/coffee/ide/file-tree/controllers/FileTreeFolderController.coffee
  25. 14 10
      services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
  26. 1 1
      services/web/public/coffee/ide/pdfng/directives/pdfHighlights.coffee
  27. 4 1
      services/web/public/coffee/main/subscription-dashboard.coffee
  28. 0 1
      services/web/public/stylesheets/components/fineupload.less
  29. 2 2
      services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee
  30. 19 2
      services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee
  31. 20 14
      services/web/test/smoke/coffee/SmokeTests.coffee

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

@@ -6,7 +6,6 @@ Metrics = require('../../infrastructure/Metrics')
 logger = require("logger-sharelatex")
 querystring = require('querystring')
 Url = require("url")
-uid = require "uid"
 
 module.exports = AuthenticationController =
 	login: (req, res, next = (error) ->) ->

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

@@ -24,5 +24,5 @@ module.exports =
 			if err?
 				logger.err err:err, query:query, "problem getting messages from chat api"
 				return res.send 500
-			logger.log messages:messages, "sending messages to client"
+			logger.log length:messages?.length, "sending messages to client"
 			res.send messages

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

@@ -71,9 +71,9 @@ module.exports = CompileManager =
 			return callback(null, true)
 		opts = 
 			endpointName:"auto_compile"
-			timeInterval:15
+			timeInterval:20
 			subjectName:"everyone"
-			throttle: 15
+			throttle: 25
 		rateLimiter.addCount opts, (err, canCompile)->
 			if err?
 				canCompile = false

+ 7 - 6
services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee

@@ -48,12 +48,13 @@ module.exports = ProjectEntityHandler =
 				for folderPath, folder of folders
 					for doc in folder.docs
 						content = docContents[doc._id.toString()]
-						docs[path.join(folderPath, doc.name)] = {
-							_id:   doc._id
-							name:  doc.name
-							lines: content.lines
-							rev:   content.rev
-						}
+						if content?
+							docs[path.join(folderPath, doc.name)] = {
+								_id:   doc._id
+								name:  doc.name
+								lines: content.lines
+								rev:   content.rev
+							}
 				logger.log count:_.keys(docs).length, project_id:project_id, "returning docs for project"
 				callback null, docs
 

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

@@ -92,6 +92,11 @@ module.exports =
 
 
 		Project.getProject project_or_id, "", (err, project)->
+			if err?
+				logger.err err:err, project_or_id:project_or_id, "error getting project for finding element"
+				return callback(err)
+			if !project?
+				return callback("project could not be found for finding a element #{project_or_id}")
 			if needlePath == '' || needlePath == '/'
 				return callback(null, project.rootFolder[0])
 

+ 0 - 1
services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee

@@ -14,7 +14,6 @@ module.exports =
 		self = @
 		clientTokenId = ""
 		RecurlyWrapper.createSubscription user, subscriptionDetails, recurly_token_id, (error, recurlySubscription)->
-			console.log recurlySubscription
 			return callback(error) if error?
 			SubscriptionUpdater.syncSubscription recurlySubscription, user._id, (error) ->
 				return callback(error) if error?

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

@@ -53,7 +53,9 @@ module.exports =
 				async.series [
 					(cb)-> User.update {_id: user._id}, {"$set":{holdingAccount:false}}, cb
 					(cb)-> AuthenticationManager.setUserPassword user._id, userDetails.password, cb
-					(cb)-> NewsLetterManager.subscribe user, cb
+					(cb)->
+						NewsLetterManager.subscribe user, ->
+						cb() #this can be slow, just fire it off
 					(cb)-> 
 						emailOpts =
 							first_name:user.first_name

+ 30 - 20
services/web/app/coffee/Features/Wiki/WikiController.coffee

@@ -3,37 +3,47 @@ settings = require("settings-sharelatex")
 logger = require("logger-sharelatex")
 ErrorController = require "../Errors/ErrorController"
 _ = require("underscore")
+AuthenticationController = require("../Authentication/AuthenticationController")
 
 other_lngs = ["es"]
 
 module.exports = WikiController = 
-	getPage: (req, res, next) ->
-		
-		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}"
+
+	_checkIfLoginIsNeeded: (req, res, next)->
+		if settings.apis.wiki.requireLogin
+			AuthenticationController.requireLogin()(req, res, next)
 		else
-			lngPage = page
+			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
 
-		WikiController._getPageContent "Contents", (error, contents) ->
-			return next(error) if error?
-			WikiController._getPageContent lngPage, (error, pageData) ->
+			WikiController._getPageContent "Contents", (error, contents) ->
 				return next(error) if error?
-				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._getPageContent lngPage, (error, pageData) ->
+					return next(error) if error?
+					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 {

+ 5 - 0
services/web/app/coffee/infrastructure/ExpressLocals.coffee

@@ -112,6 +112,11 @@ module.exports = (app)->
 		res.locals.formatPrice = SubscriptionFormatters.formatPrice
 		next()
 
+	app.use (req, res, next)->
+		res.locals.externalAuthenticationSystemUsed = ->
+			Settings.ldap?
+		next()
+
 	app.use (req, res, next)->
 		if req.session.user?
 			res.locals.user =

+ 7 - 4
services/web/app/views/layout.jade

@@ -27,11 +27,11 @@ html(itemscope, itemtype='http://schema.org/Product')
 		meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor")
 		
 		-if (typeof(meta) == "undefined")
-			meta(itemprop="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="An online LaTeX editor that's easy to use. No installation, real-time collaboration, version control, hundreds of LaTeX templates, and more.")
 		-else
-			meta(itemprop="description", content=meta)
+			meta(itemprop="description", name="description" , content=meta)
 		
-		meta(itemprop="image", content="https://www.sharelatex.com/favicon.ico")
+		meta(itemprop="image", name="image", content="https://www.sharelatex.com/favicon.ico")
 
 		- if (typeof(gaToken) != "undefined")
 			script(type='text/javascript').
@@ -98,7 +98,10 @@ html(itemscope, itemtype='http://schema.org/Product')
 						// Other plugins
 						/127\.0\.0\.1:4001\/isrunning/i,  // Cacaoweb
 						/webappstoolbarba\.texthelp\.com\//i,
-						/metrics\.itunes\.apple\.com\.edgesuite\.net\//i
+						/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
+						/a\.disquscdn\.com/i,
+						/platform\.twitter\.com/i,
+						/pstatic\.datafastguru\.info/i
 					],
 					shouldSendCallback: function(data) {
 						// only send a fraction of errors

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

@@ -41,12 +41,12 @@ aside#left-menu.full-size(
 				)
 					i.fa.fa-fw.fa-copy
 					|    #{translate("copy_project")}
-			!{moduleIncludes("editorLeftMenu:actions", locals)}
+			!= moduleIncludes("editorLeftMenu:actions", locals)
 
 	if (moduleIncludesAvailable("editorLeftMenu:sync"))
 		div(ng-show="!anonymous")
 			h4() #{translate("sync")}
-			!{moduleIncludes("editorLeftMenu:sync", locals)}
+			!= moduleIncludes("editorLeftMenu:sync", locals)
 
 	h4(ng-show="!anonymous") #{translate("settings")}
 	form.settings(ng-controller="SettingsController", ng-show="!anonymous")

+ 5 - 0
services/web/app/views/project/editor/track-changes.jade

@@ -8,9 +8,14 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
 					ng-class="buttonClass"
 					ng-click="startFreeTrial('track-changes')"
 				) #{translate("start_free_trial")}
+				|    
+				a.small(href, ng-click="toggleTrackChanges()") #{translate("cancel")}
 			p.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
+
 		.message(ng-show="project.owner._id != user.id")
 			p #{translate("ask_proj_owner_to_upgrade_for_history")}
+			p
+				a.small(href, ng-click="toggleTrackChanges()") #{translate("cancel")}
 
 	aside.change-list(
 		ng-controller="TrackChangesListController"

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

@@ -21,7 +21,7 @@
 				href,
 				ng-click="openUploadProjectModal()"
 			) #{translate("upload_project")}
-		!{moduleIncludes("newProjectMenu", locals)}
+		!= moduleIncludes("newProjectMenu", locals)
 		if (templates)
 			li.divider
 			li.dropdown-header #{translate("templates")}

+ 3 - 2
services/web/app/views/referal/bonus.jade

@@ -143,8 +143,9 @@ block content
 			function callback(response) {
 				// document.getElementById('msg').innerHTML = "Post ID: " + response['post_id'];
 			}
-
-			FB.ui(obj, callback);
+			if (typeof FB !== "undefined" && FB !== null) {
+				FB.ui(obj, callback);
+			}
 		}
 
 	script(type="text/javascript").

+ 79 - 76
services/web/app/views/user/settings.jade

@@ -19,19 +19,20 @@ block content
 										h3  #{translate("update_account_info")}
 										form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate)
 											input(type="hidden", name="_csrf", value=csrfToken)
-											.form-group
-												label(for='email')  #{translate("email")}
-												input.form-control(
-													type='email',
-													name='email',
-													placeholder="email@example.com"
-													required,
-													ng-model="email",
-													ng-init="email = #{JSON.stringify(user.email)}",
-													ng-model-options="{ updateOn: 'blur' }"
-												)
-												span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty")
-													|  #{translate("must_be_email_address")}
+											if !externalAuthenticationSystemUsed()
+												.form-group
+													label(for='email')  #{translate("email")}
+													input.form-control(
+														type='email',
+														name='email',
+														placeholder="email@example.com"
+														required,
+														ng-model="email",
+														ng-init="email = #{JSON.stringify(user.email)}",
+														ng-model-options="{ updateOn: 'blur' }"
+													)
+													span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty")
+														|  #{translate("must_be_email_address")}
 											.form-group
 												label(for='firstName').control-label #{translate("first_name")}
 												input.form-control(
@@ -51,75 +52,77 @@ block content
 													type='submit',
 													ng-disabled="settingsForm.$invalid"
 												)  #{translate("update")}
-
-									.col-md-5.col-md-offset-1
-										h3  #{translate("change_password")}
-										form(async-form="changepassword", name="changePasswordForm", action="/user/password/update", method="POST", novalidate)
-											input(type="hidden", name="_csrf", value=csrfToken)
-											.form-group
-												label(for='currentPassword') #{translate("current_password")}
-												input.form-control(
-													type='password',
-													name='currentPassword',
-													placeholder='*********',
-													ng-model="currentPassword",
-													required
-												)
-												span.small.text-primary(ng-show="changePasswordForm.currentPassword.$invalid && changePasswordForm.currentPassword.$dirty")
-													| #{translate("required")}
-											.form-group
-												label(for='newPassword1') #{translate("new_password")}
-												input.form-control(
-													id='newPassword1',
-													type='password',
-													name='newPassword1',
-													placeholder='*********',
-													ng-model="newPassword1",
-													required
-												)
-												span.small.text-primary(ng-show="changePasswordForm.newPassword1.$invalid && changePasswordForm.newPassword1.$dirty")
-													| #{translate("required")}
-											.form-group
-												label(for='newPassword2') #{translate("confirm_new_password")}
-												input.form-control(
-													type='password',
-													name='newPassword2',
-													placeholder='*********',
-													ng-model="newPassword2",
-													equals="newPassword1"
-												)
-												span.small.text-primary(ng-show="changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty")
-													| #{translate("doesnt_match")}
-											.actions
-												button.btn.btn-primary(
-													type='submit',
-													ng-disabled="changePasswordForm.$invalid"
-												) #{translate("change")}
+									if !externalAuthenticationSystemUsed()
+										.col-md-5.col-md-offset-1
+											h3  #{translate("change_password")}
+											form(async-form="changepassword", name="changePasswordForm", action="/user/password/update", method="POST", novalidate)
+												input(type="hidden", name="_csrf", value=csrfToken)
+												.form-group
+													label(for='currentPassword') #{translate("current_password")}
+													input.form-control(
+														type='password',
+														name='currentPassword',
+														placeholder='*********',
+														ng-model="currentPassword",
+														required
+													)
+													span.small.text-primary(ng-show="changePasswordForm.currentPassword.$invalid && changePasswordForm.currentPassword.$dirty")
+														| #{translate("required")}
+												.form-group
+													label(for='newPassword1') #{translate("new_password")}
+													input.form-control(
+														id='newPassword1',
+														type='password',
+														name='newPassword1',
+														placeholder='*********',
+														ng-model="newPassword1",
+														required
+													)
+													span.small.text-primary(ng-show="changePasswordForm.newPassword1.$invalid && changePasswordForm.newPassword1.$dirty")
+														| #{translate("required")}
+												.form-group
+													label(for='newPassword2') #{translate("confirm_new_password")}
+													input.form-control(
+														type='password',
+														name='newPassword2',
+														placeholder='*********',
+														ng-model="newPassword2",
+														equals="newPassword1"
+													)
+													span.small.text-primary(ng-show="changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty")
+														| #{translate("doesnt_match")}
+												.actions
+													button.btn.btn-primary(
+														type='submit',
+														ng-disabled="changePasswordForm.$invalid"
+													) #{translate("change")}
 
 							| !{moduleIncludes("userSettings", locals)}
 
 							hr
 
-							p.small
-								| #{translate("newsletter_info_and_unsubscribe")} 
-								a(
-									href,
-									ng-click="unsubscribe()",
-									ng-show="subscribed && !unsubscribing"
-								) #{translate("unsubscribe")}
-								span(
-									ng-show="unsubscribing"
-								)
-									i.fa.fa-spin.fa-refresh
-									| #{translate("unsubscribing")}
-								span.text-success(
-									ng-show="!subscribed"
-								)
-									i.fa.fa-check
-									| #{translate("unsubscribed")}
+							if !externalAuthenticationSystemUsed()
 
-							p #{translate("need_to_leave")} 
-								a(href, ng-click="deleteAccount()")  #{translate("delete_your_account")}
+								p.small
+									| #{translate("newsletter_info_and_unsubscribe")} 
+									a(
+										href,
+										ng-click="unsubscribe()",
+										ng-show="subscribed && !unsubscribing"
+									) #{translate("unsubscribe")}
+									span(
+										ng-show="unsubscribing"
+									)
+										i.fa.fa-spin.fa-refresh
+										| #{translate("unsubscribing")}
+									span.text-success(
+										ng-show="!subscribed"
+									)
+										i.fa.fa-check
+										| #{translate("unsubscribed")}
+				
+									p #{translate("need_to_leave")} 
+										a(href, ng-click="deleteAccount()")  #{translate("delete_your_account")}
 
 
 	script(type='text/ng-template', id='deleteAccountModalTemplate')

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

@@ -331,3 +331,19 @@ module.exports =
 	proxyUrls: {}
 	
 	reloadModuleViewsOnEachRequest: true
+
+	# ShareLaTeX Server Pro options (https://www.sharelatex.com/university/onsite.html)
+	# ----------
+
+
+	
+	# ldap:
+	# 	host: 'ldap://ldap.forumsys.com'
+	# 	dnObj: 'uid'
+	# 	dnSuffix: 'dc=example,dc=com'
+	# 	failMessage: 'LDAP User Fail'
+	# 	fieldName: 'LDAP User'
+	# 	placeholder: 'LDAP User ID'
+	# 	emailAtt: 'mail'
+
+

+ 2 - 2
services/web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "web-sharelatex",
-  "version": "0.1.2",
+  "version": "0.1.3",
   "description": "The HTTP front end for ShareLaTeX",
   "repository": {
     "type": "git",
@@ -19,6 +19,7 @@
     "express": "3.3.4",
     "fairy": "0.0.2",
     "jade": "~1.3.1",
+    "ldapjs": "^0.7.1",
     "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.0.0",
     "lynx": "0.1.1",
     "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.0.0",
@@ -39,7 +40,6 @@
     "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
     "socket.io": "0.9.16",
     "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master",
-    "uid": "0.0.2",
     "underscore": "1.6.0",
     "underscore.string": "^3.0.2",
     "v8-profiler": "^5.2.3",

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

@@ -83,7 +83,7 @@ define [
 		$scope.$on "project:joined", () ->
 			return if inited
 			inited = true
-			if $scope.project.deletedByExternalDataSource
+			if $scope?.project?.deletedByExternalDataSource
 				ide.showGenericMessageModal("Project Renamed or Deleted", """
 					This project has either been renamed or deleted by an external data source such as Dropbox.
 					We don't want to delete your data on ShareLaTeX, so this project still contains your history and collaborators.

+ 1 - 1
services/web/public/coffee/ide/chat/controllers/ChatButtonController.coffee

@@ -15,7 +15,7 @@ define [
 			
 		$scope.$on "chat:newMessage", (e, message) ->
 			if message?
-				if message.user.id != ide.$scope.user.id
+				if message?.user?.id != ide.$scope.user.id
 					if !$scope.ui.chatOpen
 						$scope.unreadMessages += 1
 					flashTitle()

+ 1 - 1
services/web/public/coffee/ide/chat/controllers/ChatController.coffee

@@ -30,7 +30,7 @@ define [
 		$scope.loadMoreMessages = ->
 			chatMessages.loadMoreMessages()
 			
-		$scope.linkify = (message)->
+		$scope.linkify = (message = "")->
 			return autolinker.link(message)
 
 			

+ 2 - 2
services/web/public/coffee/ide/chat/directives/mathjax.coffee

@@ -15,12 +15,12 @@ define [
 			processEscapes: true
 		skipStartupTypeset: true
 
-	MathJax.Hub.Config(mathjaxConfig);
+	MathJax?.Hub?.Config(mathjaxConfig);
 	
 	App.directive "mathjax", () ->
 		return {
 			link: (scope, element, attrs) ->
 				setTimeout () ->
-					MathJax.Hub.Queue(["Typeset", MathJax.Hub, element.get(0)])
+					MathJax?.Hub?.Queue(["Typeset", MathJax?.Hub, element.get(0)])
 				, 0
 		}

+ 5 - 5
services/web/public/coffee/ide/chat/services/chatMessages.coffee

@@ -18,7 +18,7 @@ define [
 		
 		justSent = false	
 		ide.socket.on "new-chat-message", (message) =>
-			if message.user.id == ide.$scope.user.id and justSent
+			if message?.user?.id == ide.$scope.user.id and justSent
 				# Nothing to do
 			else
 				ide.$scope.$apply () ->
@@ -51,9 +51,9 @@ define [
 					if messages.length < MESSAGE_LIMIT
 						chat.state.atEnd = true
 					if !messages.reverse?
-						Raven?.captureException(new Error("messages has no reverse property #{JSON.stringify(messages)}"))
+						Raven?.captureException(new Error("messages has no reverse property #{typeof(messages)}"))
 					if typeof messages.reverse isnt 'function'
-						Raven?.captureException(new Error("messages.reverse not a function #{typeof(messages.reverse)} #{JSON.stringify(messages)}"))
+						Raven?.captureException(new Error("messages.reverse not a function #{typeof(messages.reverse)} #{typeof(messages)}"))
 						chat.state.errored = true
 					else
 						messages.reverse()
@@ -65,7 +65,7 @@ define [
 		prependMessage = (message) ->
 			firstMessage = chat.state.messages[0]
 			shouldGroup = firstMessage? and
-				firstMessage.user.id == message.user.id and
+				firstMessage.user.id == message?.user?.id and
 				firstMessage.timestamp - message.timestamp < TIMESTAMP_GROUP_SIZE
 			if shouldGroup
 				firstMessage.timestamp = message.timestamp
@@ -86,7 +86,7 @@ define [
 			
 			lastMessage = chat.state.messages[chat.state.messages.length - 1]
 			shouldGroup = lastMessage? and
-				lastMessage.user.id == message.user.id and
+				lastMessage.user.id == message?.user?.id and
 				message.timestamp - lastMessage.timestamp < TIMESTAMP_GROUP_SIZE
 			if shouldGroup
 				lastMessage.timestamp = message.timestamp

+ 2 - 6
services/web/public/coffee/ide/file-tree/FileTreeManager.coffee

@@ -162,13 +162,8 @@ define [
 			return if !path?
 			return path.split("/").slice(0, -1).join("/")
 
-		# forEachFolder: (callback) ->
-		# 	@forEachEntity (entity) ->
-		# 		if entity.type == "folder"
-		# 			callback(entity)
-
 		loadRootFolder: () ->
-			@$scope.rootFolder = @_parseFolder(@$scope.project.rootFolder[0])
+			@$scope.rootFolder = @_parseFolder(@$scope?.project?.rootFolder[0])
 
 		_parseFolder: (rawFolder) ->
 			folder = {
@@ -306,6 +301,7 @@ define [
 			return (child_path.slice(0, parent_path.length) == parent_path)
 
 		_deleteEntityFromScope: (entity, options = { moveToDeleted: true }) ->
+			return if !entity?
 			parent_folder = null
 			@forEachEntity (possible_entity, folder) ->
 				if possible_entity == entity

+ 1 - 1
services/web/public/coffee/ide/file-tree/controllers/FileTreeFolderController.coffee

@@ -17,7 +17,7 @@ define [
 			# We need this here as well as in FileTreeController
 			# since the file-entity diretive creates a new scope
 			# that doesn't inherit from previous scopes.
-			return '0' if entity.type == "folder"
+			return '0' if entity?.type == "folder"
 			return '1'
 
 		$scope.openNewDocModal = () ->

+ 14 - 10
services/web/public/coffee/ide/pdf/controllers/PdfController.coffee

@@ -60,6 +60,9 @@ define [
 
 			IGNORE_FILES = ["output.fls", "output.fdb_latexmk"]
 			$scope.pdf.outputFiles = []
+
+			if !response.outputFiles?
+				return
 			for file in response.outputFiles
 				if IGNORE_FILES.indexOf(file.path) == -1
 					# Turn 'output.blg' into 'blg file'.
@@ -80,16 +83,17 @@ define [
 
 					$scope.pdf.logEntryAnnotations = {}
 					for entry in logEntries.all
-						entry.file = normalizeFilePath(entry.file)
-
-						entity = ide.fileTreeManager.findEntityByPath(entry.file)
-						if entity?
-							$scope.pdf.logEntryAnnotations[entity.id] ||= []
-							$scope.pdf.logEntryAnnotations[entity.id].push {
-								row: entry.line - 1
-								type: if entry.level == "error" then "error" else "warning"
-								text: entry.message
-							}
+						if entry.file?
+							entry.file = normalizeFilePath(entry.file)
+
+							entity = ide.fileTreeManager.findEntityByPath(entry.file)
+							if entity?
+								$scope.pdf.logEntryAnnotations[entity.id] ||= []
+								$scope.pdf.logEntryAnnotations[entity.id].push {
+									row: entry.line - 1
+									type: if entry.level == "error" then "error" else "warning"
+									text: entry.message
+								}
 
 				.error () ->
 					$scope.pdf.logEntries = []

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

@@ -24,6 +24,6 @@ define [
 
 			clearHighlights: () ->
 				for h in @highlightElements
-					h.remove()
+					h?.remove()
 				@highlightElements = []
 	]

+ 4 - 1
services/web/public/coffee/main/subscription-dashboard.coffee

@@ -45,7 +45,10 @@ define [
 			pricing.plan(planCode, { quantity: 1 }).currency(MultiCurrencyPricing.currencyCode).done (price)->
 				totalPriceExTax = parseFloat(price.next.total)
 				$scope.$evalAsync () ->
-					$scope.prices[planCode] = $scope.currencySymbol + (totalPriceExTax + (totalPriceExTax * taxRate))
+					taxAmmount = totalPriceExTax * taxRate
+					if isNaN(taxAmmount)
+						taxAmmount = 0
+					$scope.prices[planCode] = $scope.currencySymbol + (totalPriceExTax + taxAmmount)
 
 			price = ""
 

+ 0 - 1
services/web/public/stylesheets/components/fineupload.less

@@ -84,7 +84,6 @@
 }
 .qq-upload-spinner {
     display: inline-block;
-    background: url("loading.gif");
     width: 15px;
     height: 15px;
     vertical-align: text-bottom;

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

@@ -241,9 +241,9 @@ describe "CompileManager", ->
 			@ratelimiter.addCount.callsArgWith(1, null, true)
 			@CompileManager._checkIfAutoCompileLimitHasBeenHit true, (err, canCompile)=>
 				args = @ratelimiter.addCount.args[0][0]
-				args.throttle.should.equal 15
+				args.throttle.should.equal 25
 				args.subjectName.should.equal "everyone"
-				args.timeInterval.should.equal 15
+				args.timeInterval.should.equal 20
 				args.endpointName.should.equal "auto_compile"
 				canCompile.should.equal true
 				done()

+ 19 - 2
services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee

@@ -6,7 +6,7 @@ modulePath = "../../../../app/js/Features/Project/ProjectLocator"
 SandboxedModule = require('sandboxed-module')
 sinon = require('sinon')
 Errors = require "../../../../app/js/errors"
-
+expect = require("chai").expect
 Project = class Project
 
 project = _id : "1234566", rootFolder:[]
@@ -221,6 +221,7 @@ describe 'project model', ->
 				assert.equal element, undefined
 				done()
 
+
 		describe "where duplicate folder exists", ->
 
 			beforeEach ->
@@ -266,7 +267,23 @@ describe 'project model', ->
 					element.name.should.equal "other.tex"
 					done()
 
-			
+
+		describe "with a null project", ->
+			beforeEach ->
+				@project =
+					rootFolder:[
+						folders: []
+						fileRefs: []
+						docs: [{name:"main.tex"}, null, {name:"other.tex"}]
+					]
+				Project.getProject = sinon.stub()
+				Project.getProject.callsArgWith(2, null)
+
+			it "should not crash with a null", (done)->
+				callback = sinon.stub()
+				@locator.findElementByPath project._id, "/other.tex", (err, element)->
+					expect(err).to.exist
+					done()			
 
 
 	describe 'finding a project by user_id and project name', ()->

+ 20 - 14
services/web/test/smoke/coffee/SmokeTests.coffee

@@ -10,6 +10,18 @@ cookeFilePath = "/tmp/smoke-test-cookie-#{port}.txt"
 buildUrl = (path) -> " -b #{cookeFilePath} --resolve 'smoke#{Settings.cookieDomain}:#{port}:127.0.0.1' http://smoke#{Settings.cookieDomain}:#{port}/#{path}?setLng=en"
 logger = require "logger-sharelatex"
 
+# Change cookie to be non secure so curl will send it
+convertCookieFile = (callback) ->
+	fs = require("fs")
+	fs.readFile cookeFilePath, "utf8", (err, data) ->
+		return callback(err) if err
+		firstTrue = data.indexOf("TRUE")
+		secondTrue = data.indexOf("TRUE", firstTrue+4)
+		result = data.slice(0, secondTrue)+"FALSE"+data.slice(secondTrue+4)
+		fs.writeFile cookeFilePath, result, "utf8", (err) ->
+			return callback(err) if err
+			callback()
+
 describe "Opening", ->
 
 	before (done) ->
@@ -26,20 +38,14 @@ describe "Opening", ->
 						return done("smoke test: does not have csrf token")
 					csrf = csrfMatches[1]
 
-					# Change cookie to be non secure so curl will send it
-					fs = require("fs")
-					fs.readFile cookeFilePath, "utf8", (err, data) ->
-						return done(err) if err
-						firstTrue = data.indexOf("TRUE")
-						secondTrue = data.indexOf("TRUE", firstTrue+4)
-						result = data.slice(0, secondTrue)+"FALSE"+data.slice(secondTrue+4)
-						fs.writeFile cookeFilePath, result, "utf8", (err) ->
-							return done(err) if err
-
-							command = """
-								curl -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d '{"_csrf":"#{csrf}", "email":"#{Settings.smokeTest.user}", "password":"#{Settings.smokeTest.password}"}' #{buildUrl('register')}
-							"""
-							child.exec command, done
+					convertCookieFile (err) ->
+						return done(err) if err?
+						command = """
+							curl -c #{cookeFilePath} -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d '{"_csrf":"#{csrf}", "email":"#{Settings.smokeTest.user}", "password":"#{Settings.smokeTest.password}"}' #{buildUrl('register')}
+						"""
+						child.exec command, (err) ->
+							return done(err) if err?
+							convertCookieFile done
 
 	after (done)-> 
 		command =  """