Przeglądaj źródła

Merge branch 'master' into support-cached-pdfs

Brian Gough 11 lat temu
rodzic
commit
0315954b47
31 zmienionych plików z 241 dodań i 167 usunięć
  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")
 logger = require("logger-sharelatex")
 querystring = require('querystring')
 querystring = require('querystring')
 Url = require("url")
 Url = require("url")
-uid = require "uid"
 
 
 module.exports = AuthenticationController =
 module.exports = AuthenticationController =
 	login: (req, res, next = (error) ->) ->
 	login: (req, res, next = (error) ->) ->

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

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

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

@@ -71,9 +71,9 @@ module.exports = CompileManager =
 			return callback(null, true)
 			return callback(null, true)
 		opts = 
 		opts = 
 			endpointName:"auto_compile"
 			endpointName:"auto_compile"
-			timeInterval:15
+			timeInterval:20
 			subjectName:"everyone"
 			subjectName:"everyone"
-			throttle: 15
+			throttle: 25
 		rateLimiter.addCount opts, (err, canCompile)->
 		rateLimiter.addCount opts, (err, canCompile)->
 			if err?
 			if err?
 				canCompile = false
 				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 folderPath, folder of folders
 					for doc in folder.docs
 					for doc in folder.docs
 						content = docContents[doc._id.toString()]
 						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"
 				logger.log count:_.keys(docs).length, project_id:project_id, "returning docs for project"
 				callback null, docs
 				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)->
 		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 == '/'
 			if needlePath == '' || needlePath == '/'
 				return callback(null, project.rootFolder[0])
 				return callback(null, project.rootFolder[0])
 
 

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

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

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

@@ -53,7 +53,9 @@ module.exports =
 				async.series [
 				async.series [
 					(cb)-> User.update {_id: user._id}, {"$set":{holdingAccount:false}}, cb
 					(cb)-> User.update {_id: user._id}, {"$set":{holdingAccount:false}}, cb
 					(cb)-> AuthenticationManager.setUserPassword user._id, userDetails.password, 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)-> 
 					(cb)-> 
 						emailOpts =
 						emailOpts =
 							first_name:user.first_name
 							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")
 logger = require("logger-sharelatex")
 ErrorController = require "../Errors/ErrorController"
 ErrorController = require "../Errors/ErrorController"
 _ = require("underscore")
 _ = require("underscore")
+AuthenticationController = require("../Authentication/AuthenticationController")
 
 
 other_lngs = ["es"]
 other_lngs = ["es"]
 
 
 module.exports = WikiController = 
 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
 		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?
 				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)
 						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: "" }) ->) ->
 	_getPageContent: (page, callback = (error, data = { content: "", title: "" }) ->) ->
 		request {
 		request {

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

@@ -112,6 +112,11 @@ module.exports = (app)->
 		res.locals.formatPrice = SubscriptionFormatters.formatPrice
 		res.locals.formatPrice = SubscriptionFormatters.formatPrice
 		next()
 		next()
 
 
+	app.use (req, res, next)->
+		res.locals.externalAuthenticationSystemUsed = ->
+			Settings.ldap?
+		next()
+
 	app.use (req, res, next)->
 	app.use (req, res, next)->
 		if req.session.user?
 		if req.session.user?
 			res.locals.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")
 		meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor")
 		
 		
 		-if (typeof(meta) == "undefined")
 		-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
 		-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")
 		- if (typeof(gaToken) != "undefined")
 			script(type='text/javascript').
 			script(type='text/javascript').
@@ -98,7 +98,10 @@ html(itemscope, itemtype='http://schema.org/Product')
 						// Other plugins
 						// Other plugins
 						/127\.0\.0\.1:4001\/isrunning/i,  // Cacaoweb
 						/127\.0\.0\.1:4001\/isrunning/i,  // Cacaoweb
 						/webappstoolbarba\.texthelp\.com\//i,
 						/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) {
 					shouldSendCallback: function(data) {
 						// only send a fraction of errors
 						// 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
 					i.fa.fa-fw.fa-copy
 					|    #{translate("copy_project")}
 					|    #{translate("copy_project")}
-			!{moduleIncludes("editorLeftMenu:actions", locals)}
+			!= moduleIncludes("editorLeftMenu:actions", locals)
 
 
 	if (moduleIncludesAvailable("editorLeftMenu:sync"))
 	if (moduleIncludesAvailable("editorLeftMenu:sync"))
 		div(ng-show="!anonymous")
 		div(ng-show="!anonymous")
 			h4() #{translate("sync")}
 			h4() #{translate("sync")}
-			!{moduleIncludes("editorLeftMenu:sync", locals)}
+			!= moduleIncludes("editorLeftMenu:sync", locals)
 
 
 	h4(ng-show="!anonymous") #{translate("settings")}
 	h4(ng-show="!anonymous") #{translate("settings")}
 	form.settings(ng-controller="SettingsController", ng-show="!anonymous")
 	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-class="buttonClass"
 					ng-click="startFreeTrial('track-changes')"
 					ng-click="startFreeTrial('track-changes')"
 				) #{translate("start_free_trial")}
 				) #{translate("start_free_trial")}
+				|    
+				a.small(href, ng-click="toggleTrackChanges()") #{translate("cancel")}
 			p.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
 			p.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
+
 		.message(ng-show="project.owner._id != user.id")
 		.message(ng-show="project.owner._id != user.id")
 			p #{translate("ask_proj_owner_to_upgrade_for_history")}
 			p #{translate("ask_proj_owner_to_upgrade_for_history")}
+			p
+				a.small(href, ng-click="toggleTrackChanges()") #{translate("cancel")}
 
 
 	aside.change-list(
 	aside.change-list(
 		ng-controller="TrackChangesListController"
 		ng-controller="TrackChangesListController"

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

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

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

@@ -143,8 +143,9 @@ block content
 			function callback(response) {
 			function callback(response) {
 				// document.getElementById('msg').innerHTML = "Post ID: " + response['post_id'];
 				// 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").
 	script(type="text/javascript").

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

@@ -19,19 +19,20 @@ block content
 										h3  #{translate("update_account_info")}
 										h3  #{translate("update_account_info")}
 										form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate)
 										form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate)
 											input(type="hidden", name="_csrf", value=csrfToken)
 											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
 											.form-group
 												label(for='firstName').control-label #{translate("first_name")}
 												label(for='firstName').control-label #{translate("first_name")}
 												input.form-control(
 												input.form-control(
@@ -51,75 +52,77 @@ block content
 													type='submit',
 													type='submit',
 													ng-disabled="settingsForm.$invalid"
 													ng-disabled="settingsForm.$invalid"
 												)  #{translate("update")}
 												)  #{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)}
 							| !{moduleIncludes("userSettings", locals)}
 
 
 							hr
 							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')
 	script(type='text/ng-template', id='deleteAccountModalTemplate')

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

@@ -331,3 +331,19 @@ module.exports =
 	proxyUrls: {}
 	proxyUrls: {}
 	
 	
 	reloadModuleViewsOnEachRequest: true
 	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",
   "name": "web-sharelatex",
-  "version": "0.1.2",
+  "version": "0.1.3",
   "description": "The HTTP front end for ShareLaTeX",
   "description": "The HTTP front end for ShareLaTeX",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
@@ -19,6 +19,7 @@
     "express": "3.3.4",
     "express": "3.3.4",
     "fairy": "0.0.2",
     "fairy": "0.0.2",
     "jade": "~1.3.1",
     "jade": "~1.3.1",
+    "ldapjs": "^0.7.1",
     "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.0.0",
     "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.0.0",
     "lynx": "0.1.1",
     "lynx": "0.1.1",
     "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.0.0",
     "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",
     "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
     "socket.io": "0.9.16",
     "socket.io": "0.9.16",
     "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master",
     "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master",
-    "uid": "0.0.2",
     "underscore": "1.6.0",
     "underscore": "1.6.0",
     "underscore.string": "^3.0.2",
     "underscore.string": "^3.0.2",
     "v8-profiler": "^5.2.3",
     "v8-profiler": "^5.2.3",

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

@@ -83,7 +83,7 @@ define [
 		$scope.$on "project:joined", () ->
 		$scope.$on "project:joined", () ->
 			return if inited
 			return if inited
 			inited = true
 			inited = true
-			if $scope.project.deletedByExternalDataSource
+			if $scope?.project?.deletedByExternalDataSource
 				ide.showGenericMessageModal("Project Renamed or Deleted", """
 				ide.showGenericMessageModal("Project Renamed or Deleted", """
 					This project has either been renamed or deleted by an external data source such as Dropbox.
 					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.
 					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) ->
 		$scope.$on "chat:newMessage", (e, message) ->
 			if message?
 			if message?
-				if message.user.id != ide.$scope.user.id
+				if message?.user?.id != ide.$scope.user.id
 					if !$scope.ui.chatOpen
 					if !$scope.ui.chatOpen
 						$scope.unreadMessages += 1
 						$scope.unreadMessages += 1
 					flashTitle()
 					flashTitle()

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

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

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

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

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

@@ -18,7 +18,7 @@ define [
 		
 		
 		justSent = false	
 		justSent = false	
 		ide.socket.on "new-chat-message", (message) =>
 		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
 				# Nothing to do
 			else
 			else
 				ide.$scope.$apply () ->
 				ide.$scope.$apply () ->
@@ -51,9 +51,9 @@ define [
 					if messages.length < MESSAGE_LIMIT
 					if messages.length < MESSAGE_LIMIT
 						chat.state.atEnd = true
 						chat.state.atEnd = true
 					if !messages.reverse?
 					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'
 					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
 						chat.state.errored = true
 					else
 					else
 						messages.reverse()
 						messages.reverse()
@@ -65,7 +65,7 @@ define [
 		prependMessage = (message) ->
 		prependMessage = (message) ->
 			firstMessage = chat.state.messages[0]
 			firstMessage = chat.state.messages[0]
 			shouldGroup = firstMessage? and
 			shouldGroup = firstMessage? and
-				firstMessage.user.id == message.user.id and
+				firstMessage.user.id == message?.user?.id and
 				firstMessage.timestamp - message.timestamp < TIMESTAMP_GROUP_SIZE
 				firstMessage.timestamp - message.timestamp < TIMESTAMP_GROUP_SIZE
 			if shouldGroup
 			if shouldGroup
 				firstMessage.timestamp = message.timestamp
 				firstMessage.timestamp = message.timestamp
@@ -86,7 +86,7 @@ define [
 			
 			
 			lastMessage = chat.state.messages[chat.state.messages.length - 1]
 			lastMessage = chat.state.messages[chat.state.messages.length - 1]
 			shouldGroup = lastMessage? and
 			shouldGroup = lastMessage? and
-				lastMessage.user.id == message.user.id and
+				lastMessage.user.id == message?.user?.id and
 				message.timestamp - lastMessage.timestamp < TIMESTAMP_GROUP_SIZE
 				message.timestamp - lastMessage.timestamp < TIMESTAMP_GROUP_SIZE
 			if shouldGroup
 			if shouldGroup
 				lastMessage.timestamp = message.timestamp
 				lastMessage.timestamp = message.timestamp

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

@@ -162,13 +162,8 @@ define [
 			return if !path?
 			return if !path?
 			return path.split("/").slice(0, -1).join("/")
 			return path.split("/").slice(0, -1).join("/")
 
 
-		# forEachFolder: (callback) ->
-		# 	@forEachEntity (entity) ->
-		# 		if entity.type == "folder"
-		# 			callback(entity)
-
 		loadRootFolder: () ->
 		loadRootFolder: () ->
-			@$scope.rootFolder = @_parseFolder(@$scope.project.rootFolder[0])
+			@$scope.rootFolder = @_parseFolder(@$scope?.project?.rootFolder[0])
 
 
 		_parseFolder: (rawFolder) ->
 		_parseFolder: (rawFolder) ->
 			folder = {
 			folder = {
@@ -306,6 +301,7 @@ define [
 			return (child_path.slice(0, parent_path.length) == parent_path)
 			return (child_path.slice(0, parent_path.length) == parent_path)
 
 
 		_deleteEntityFromScope: (entity, options = { moveToDeleted: true }) ->
 		_deleteEntityFromScope: (entity, options = { moveToDeleted: true }) ->
+			return if !entity?
 			parent_folder = null
 			parent_folder = null
 			@forEachEntity (possible_entity, folder) ->
 			@forEachEntity (possible_entity, folder) ->
 				if possible_entity == entity
 				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
 			# We need this here as well as in FileTreeController
 			# since the file-entity diretive creates a new scope
 			# since the file-entity diretive creates a new scope
 			# that doesn't inherit from previous scopes.
 			# that doesn't inherit from previous scopes.
-			return '0' if entity.type == "folder"
+			return '0' if entity?.type == "folder"
 			return '1'
 			return '1'
 
 
 		$scope.openNewDocModal = () ->
 		$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"]
 			IGNORE_FILES = ["output.fls", "output.fdb_latexmk"]
 			$scope.pdf.outputFiles = []
 			$scope.pdf.outputFiles = []
+
+			if !response.outputFiles?
+				return
 			for file in response.outputFiles
 			for file in response.outputFiles
 				if IGNORE_FILES.indexOf(file.path) == -1
 				if IGNORE_FILES.indexOf(file.path) == -1
 					# Turn 'output.blg' into 'blg file'.
 					# Turn 'output.blg' into 'blg file'.
@@ -80,16 +83,17 @@ define [
 
 
 					$scope.pdf.logEntryAnnotations = {}
 					$scope.pdf.logEntryAnnotations = {}
 					for entry in logEntries.all
 					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 () ->
 				.error () ->
 					$scope.pdf.logEntries = []
 					$scope.pdf.logEntries = []

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

@@ -24,6 +24,6 @@ define [
 
 
 			clearHighlights: () ->
 			clearHighlights: () ->
 				for h in @highlightElements
 				for h in @highlightElements
-					h.remove()
+					h?.remove()
 				@highlightElements = []
 				@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)->
 			pricing.plan(planCode, { quantity: 1 }).currency(MultiCurrencyPricing.currencyCode).done (price)->
 				totalPriceExTax = parseFloat(price.next.total)
 				totalPriceExTax = parseFloat(price.next.total)
 				$scope.$evalAsync () ->
 				$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 = ""
 			price = ""
 
 

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

@@ -84,7 +84,6 @@
 }
 }
 .qq-upload-spinner {
 .qq-upload-spinner {
     display: inline-block;
     display: inline-block;
-    background: url("loading.gif");
     width: 15px;
     width: 15px;
     height: 15px;
     height: 15px;
     vertical-align: text-bottom;
     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)
 			@ratelimiter.addCount.callsArgWith(1, null, true)
 			@CompileManager._checkIfAutoCompileLimitHasBeenHit true, (err, canCompile)=>
 			@CompileManager._checkIfAutoCompileLimitHasBeenHit true, (err, canCompile)=>
 				args = @ratelimiter.addCount.args[0][0]
 				args = @ratelimiter.addCount.args[0][0]
-				args.throttle.should.equal 15
+				args.throttle.should.equal 25
 				args.subjectName.should.equal "everyone"
 				args.subjectName.should.equal "everyone"
-				args.timeInterval.should.equal 15
+				args.timeInterval.should.equal 20
 				args.endpointName.should.equal "auto_compile"
 				args.endpointName.should.equal "auto_compile"
 				canCompile.should.equal true
 				canCompile.should.equal true
 				done()
 				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')
 SandboxedModule = require('sandboxed-module')
 sinon = require('sinon')
 sinon = require('sinon')
 Errors = require "../../../../app/js/errors"
 Errors = require "../../../../app/js/errors"
-
+expect = require("chai").expect
 Project = class Project
 Project = class Project
 
 
 project = _id : "1234566", rootFolder:[]
 project = _id : "1234566", rootFolder:[]
@@ -221,6 +221,7 @@ describe 'project model', ->
 				assert.equal element, undefined
 				assert.equal element, undefined
 				done()
 				done()
 
 
+
 		describe "where duplicate folder exists", ->
 		describe "where duplicate folder exists", ->
 
 
 			beforeEach ->
 			beforeEach ->
@@ -266,7 +267,23 @@ describe 'project model', ->
 					element.name.should.equal "other.tex"
 					element.name.should.equal "other.tex"
 					done()
 					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', ()->
 	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"
 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"
 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", ->
 describe "Opening", ->
 
 
 	before (done) ->
 	before (done) ->
@@ -26,20 +38,14 @@ describe "Opening", ->
 						return done("smoke test: does not have csrf token")
 						return done("smoke test: does not have csrf token")
 					csrf = csrfMatches[1]
 					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)-> 
 	after (done)-> 
 		command =  """
 		command =  """