Răsfoiți Sursa

Merge branch 'master' into sk-rate-limit-cluster

Shane Kilkelly 9 ani în urmă
părinte
comite
525e871d55

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

@@ -148,6 +148,7 @@ module.exports = AuthenticationController =
 			return next()
 		else
 			logger.log url:req.url, "user trying to access endpoint not in global whitelist"
+			AuthenticationController._setRedirectInSession(req)
 			return res.redirect "/login"
 
 	httpAuth: basicAuth (user, pass)->

+ 23 - 5
services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee

@@ -4,6 +4,7 @@ UserGetter = require "../User/UserGetter"
 CollaboratorsHandler = require('./CollaboratorsHandler')
 CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
 logger = require('logger-sharelatex')
+Settings = require('settings-sharelatex')
 EmailHelper = require "../Helpers/EmailHelper"
 EditorRealTimeController = require("../Editor/EditorRealTimeController")
 NotificationsBuilder = require("../Notifications/NotificationsBuilder")
@@ -21,6 +22,16 @@ module.exports = CollaboratorsInviteController =
 				return next(err)
 			res.json({invites: invites})
 
+	_checkShouldInviteEmail: (email, callback=(err, shouldAllowInvite)->) ->
+		if Settings.restrictInvitesToExistingAccounts == true
+			logger.log {email}, "checking if user exists with this email"
+			UserGetter.getUser {email: email}, {_id: 1}, (err, user) ->
+				return callback(err) if err?
+				userExists = user? and user?._id?
+				callback(null, userExists)
+		else
+			callback(null, true)
+
 	inviteToProject: (req, res, next) ->
 		projectId = req.params.Project_id
 		email = req.body.email
@@ -37,13 +48,20 @@ module.exports = CollaboratorsInviteController =
 			if !email? or email == ""
 				logger.log {projectId, email, sendingUserId}, "invalid email address"
 				return res.sendStatus(400)
-			CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
+			CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)->
 				if err?
-					logger.err {projectId, email, sendingUserId}, "error creating project invite"
+					logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address"
 					return next(err)
-				logger.log {projectId, email, sendingUserId}, "invite created"
-				EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
-				return res.json {invite: invite}
+				if !shouldAllowInvite
+					logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address"
+					return res.json {invite: null, error: 'cannot_invite_non_user'}
+				CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
+					if err?
+						logger.err {projectId, email, sendingUserId}, "error creating project invite"
+						return next(err)
+					logger.log {projectId, email, sendingUserId}, "invite created"
+					EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
+					return res.json {invite: invite}
 
 	revokeInvite: (req, res, next) ->
 		projectId = req.params.Project_id

+ 49 - 0
services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee

@@ -0,0 +1,49 @@
+_ = require("underscore")
+settings = require "settings-sharelatex"
+
+module.exports = _.template """
+<table class="row" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
+	<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 16px; padding-left: 16px; padding-right: 16px; text-align: left; width: 564px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
+		<h3 class="avoid-auto-linking" style="Margin: 0; Margin-bottom: px; color: inherit; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 24px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: px; padding: 0; text-align: left; word-wrap: normal;">
+			<%= title %>
+		</h3>
+		<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table> 
+		<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
+			<%= greeting %>
+		</p>
+		<p class="avoid-auto-linking" style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
+			<%= message %>
+		</p>
+		<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table> 
+		<center data-parsed="" style="min-width: 532px; width: 100%;">
+			<table class="button float-center" style="Margin: 0 0 16px 0; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 0 16px 0; padding: 0; text-align: center; vertical-align: top; width: auto;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; background: #a93529; border: 2px solid #a93529; border-collapse: collapse !important; color: #fefefe; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
+				<a href="<%= ctaURL %>" style="Margin: 0; border: 0 solid #a93529; border-radius: 3px; color: #fefefe; display: inline-block; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: bold; line-height: 1.3; margin: 0; padding: 8px 16px 8px 16px; text-align: left; text-decoration: none;">
+					<%= ctaText %>
+				</a>
+			</td></tr></table></td></tr></table>
+		</center>
+		<% if (secondaryMessage) { %>
+			<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table> 
+			<p class="avoid-auto-linking" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
+				<%= secondaryMessage %>
+			</p>
+		<% } %>
+	</th>
+	<th class="expander" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0 !important; text-align: left; visibility: hidden; width: 0;"></th></tr></table></th>
+</tr></tbody></table>
+<% if (gmailGoToAction) { %>
+	<script type="application/ld+json">
+		{
+			"@context": "http://schema.org",
+			"@type": "EmailMessage",
+			"potentialAction": {
+				"@type": "ViewAction",
+				"target": "<%= gmailGoToAction.target %>",
+				"url": "<%= gmailGoToAction.target %>",
+				"name": "<%= gmailGoToAction.name %>"
+			},
+			"description": "<%= gmailGoToAction.description %>"
+		}
+	</script>
+<% } %>
+"""

+ 44 - 58
services/web/app/coffee/Features/Email/EmailBuilder.coffee

@@ -1,6 +1,12 @@
 _ = require('underscore')
+
 PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
 NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
+BaseWithHeaderEmailLayout = require("./Layouts/BaseWithHeaderEmailLayout") 
+
+SingleCTAEmailBody = require("./Bodies/SingleCTAEmailBody")
+
+
 settings = require("settings-sharelatex")
 
 
@@ -61,7 +67,7 @@ ShareLaTeX Co-founder
 
 templates.passwordResetRequested =
 	subject:  _.template "Password Reset - #{settings.appName}"
-	layout: NotificationEmailLayout
+	layout: BaseWithHeaderEmailLayout
 	type:"notification"
 	plainTextTemplate: _.template """
 Password Reset
@@ -78,36 +84,21 @@ Thank you
 
 #{settings.appName} - <%= siteUrl %>
 """
-	compiledTemplate: _.template """
-<h2>Password Reset</h2>
-<p>
-We got a request to reset your #{settings.appName} password.
-<p>
-<center>
-	<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
-		<div style="padding-right:10px;padding-left:10px">
-			<a href="<%= setNewPasswordUrl %>" style="text-decoration:none" target="_blank">
-				<span style= "font-size:16px;font-family:Arial;font-weight:bold;color:#fff;white-space:nowrap;display:block; text-align:center">
-		  			Reset password
-				</span>
-			</a>
-		</div>
-	</div>
-</center>
-
-If you ignore this message, your password won't be changed.
-<p>
-If you didn't request a password reset, let us know.
-
-</p>
-<p>Thank you</p>
-<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
-"""
+	compiledTemplate: (opts) -> 
+		SingleCTAEmailBody({
+			title: "Password Reset"
+			greeting: "Hi,"
+			message: "We got a request to reset your #{settings.appName} password."
+			secondaryMessage: "If you ignore this message, your password won't be changed.<br>If you didn't request a password reset, let us know."
+			ctaText: "Reset password"
+			ctaURL: opts.setNewPasswordUrl
+			gmailGoToAction: null
+		})
 
 
 templates.projectInvite =
 	subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
-	layout: NotificationEmailLayout
+	layout: BaseWithHeaderEmailLayout
 	type:"notification"
 	plainTextTemplate: _.template """
 Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
@@ -118,23 +109,25 @@ Thank you
 
 #{settings.appName} - <%= siteUrl %>
 """
-	compiledTemplate: _.template """
-<p>Hi, <%= owner.email %> wants to share <a href="<%= inviteUrl %>">'<%= project.name %>'</a> with you</p>
-<center>
-			<a style="text-decoration: none; width: 200px; background-color: #a93629; border: 1px solid #e24b3b; border-radius: 3px; padding: 15px; margin: 24px; display: block;" href="<%= inviteUrl %>" style="text-decoration:none" target="_blank">
-				<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
-					View Project
-				</span>
-			</a>
-</center>
-<p> Thank you</p>
-<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
-"""
+	compiledTemplate: (opts) -> 
+		SingleCTAEmailBody({
+			title: "#{ opts.project.name } &ndash; shared by #{ opts.owner.email }"
+			greeting: "Hi,"
+			message: "#{ opts.owner.email } wants to share &ldquo;#{ opts.project.name }&rdquo; with you."
+			secondaryMessage: null
+			ctaText: "View project"
+			ctaURL: opts.inviteUrl
+			gmailGoToAction: 
+				target: opts.inviteUrl
+				name: "View project"
+				description: "Join #{ opts.project.name } at ShareLaTeX"
+		})
+
 
 
 templates.completeJoinGroupAccount =
 	subject: _.template "Verify Email to join <%= group_name %> group"
-	layout: NotificationEmailLayout
+	layout: BaseWithHeaderEmailLayout
 	type:"notification"
 	plainTextTemplate: _.template """
 Hi, please verify your email to join the <%= group_name %> and get your free premium account
@@ -145,23 +138,16 @@ Thank You
 
 #{settings.appName} - <%= siteUrl %>
 """
-	compiledTemplate: _.template """
-<p>Hi, please verify your email to join the <%= group_name %> and get your free premium account</p>
-<center>
-	<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
-		<div style="padding-right:10px;padding-left:10px">
-			<a href="<%= completeJoinUrl %>" style="text-decoration:none" target="_blank">
-				<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
-		  			Verify now
-				</span>
-			</a>
-		</div>
-	</div>
-</center>
-<p> Thank you</p>
-<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
-"""
-
+	compiledTemplate: (opts) -> 
+		SingleCTAEmailBody({
+			title: "Verify Email to join #{ opts.group_name } group"
+			greeting: "Hi,"
+			message: "please verify your email to join the #{ opts.group_name } group and get your free premium account."
+			secondaryMessage: null
+			ctaText: "Verify now"
+			ctaURL: opts.completeJoinUrl
+			gmailGoToAction: null
+		})
 
 module.exports =
 	templates: templates
@@ -177,4 +163,4 @@ module.exports =
 			html: template.layout(opts)
 			text: template?.plainTextTemplate?(opts)
 			type:template.type
-		}
+		}

+ 380 - 0
services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee

@@ -0,0 +1,380 @@
+_ = require("underscore")
+settings = require "settings-sharelatex"
+
+module.exports = _.template """
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" style="Margin: 0; background: #f6f6f6 !important; margin: 0; min-height: 100%; padding: 0;">
+	<head>
+		
+		
+		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+		<meta name="viewport" content="width=device-width">
+		<title>Project invite</title>
+		<style>.avoid-auto-linking a,
+.avoid-auto-linking a[href] {
+	color: #a93529 !important;
+	text-decoration: none !important;
+	-moz-hyphens: none;
+	-ms-hyphens: none;
+	-webkit-hyphens: none;
+	hyphens: none; }
+	.avoid-auto-linking a:visited,
+	.avoid-auto-linking a[href]:visited {
+		color: #a93529; }
+	.avoid-auto-linking a:hover,
+	.avoid-auto-linking a[href]:hover {
+		color: #80281f; }
+	.avoid-auto-linking a:active,
+	.avoid-auto-linking a[href]:active {
+		color: #80281f; }
+@media only screen {
+	html {
+		min-height: 100%;
+		background: #f6f6f6;
+	}
+}
+
+@media only screen and (max-width: 596px) {
+	.small-float-center {
+		margin: 0 auto !important;
+		float: none !important;
+		text-align: center !important;
+	}
+
+	.small-text-center {
+		text-align: center !important;
+	}
+
+	.small-text-left {
+		text-align: left !important;
+	}
+
+	.small-text-right {
+		text-align: right !important;
+	}
+}
+
+@media only screen and (max-width: 596px) {
+	.hide-for-large {
+		display: block !important;
+		width: auto !important;
+		overflow: visible !important;
+		max-height: none !important;
+		font-size: inherit !important;
+		line-height: inherit !important;
+	}
+}
+
+@media only screen and (max-width: 596px) {
+	table.body table.container .hide-for-large,
+	table.body table.container .row.hide-for-large {
+		display: table !important;
+		width: 100% !important;
+	}
+}
+
+@media only screen and (max-width: 596px) {
+	table.body table.container .callout-inner.hide-for-large {
+		display: table-cell !important;
+		width: 100% !important;
+	}
+}
+
+@media only screen and (max-width: 596px) {
+	table.body table.container .show-for-large {
+		display: none !important;
+		width: 0;
+		mso-hide: all;
+		overflow: hidden;
+	}
+}
+
+@media only screen and (max-width: 596px) {
+	table.body img {
+		width: auto;
+		height: auto;
+	}
+
+	table.body center {
+		min-width: 0 !important;
+	}
+
+	table.body .container {
+		width: 95% !important;
+	}
+
+	table.body .columns,
+	table.body .column {
+		height: auto !important;
+		-moz-box-sizing: border-box;
+		-webkit-box-sizing: border-box;
+		box-sizing: border-box;
+		padding-left: 16px !important;
+		padding-right: 16px !important;
+	}
+
+	table.body .columns .column,
+	table.body .columns .columns,
+	table.body .column .column,
+	table.body .column .columns {
+		padding-left: 0 !important;
+		padding-right: 0 !important;
+	}
+
+	table.body .collapse .columns,
+	table.body .collapse .column {
+		padding-left: 0 !important;
+		padding-right: 0 !important;
+	}
+
+	td.small-1,
+	th.small-1 {
+		display: inline-block !important;
+		width: 8.33333% !important;
+	}
+
+	td.small-2,
+	th.small-2 {
+		display: inline-block !important;
+		width: 16.66667% !important;
+	}
+
+	td.small-3,
+	th.small-3 {
+		display: inline-block !important;
+		width: 25% !important;
+	}
+
+	td.small-4,
+	th.small-4 {
+		display: inline-block !important;
+		width: 33.33333% !important;
+	}
+
+	td.small-5,
+	th.small-5 {
+		display: inline-block !important;
+		width: 41.66667% !important;
+	}
+
+	td.small-6,
+	th.small-6 {
+		display: inline-block !important;
+		width: 50% !important;
+	}
+
+	td.small-7,
+	th.small-7 {
+		display: inline-block !important;
+		width: 58.33333% !important;
+	}
+
+	td.small-8,
+	th.small-8 {
+		display: inline-block !important;
+		width: 66.66667% !important;
+	}
+
+	td.small-9,
+	th.small-9 {
+		display: inline-block !important;
+		width: 75% !important;
+	}
+
+	td.small-10,
+	th.small-10 {
+		display: inline-block !important;
+		width: 83.33333% !important;
+	}
+
+	td.small-11,
+	th.small-11 {
+		display: inline-block !important;
+		width: 91.66667% !important;
+	}
+
+	td.small-12,
+	th.small-12 {
+		display: inline-block !important;
+		width: 100% !important;
+	}
+
+	.columns td.small-12,
+	.column td.small-12,
+	.columns th.small-12,
+	.column th.small-12 {
+		display: block !important;
+		width: 100% !important;
+	}
+
+	table.body td.small-offset-1,
+	table.body th.small-offset-1 {
+		margin-left: 8.33333% !important;
+		Margin-left: 8.33333% !important;
+	}
+
+	table.body td.small-offset-2,
+	table.body th.small-offset-2 {
+		margin-left: 16.66667% !important;
+		Margin-left: 16.66667% !important;
+	}
+
+	table.body td.small-offset-3,
+	table.body th.small-offset-3 {
+		margin-left: 25% !important;
+		Margin-left: 25% !important;
+	}
+
+	table.body td.small-offset-4,
+	table.body th.small-offset-4 {
+		margin-left: 33.33333% !important;
+		Margin-left: 33.33333% !important;
+	}
+
+	table.body td.small-offset-5,
+	table.body th.small-offset-5 {
+		margin-left: 41.66667% !important;
+		Margin-left: 41.66667% !important;
+	}
+
+	table.body td.small-offset-6,
+	table.body th.small-offset-6 {
+		margin-left: 50% !important;
+		Margin-left: 50% !important;
+	}
+
+	table.body td.small-offset-7,
+	table.body th.small-offset-7 {
+		margin-left: 58.33333% !important;
+		Margin-left: 58.33333% !important;
+	}
+
+	table.body td.small-offset-8,
+	table.body th.small-offset-8 {
+		margin-left: 66.66667% !important;
+		Margin-left: 66.66667% !important;
+	}
+
+	table.body td.small-offset-9,
+	table.body th.small-offset-9 {
+		margin-left: 75% !important;
+		Margin-left: 75% !important;
+	}
+
+	table.body td.small-offset-10,
+	table.body th.small-offset-10 {
+		margin-left: 83.33333% !important;
+		Margin-left: 83.33333% !important;
+	}
+
+	table.body td.small-offset-11,
+	table.body th.small-offset-11 {
+		margin-left: 91.66667% !important;
+		Margin-left: 91.66667% !important;
+	}
+
+	table.body table.columns td.expander,
+	table.body table.columns th.expander {
+		display: none !important;
+	}
+
+	table.body .right-text-pad,
+	table.body .text-pad-right {
+		padding-left: 10px !important;
+	}
+
+	table.body .left-text-pad,
+	table.body .text-pad-left {
+		padding-right: 10px !important;
+	}
+
+	table.menu {
+		width: 100% !important;
+	}
+
+	table.menu td,
+	table.menu th {
+		width: auto !important;
+		display: inline-block !important;
+	}
+
+	table.menu.vertical td,
+	table.menu.vertical th,
+	table.menu.small-vertical td,
+	table.menu.small-vertical th {
+		display: block !important;
+	}
+
+	table.menu[align="center"] {
+		width: auto !important;
+	}
+
+	table.button.small-expand,
+	table.button.small-expanded {
+		width: 100% !important;
+	}
+
+	table.button.small-expand table,
+	table.button.small-expanded table {
+		width: 100%;
+	}
+
+	table.button.small-expand table a,
+	table.button.small-expanded table a {
+		text-align: center !important;
+		width: 100% !important;
+		padding-left: 0 !important;
+		padding-right: 0 !important;
+	}
+
+	table.button.small-expand center,
+	table.button.small-expanded center {
+		min-width: 0;
+	}
+}</style>
+	</head>
+	<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0" bgcolor="#F6F6F6" style="-moz-box-sizing: border-box; -ms-text-size-adjust: 100%; -webkit-box-sizing: border-box; -webkit-text-size-adjust: 100%; Margin: 0; background: #f6f6f6 !important; box-sizing: border-box; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; min-height: 100%; min-width: 100%; padding: 0; text-align: left; width: 100% !important;">
+		<!-- <span class="preheader"></span> -->
+		<table class="body" border="0" cellspacing="0" cellpadding="0" width="100%" height="100%" style="Margin: 0; background: #f6f6f6 !important; border-collapse: collapse; border-spacing: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; height: 100%; line-height: 1.3; margin: 0; min-height: 100%; padding: 0; text-align: left; vertical-align: top; width: 100%;">
+			<tr style="padding: 0; text-align: left; vertical-align: top;">
+				<td class="body-cell" align="center" valign="top" bgcolor="#F6F6F6" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; background: #f6f6f6 !important; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; padding-bottom: 20px; text-align: left; vertical-align: top; word-wrap: break-word;">
+					<center data-parsed="" style="min-width: 580px; width: 100%;">
+						
+						<table align="center" class="wrapper header float-center" style="Margin: 0 auto; background: #fefefe; border-bottom: solid 1px #cfcfcf; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; padding: 0; text-align: center; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td class="wrapper-inner" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 20px; text-align: left; vertical-align: top; word-wrap: break-word;">
+							<table align="center" class="container" style="Margin: 0 auto; background: transparent; border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
+							<table class="row collapse" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
+								<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 0; padding-left: 0; padding-right: 0; text-align: left; width: 588px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
+								<h1 class="sl-logotype" style="Margin: 0; Margin-bottom: 0; color: #333333; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 26px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 0; padding: 0; text-align: left; word-wrap: normal;">
+									<span>S</span><span class="sl-logotype-small" style="font-size: 80%;">HARE</span><span>L</span><span class="sl-logotype-small" style="font-size: 80%;">A</span><span>T</span><span class="sl-logotype-small" style="font-size: 80%;">E</span><span>X</span>
+								</h1>
+								</th>
+								<th class="expander" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0 !important; text-align: left; visibility: hidden; width: 0;"></th></tr></table></th>
+							</tr></tbody></table>
+							</td></tr></tbody></table>
+						</td></tr></table>
+						<table class="spacer float-center" style="Margin: 0 auto; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; padding: 0; text-align: center; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table> 
+						<table align="center" class="container main float-center" style="Margin: 0 auto; Margin-top: 10px; background: #fefefe; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; margin-top: 10px; padding: 0; text-align: center; vertical-align: top; width: 580px;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
+							<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table> 
+							
+							<%= body %>
+							
+							<table class="wrapper secondary" align="center" style="background: #f6f6f6; border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td class="wrapper-inner" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
+								<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="10px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 10px; font-weight: normal; hyphens: auto; line-height: 10px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table> 
+								<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;"><small style="color: #7a7a7a; font-size: 80%;">
+									#{ settings.appName} &bull; <a href="#{ settings.siteUrl }" style="Margin: 0; color: #a93529; font-family: Helvetica, Arial, sans-serif; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; text-decoration: none;">#{ settings.siteUrl }</a>
+								</small></p>
+							</td></tr></table>
+						</td></tr></tbody></table>
+						
+					</center>
+				</td>
+			</tr>
+		</table>
+		<!-- prevent Gmail on iOS font size manipulation -->
+	 <div style="display:none; white-space:nowrap; font:15px courier; line-height:0;"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </div>
+	</body>
+</html>
+
+"""

+ 1 - 1
services/web/app/coffee/Features/Project/ProjectController.coffee

@@ -197,7 +197,7 @@ module.exports = ProjectController =
 			user_id = null
 
 		project_id = req.params.Project_id
-		logger.log project_id:project_id, "loading editor"
+		logger.log project_id:project_id, anonymous:anonymous, user_id:user_id, "loading editor"
 
 		async.parallel {
 			project: (cb)->

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

@@ -244,6 +244,8 @@ module.exports = (app, webRouter, apiRouter)->
 		for key, value of Settings.nav
 			res.locals.nav[key] = _.clone(Settings.nav[key])
 		res.locals.templates = Settings.templateLinks
+		if res.locals.nav.header
+			console.error {}, "The `nav.header` setting is no longer supported, use `nav.header_extras` instead"
 		next()
 
 	webRouter.use (req, res, next) ->

+ 1 - 1
services/web/app/views/general/closed.jade

@@ -11,4 +11,4 @@ block content
 						| Sorry, ShareLaTeX is briefly down for maintenance.
 						| We should be back within minutes, but if not, or you have
 						| an urgent request, please contact us at
-						| support@sharelatex.com
+						| #{settings.adminEmail}

+ 35 - 7
services/web/app/views/layout/navbar.jade

@@ -24,7 +24,10 @@ nav.navbar.navbar-default
 							li
 								a(href="/admin/user") Manage Users
 
-				each item in nav.header
+
+				// loop over header_extras
+				each item in nav.header_extras
+
 					if ((item.only_when_logged_in && getSessionUser()) || (item.only_when_logged_out && (!getSessionUser())) || (!item.only_when_logged_out && !item.only_when_logged_in))
 						if item.dropdown
 							li.dropdown(class=item.class, dropdown)
@@ -35,9 +38,6 @@ nav.navbar.navbar-default
 									each child in item.dropdown
 										if child.divider
 											li.divider
-										else if child.user_email
-											li
-												div.subdued #{getUserEmail()}
 										else
 											li
 												if child.url
@@ -50,7 +50,35 @@ nav.navbar.navbar-default
 									a(href=item.url, class=item.class) !{translate(item.text)}
 								else
 									| !{translate(item.text)}
-				
-				
-				
 
+				// logged out
+				if !getSessionUser()
+					// register link
+					if !externalAuthenticationSystemUsed()
+						li
+							a(href="/register") #{translate('register')}
+
+					// login link
+					li
+						a(href="/login") #{translate('log_in')}
+
+				// projects link and account menu
+				if getSessionUser()
+					li
+						a(href="/project") #{translate('Projects')}
+					li.dropdown(dropdown)
+						a.dropbodw-toggle(href, dropdown-toggle)
+							| #{translate('Account')}
+							b.caret
+						ul.dropdown-menu
+							li
+								div.subdued #{getUserEmail()}
+							li.divider
+							li
+								a(href="/user/settings") #{translate('Account Settings')}
+							if nav.showSubscriptionLink
+								li
+									a(href="/user/subscription") #{translate('subscription')}
+							li.divider
+							li
+								a(href="/logout") #{translate('log_out')}

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

@@ -137,10 +137,15 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 					p.small(ng-show="startedFreeTrial")
 						| #{translate("refresh_page_after_starting_free_trial")}.
 
-	.modal-footer
+	.modal-footer.modal-footer-share
 		.modal-footer-left
 			i.fa.fa-refresh.fa-spin(ng-show="state.inflight")
-			span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
+			span.text-danger.error(ng-show="state.error")
+				span(ng-switch="state.errorReason")
+					span(ng-switch-when="cannot_invite_non_user")
+						| #{translate("cannot_invite_non_user")}
+					span(ng-switch-default)
+						| #{translate("generic_something_went_wrong")}
 		button.btn.btn-default(
 			ng-click="done()"
 		) #{translate("close")}

+ 9 - 29
services/web/config/settings.defaults.coffee

@@ -286,6 +286,10 @@ module.exports = settings =
 	# Cookie max age (in milliseconds). Set to false for a browser session.
 	cookieSessionLength: 5 * 24 * 60 * 60 * 1000 # 5 days
 
+	# When true, only allow invites to be sent to email addresses that
+	# already have user accounts
+	restrictInvitesToExistingAccounts: false
+
 	# Should we allow access to any page without logging in? This includes
 	# public projects, /learn, /templates, about pages, etc.
 	allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false
@@ -341,35 +345,11 @@ module.exports = settings =
 			url: "https://github.com/sharelatex/sharelatex"
 		}]
 
-		header: [{
-			text: "Register"
-			url: "/register"
-			only_when_logged_out: true
-		}, {
-			text: "Log In"
-			url: "/login"
-			only_when_logged_out: true
-		}, {
-			text: "Projects"
-			url: "/project"
-			only_when_logged_in: true
-		}, {
-			text: "Account"
-			only_when_logged_in: true
-			dropdown: [{
-				user_email: true
-			},{
-				divider: true
-			}, {
-				text: "Account Settings"
-				url: "/user/settings"
-			}, {
-				divider: true
-			}, {
-				text: "Log out"
-				url: "/logout"
-			}]
-		}]
+		showSubscriptionLink: false
+
+		header_extras: []
+		# Example:
+		#   header_extras: [{text: "Some Page", url: "http://example.com/some/page", class: "subdued"}]
 
 	customisation: {}
 

+ 18 - 10
services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee

@@ -8,6 +8,7 @@ define [
 		}
 		$scope.state = {
 			error: null
+			errorReason: null
 			inflight: false
 			startedFreeTrial: false
 			invites: []
@@ -69,7 +70,8 @@ define [
 
 				members = $scope.inputs.contacts
 				$scope.inputs.contacts = []
-				$scope.state.error = null
+				$scope.state.error = false
+				$scope.state.errorReason = null
 				$scope.state.inflight = true
 
 				if !$scope.project.invites?
@@ -101,17 +103,22 @@ define [
 
 					request
 						.success (data) ->
-							if data.invite
-								invite = data.invite
-								$scope.project.invites.push invite
+							if data.error
+								$scope.state.error = true
+								$scope.state.errorReason = "#{data.error}"
+								$scope.state.inflight = false
 							else
-								if data.users?
-									users = data.users
-								else if data.user?
-									users = [data.user]
+								if data.invite
+									invite = data.invite
+									$scope.project.invites.push invite
 								else
-									users = []
-								$scope.project.members.push users...
+									if data.users?
+										users = data.users
+									else if data.user?
+										users = [data.user]
+									else
+										users = []
+									$scope.project.members.push users...
 
 							setTimeout () ->
 								# Give $scope a chance to update $scope.canAddCollaborators
@@ -121,6 +128,7 @@ define [
 						.error () ->
 							$scope.state.inflight = false
 							$scope.state.error = true
+							$scope.state.errorReason = null
 
 			$timeout addMembers, 50 # Give email list a chance to update
 

+ 4 - 10
services/web/public/js/ace-1.2.5/mode-latex.js

@@ -242,10 +242,6 @@ var createLatexWorker = function (session) {
         var annotations = [];
         var newRange = {};
         var cursor = selection.getCursor();
-        var maxRow = session.getLength() - 1;
-        var maxCol = (maxRow > 0) ? session.getLine(maxRow).length : 0;
-        var cursorAtEndOfDocument = (cursor.row == maxRow) && (cursor.column === maxCol);
-
         suppressions = [];
 
         for (var i = 0, len = hints.length; i<len; i++) {
@@ -254,8 +250,8 @@ var createLatexWorker = function (session) {
             var suppressedChanges = 0;
             var hintRange = new Range(hint.start_row, hint.start_col, hint.end_row, hint.end_col);
 
-            var cursorInRange = hintRange.insideEnd(cursor.row, cursor.column);
-            var cursorAtStart = hintRange.isStart(cursor.row, cursor.column - 1); // cursor after start not before
+            var cursorInRange = hintRange.insideStart(cursor.row, cursor.column);
+            var cursorAtStart = hintRange.isStart(cursor.row, cursor.column);
             var cursorAtEnd = hintRange.isEnd(cursor.row, cursor.column);
             if (hint.suppressIfEditing && (cursorAtStart || cursorAtEnd)) {
                 suppressions.push(hintRange);
@@ -293,10 +289,8 @@ var createLatexWorker = function (session) {
                 cursorInRange = newRange[key].cursorInRange;
                 hint = newRange[key].hint;
                 var errorAtStart = (hint.row === hint.start_row && hint.column === hint.start_col);
-                var movableStart = (cursorInRange && !errorAtStart) && !cursorAtEndOfDocument;
-                var movableEnd = (cursorInRange && errorAtStart) && !cursorAtEndOfDocument;
-                var a = movableStart ? cursorAnchor : doc.createAnchor(new_range.start);
-                var b = movableEnd ? cursorAnchor : doc.createAnchor(new_range.end);
+                var a = (cursorInRange && !errorAtStart) ? cursorAnchor : doc.createAnchor(new_range.start);
+                var b = (cursorInRange && errorAtStart) ? cursorAnchor : doc.createAnchor(new_range.end);
                 var range = new Range();
                 range.start = a;
                 range.end = b;

+ 102 - 403
services/web/public/js/ace-1.2.5/worker-latex.js

@@ -1490,7 +1490,7 @@ var Tokenise = function (text) {
             var controlSequence = NEXTCS.exec(text);
             var nextSpecialPos = controlSequence === null ? idx : controlSequence.index;
             if (nextSpecialPos === idx) {
-                Tokens.push([lineNumber, code, pos, idx + 1, text[idx], "control-symbol"]);
+                Tokens.push([lineNumber, code, pos, idx + 1, text[idx]]);
                 idx = SPECIAL.lastIndex = idx + 1;
                 char = text[nextSpecialPos];
                 if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;};
@@ -1508,7 +1508,12 @@ var Tokenise = function (text) {
         } else if (code === "}") {  // close group
             Tokens.push([lineNumber, code, pos]);
         } else if (code === "$") {  // math mode
-            Tokens.push([lineNumber, code, pos]);
+            if (text[idx] === "$") {
+                idx = SPECIAL.lastIndex = idx + 1;
+                Tokens.push([lineNumber, "$$", pos]);
+            } else {
+                Tokens.push([lineNumber, code, pos]);
+            }
         } else if (code === "&") {  // tabalign
             Tokens.push([lineNumber, code, pos]);
         } else if (code === "#") {  // macro parameter
@@ -1589,29 +1594,6 @@ var read1name = function (TokeniseResult, k) {
     }
 };
 
-var read1filename = function (TokeniseResult, k) {
-    var Tokens = TokeniseResult.tokens;
-    var text = TokeniseResult.text;
-
-    var fileName = "";
-    for (var j = k + 1, tok; (tok = Tokens[j]); j++) {
-        if (tok[1] === "Text") {
-            var str = text.substring(tok[2], tok[3]);
-            if (!str.match(/^\S*$/)) { break; }
-            fileName = fileName + str;
-        } else if (tok[1] === "_") {
-            fileName = fileName + "_";
-        } else {
-            break;
-        }
-    }
-    if (fileName.length > 0) {
-        return  j; // advance past these tokens
-    } else {
-        return null;
-    }
-};
-
 var readOptionalParams = function(TokeniseResult, k) {
     var Tokens = TokeniseResult.tokens;
     var text = TokeniseResult.text;
@@ -1715,6 +1697,7 @@ var readUrl = function(TokeniseResult, k) {
     return null;
 };
 
+
 var InterpretTokens = function (TokeniseResult, ErrorReporter) {
     var Tokens = TokeniseResult.tokens;
     var linePosition = TokeniseResult.linePosition;
@@ -1723,9 +1706,7 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
 
     var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo;
     var TokenError = ErrorReporter.TokenError;
-    var Environments = new EnvHandler(ErrorReporter);
-
-    var nextGroupMathMode = null; // if the next group should have math mode on or off (for \hbox)
+    var Environments = [];
 
     for (var i = 0, len = Tokens.length; i < len; i++) {
         var token = Tokens[i];
@@ -1810,407 +1791,128 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
             } else if (seq === "url") {
                 newPos = readUrl(TokeniseResult, i);
                 if (newPos === null) { TokenError(token, "invalid url command"); } else {i = newPos;};
-            } else if (seq === "left" || seq === "right") {
-                var nextToken = Tokens[i+1];
-                char = "";
-                if (nextToken && nextToken[1] === "Text") {
-                    char = text.substring(nextToken[2], nextToken[2] + 1);
-                } else if (nextToken && nextToken[1] === "\\" && nextToken[5] == "control-symbol") {
-                    char = nextToken[4];
-                } else if (nextToken && nextToken[1] === "\\") {
-                    char = "unknown";
-                }
-                if (char === "" || (char !== "unknown" && "(){}[]<>|.".indexOf(char) === -1)) {
-                    TokenError(token, "invalid bracket command");
-                } else {
-                    i = i + 1;
-                    Environments.push({command:seq, token:token});
-                };
-            } else if (seq === "(" || seq === ")" || seq === "[" || seq === "]") {
-                Environments.push({command:seq, token:token});
-            } else if (seq === "input") {
-                newPos = read1filename(TokeniseResult, i);
-                if (newPos === null) { continue; } else {i = newPos;};
-            } else if (seq === "hbox" || seq === "text" || seq === "mbox") {
-                nextGroupMathMode = false;
-            } else if (typeof seq === "string" && seq.match(/^(alpha|beta|gamma|delta|epsilon|varepsilon|zeta|eta|theta|vartheta|iota|kappa|lambda|mu|nu|xi|pi|varpi|rho|varrho|sigma|varsigma|tau|upsilon|phi|varphi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)$/)) {
-                var currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
-                if (currentMathMode === null && !insideGroup) {
-                    TokenError(token, type + seq + " must be inside math mode");
-                };
-            } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection|cite|ref)/)) {
-                currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
-                if (currentMathMode && !insideGroup) {
-                    TokenError(token, type + seq + " used inside math mode");
-                    Environments.resetMathMode();
-                };
-            };
+            }
         } else if (type === "{") {
-            Environments.push({command:"{", token:token, mathMode: nextGroupMathMode});
-            nextGroupMathMode = null;
+            Environments.push({command:"{", token:token});
         } else if (type === "}") {
             Environments.push({command:"}", token:token});
-        } else if (type === "$") {
-            var lookAhead = Tokens[i+1];
-            var nextIsDollar = lookAhead && lookAhead[1] === "$";
-            currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
-            if (nextIsDollar && (!currentMathMode || currentMathMode.command == "$$")) {
-                Environments.push({command:"$$", token:token});
-                i = i + 1;
-            } else {
-                Environments.push({command:"$", token:token});
-            }
-        } else if (type === "^" || type === "_") {
-            currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
-            var insideGroup = Environments.insideGroup();  // true if inside {....}
-            if (currentMathMode === null && !insideGroup) {
-                TokenError(token, type + " must be inside math mode");
-            };
-        } else {
-            nextGroupMathMode = null;
-        }
+        };
     };
     return Environments;
 };
 
-var EnvHandler = function (ErrorReporter) {
+
+var CheckEnvironments = function (Environments, ErrorReporter) {
     var ErrorTo = ErrorReporter.EnvErrorTo;
     var ErrorFromTo = ErrorReporter.EnvErrorFromTo;
     var ErrorFrom = ErrorReporter.EnvErrorFrom;
 
-    var envs = [];
-
     var state = [];
     var documentClosed = null;
     var inVerbatim = false;
     var verbatimRanges = [];
-
-    this.Environments = envs;
-
-    this.push = function (newEnv) {
-        this.setEnvProps(newEnv);
-        this.checkAndUpdateState(newEnv);
-        envs.push(newEnv);
-    };
-
-    this._endVerbatim = function (thisEnv) {
-        var lastEnv = state.pop();
-        if (lastEnv && lastEnv.name === thisEnv.name) {
-            inVerbatim = false;
-            verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]});
-        } else {
-            if(lastEnv) { state.push(lastEnv); } ;
+    for (var i = 0, len = Environments.length; i < len; i++) {
+        var name = Environments[i].name ;
+        if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted)$/)) {
+            Environments[i].verbatim = true;
         }
-    };
-
-    var invalidEnvs = [];
-
-    this._end = function (thisEnv) {
-        do {
+    }
+    for (i = 0, len = Environments.length; i < len; i++) {
+        var thisEnv = Environments[i];
+        if(thisEnv.command === "begin" || thisEnv.command === "{") {
+            if (inVerbatim) { continue; } // ignore anything in verbatim environments
+            if (thisEnv.verbatim) {inVerbatim = true;};
+            state.push(thisEnv);
+        } else if (thisEnv.command === "end" || thisEnv.command === "}") {
             var lastEnv = state.pop();
-            var retry = false;
-            var i;
 
-            if (closedBy(lastEnv, thisEnv)) {
-                if (thisEnv.command === "end" && thisEnv.name === "document" && !documentClosed) {
+            if (inVerbatim) {
+                if (lastEnv && lastEnv.name === thisEnv.name) {
+                    inVerbatim = false;
+                    verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]});
+                    continue;
+                } else {
+                    if(lastEnv) { state.push(lastEnv); } ;
+                    continue;  // ignore all other commands
+                }
+            };
+
+            if (lastEnv && lastEnv.command === "{" && thisEnv.command === "}") {
+                continue;
+            } else if (lastEnv && lastEnv.name === thisEnv.name) {
+                if (thisEnv.name === "document" && !documentClosed) {
                     documentClosed = thisEnv;
                 };
-                return;
+                continue;
             } else if (!lastEnv) {
-                if (documentClosed) {
-                    ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
-                } else {
-                    ErrorTo(thisEnv, "unexpected \\end{" + thisEnv.name + "}");
+                if (thisEnv.command === "}") {
+                    if (documentClosed) {
+                        ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"});
+                    } else {
+                        ErrorTo(thisEnv, "unexpected end group }");
+                    };
+                } else if (thisEnv.command === "end") {
+                    if (documentClosed) {
+                        ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
+                    } else {
+                        ErrorTo(thisEnv, "unexpected \\end{" + thisEnv.name + "}");
+                    }
                 }
-            } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) {
-                invalidEnvs.splice(i, 1);
-                if (lastEnv) { state.push(lastEnv); } ;
-                return;
-            } else {
-                var status = reportError(lastEnv, thisEnv);
-                if (envPrecedence(lastEnv) < envPrecedence(thisEnv)) {
-                    invalidEnvs.push(lastEnv);
-                    retry = true;
-                } else {
-                    var prevLastEnv = state.pop();
-                    if(prevLastEnv) {
-                        if (thisEnv.name === prevLastEnv.name) {
-                            return;
-                        } else {
-                            state.push(prevLastEnv);
-                        }
+            } else if (lastEnv.command === "begin" && thisEnv.command === "}") {
+                ErrorFromTo(lastEnv, thisEnv, "unexpected end group } after \\begin{" + lastEnv.name +"}");
+                state.push(lastEnv);
+            } else if (lastEnv.command === "{" && thisEnv.command === "end") {
+                ErrorFromTo(lastEnv, thisEnv,
+                            "unclosed group { found at \\end{" + thisEnv.name + "}",
+                            {suppressIfEditing:true, errorAtStart: true, type:"warning"});
+                i--;
+            } else if (lastEnv.command === "begin" && thisEnv.command === "end") {
+                ErrorFromTo(lastEnv, thisEnv,
+                            "unclosed \\begin{" + lastEnv.name + "} found at \\end{" + thisEnv.name + "} " ,
+                            {errorAtStart: true});
+                for (var j = i + 1; j < len; j++) {
+                    var futureEnv = Environments[j];
+                    if (futureEnv.command === "end" && futureEnv.name === lastEnv.name) {
+                        state.push(lastEnv);
+                        continue;
+                    }
+                }
+                lastEnv = state.pop();
+                if(lastEnv) {
+                    if (thisEnv.name === lastEnv.name) {
+                        continue;
+                    } else {
+                        state.push(lastEnv);
                     }
-                    invalidEnvs.push(lastEnv);
                 }
 
             }
-        } while (retry === true);
-    };
-
-    var CLOSING_DELIMITER = {
-        "{" : "}",
-        "left" : "right",
-        "[" : "]",
-        "(" : ")",
-        "$" : "$",
-        "$$": "$$"
-    };
-
-    var closedBy = function (lastEnv, thisEnv) {
-        if (!lastEnv) {
-            return false ;
-        } else if (thisEnv.command === "end") {
-            return lastEnv.command === "begin" && lastEnv.name === thisEnv.name;
-        } else if (thisEnv.command === CLOSING_DELIMITER[lastEnv.command]) {
-            return true;
-        } else {
-            return false;
-        }
-    };
-
-    var indexOfClosingEnvInArray = function (envs, thisEnv) {
-        for (var i = 0, n = envs.length; i < n ; i++) {
-            if (closedBy(envs[i], thisEnv)) {
-                return i;
-            }
-        }
-        return -1;
-    };
-
-    var envPrecedence = function (env) {
-        var openScore = {
-            "{" : 1,
-            "left" : 2,
-            "$" : 3,
-            "$$" : 4,
-            "begin": 4
-        };
-        var closeScore = {
-            "}" : 1,
-            "right" : 2,
-            "$" : 3,
-            "$$" : 5,
-            "end": 4
-        };
-        if (env.command) {
-            return openScore[env.command] || closeScore[env.command];
-        } else {
-            return 0;
-        }
-    };
-
-    var getName = function(env) {
-        var description = {
-            "{" : "open group {",
-            "}" : "close group }",
-            "[" : "open display math \\[",
-            "]" : "close display math \\]",
-            "(" : "open inline math \\(",
-            ")" : "close inline math \\)",
-            "$" : "$",
-            "$$" : "$$",
-            "left" : "\\left",
-            "right" : "\\right"
-        };
-        if (env.command === "begin" || env.command === "end") {
-            return "\\" + env.command + "{" + env.name + "}";
-        } else if (env.command in description) {
-            return description[env.command];
-        } else {
-            return env.command;
-        }
-    };
-
-    var EXTRA_CLOSE = 1;
-    var UNCLOSED_GROUP = 2;
-    var UNCLOSED_ENV = 3;
-
-    var reportError = function(lastEnv, thisEnv) {
-        if (!lastEnv) { // unexpected close, nothing was open!
-            if (documentClosed) {
-                ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected end group }",{errorAtStart: true, type: "info"});
-            } else {
-                ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
-            };
-            return EXTRA_CLOSE;
-        } else if (lastEnv.command === "{" && thisEnv.command === "end") {
-            ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv),
-                        {suppressIfEditing:true, errorAtStart: true, type:"warning"});
-            return UNCLOSED_GROUP;
-        } else {
-            var pLast = envPrecedence(lastEnv);
-            var pThis = envPrecedence(thisEnv);
-            if (pThis > pLast) {
-                ErrorFromTo(lastEnv, thisEnv, "unclosed " + getName(lastEnv) + " found at " + getName(thisEnv),
-                           {suppressIfEditing:true, errorAtStart: true});
-            } else {
-                ErrorFromTo(lastEnv, thisEnv, "unexpected " + getName(thisEnv) + " after " + getName(lastEnv));
-            }
-            return UNCLOSED_ENV;
-        };
-    };
-
-    this._beginMathMode = function (thisEnv) {
-        var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env
-        if (currentMathMode) {
-            ErrorFrom(thisEnv, thisEnv.name + " used inside existing math mode " + getName(currentMathMode),
-                      {suppressIfEditing:true, errorAtStart: true});
-        };
-        thisEnv.mathMode = thisEnv;
-        state.push(thisEnv);
-    };
-
-    this._toggleMathMode = function (thisEnv) {
-        var lastEnv = state.pop();
-        if (closedBy(lastEnv, thisEnv)) {
-            return;
-        } else {
-            if (lastEnv) {state.push(lastEnv);}
-            if (lastEnv && lastEnv.mathMode) {
-                this._end(thisEnv);
-            } else {
-                thisEnv.mathMode = thisEnv;
-                state.push(thisEnv);
-            }
-        };
-    };
-
-    this.getMathMode = function () {
-        var n = state.length;
-        if (n > 0) {
-            return state[n-1].mathMode;
-        } else {
-            return null;
-        }
-    };
-
-    this.insideGroup = function () {
-        var n = state.length;
-        if (n > 0) {
-            return (state[n-1].command === "{");
-        } else {
-            return null;
         }
-    };
-
-    var resetMathMode = function () {
-        var n = state.length;
-        if (n > 0) {
-            var lastMathMode = state[n-1].mathMode;
-            do {
-                var lastEnv = state.pop();
-            } while (lastEnv && lastEnv !== lastMathMode);
-        } else {
-            return;
-        }
-    };
-
-    this.resetMathMode = resetMathMode;
-
-    var getNewMathMode = function (currentMathMode, thisEnv) {
-        var newMathMode = null;
-
+    }
+    while (state.length > 0) {
+        thisEnv = state.pop();
         if (thisEnv.command === "{") {
-            if (thisEnv.mathMode !== null) {
-                newMathMode = thisEnv.mathMode;
-            } else {
-                newMathMode = currentMathMode;
-            }
-        } else if (thisEnv.command === "left") {
-            if (currentMathMode === null) {
-                ErrorFrom(thisEnv, "\\left can only be used in math mode");
-            };
-            newMathMode = currentMathMode;
+            ErrorFrom(thisEnv, "unclosed group {", {type:"warning"});
         } else if (thisEnv.command === "begin") {
-            var name = thisEnv.name;
-            if (name) {
-                if (name.match(/^(document|figure|center|tabular|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) {
-                    if (currentMathMode) {
-                        ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
-                                    {suppressIfEditing:true, errorAtStart: true});
-                        resetMathMode();
-                    };
-                    newMathMode = null;
-                } else if (name.match(/^(array|gathered|split|aligned|alignedat)/)) {
-                    if (!currentMathMode) {
-                        ErrorFrom(thisEnv, thisEnv.name + " not inside math mode");
-                    };
-                    newMathMode = currentMathMode;
-                } else if (name.match(/^(math|displaymath|equation|eqnarray|multline|align|gather|flalign|alignat)\*?$/)) {
-                    if (currentMathMode) {
-                        ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
-                                    {suppressIfEditing:true, errorAtStart: true});
-                        resetMathMode();
-                    };
-                    newMathMode = thisEnv;
-                } else {
-                    newMathMode = undefined;  // undefined means we don't know if we are in math mode or not
-                }
-            }
+            ErrorFrom(thisEnv, "unclosed environment \\begin{" + thisEnv.name + "}");
         };
-        return newMathMode;
-    };
-
-    this.checkAndUpdateState = function (thisEnv) {
-        if (inVerbatim) {
-            if (thisEnv.command === "end") {
-                this._endVerbatim(thisEnv);
-            } else {
-                return; // ignore anything in verbatim environments
-            }
-        } else if(thisEnv.command === "begin" || thisEnv.command === "{" || thisEnv.command === "left") {
-            if (thisEnv.verbatim) {inVerbatim = true;};
-            var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env
-            var newMathMode = getNewMathMode(currentMathMode, thisEnv);
-            thisEnv.mathMode = newMathMode;
-            state.push(thisEnv);
-        } else if (thisEnv.command === "end") {
-            this._end(thisEnv);
-        } else if (thisEnv.command === "(" || thisEnv.command === "[") {
-            this._beginMathMode(thisEnv);
-        } else if (thisEnv.command === ")" || thisEnv.command === "]") {
-            this._end(thisEnv);
-        } else if (thisEnv.command === "}") {
-            this._end(thisEnv);
-        } else if (thisEnv.command === "right") {
-            this._end(thisEnv);
-        } else if (thisEnv.command === "$" || thisEnv.command === "$$") {
-            this._toggleMathMode(thisEnv);
-        }
-    };
-
-    this.close = function () {
-        while (state.length > 0) {
-            var thisEnv = state.pop();
-            if (thisEnv.command === "{") {
-                ErrorFrom(thisEnv, "unclosed group {", {type:"warning"});
-            } else {
-                ErrorFrom(thisEnv, "unclosed " + getName(thisEnv));
-            }
-        }
-        var vlen = verbatimRanges.length;
-        var len = ErrorReporter.tokenErrors.length;
-        if (vlen >0 && len > 0) {
-            for (var i = 0; i < len; i++) {
-                var tokenError = ErrorReporter.tokenErrors[i];
-                var startPos = tokenError.startPos;
-                var endPos = tokenError.endPos;
-                for (var j = 0; j < vlen; j++) {
-                    if (startPos > verbatimRanges[j].start && startPos < verbatimRanges[j].end) {
-                        tokenError.ignore = true;
-                        break;
-                    }
+    }
+    var vlen = verbatimRanges.length;
+    len = ErrorReporter.tokenErrors.length;
+    if (vlen >0 && len > 0) {
+        for (i = 0; i < len; i++) {
+            var tokenError = ErrorReporter.tokenErrors[i];
+            var startPos = tokenError.startPos;
+            var endPos = tokenError.endPos;
+            for (j = 0; j < vlen; j++) {
+                if (startPos > verbatimRanges[j].start && startPos < verbatimRanges[j].end) {
+                    tokenError.ignore = true;
+                    break;
                 }
             }
         }
-    };
+    }
 
-    this.setEnvProps = function (env) {
-        var name = env.name ;
-        if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted|Verbatim)$/)) {
-            env.verbatim = true;
-        }
-    };
 };
 var ErrorReporter = function (TokeniseResult) {
     var text = TokeniseResult.text;
@@ -2229,11 +1931,9 @@ var ErrorReporter = function (TokeniseResult) {
         return returnedErrors.concat(errors);
     };
 
-    this.TokenError = function (token, message, options) {
-        if(!options) { options = { suppressIfEditing:true } ; };
+    this.TokenError = function (token, message) {
         var line = token[0], type = token[1], start = token[2], end = token[3];
         var start_col = start - linePosition[line];
-        if (!end) { end = start + 1; } ;
         var end_col = end - linePosition[line];
         tokenErrors.push({row: line,
                           column: start_col,
@@ -2245,11 +1945,10 @@ var ErrorReporter = function (TokeniseResult) {
                           text:message,
                           startPos: start,
                           endPos: end,
-                          suppressIfEditing:options.suppressIfEditing});
+                          suppressIfEditing:true});
     };
 
-    this.TokenErrorFromTo = function (fromToken, toToken, message, options) {
-        if(!options) { options = {suppressIfEditing:true } ; };
+    this.TokenErrorFromTo = function (fromToken, toToken, message) {
         var fromLine = fromToken[0], fromStart = fromToken[2], fromEnd = fromToken[3];
         var toLine = toToken[0], toStart = toToken[2], toEnd = toToken[3];
         if (!toEnd) { toEnd = toStart + 1;};
@@ -2266,7 +1965,7 @@ var ErrorReporter = function (TokeniseResult) {
                           text:message,
                           startPos: fromStart,
                           endPos: toEnd,
-                          suppressIfEditing:options.suppressIfEditing});
+                          suppressIfEditing:true});
     };
 
 
@@ -2328,7 +2027,7 @@ var Parse = function (text) {
     var TokeniseResult = Tokenise(text);
     var Reporter = new ErrorReporter(TokeniseResult);
     var Environments = InterpretTokens(TokeniseResult, Reporter);
-    Environments.close();
+    CheckEnvironments(Environments, Reporter);
     return Reporter.getErrors();
 };
 

+ 188 - 33
services/web/public/js/ace-1.2.5/worker-latex_beta.js

@@ -1554,6 +1554,25 @@ var read1arg = function (TokeniseResult, k, options) {
     }
 };
 
+var readLetDefinition = function (TokeniseResult, k) {
+
+    var Tokens = TokeniseResult.tokens;
+    var text = TokeniseResult.text;
+
+    var first = Tokens[k+1];
+    var second = Tokens[k+2];
+    var third = Tokens[k+3];
+
+    if(first && first[1] === "\\" && second && second[1] === "\\") {
+        return k + 2;
+    } else if(first && first[1] === "\\" &&
+              second && second[1] === "Text" && text.substring(second[2], second[3]) === "=" &&
+              third && third[1] === "\\") {
+        return k + 3;
+    } else {
+        return null;
+    }
+};
 
 var read1name = function (TokeniseResult, k) {
     var Tokens = TokeniseResult.tokens;
@@ -1624,9 +1643,56 @@ var readOptionalParams = function(TokeniseResult, k) {
             return k + 1; // got it
         };
     };
+    var count = 0;
+    var nextToken = Tokens[k+1];
+    var pos = nextToken[2];
+
+    for (var i = pos, end = text.length; i < end; i++) {
+        var char = text[i];
+        if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
+        if (char === "[") { count++; }
+        if (char === "]") { count--; }
+        if (count === 0 && char === "{") { return k - 1; }
+        if (count > 0 && (char  === '\r' || char === '\n')) { return null; }
+    };
+    return null;
+};
+
+var readOptionalGeneric = function(TokeniseResult, k) {
+    var Tokens = TokeniseResult.tokens;
+    var text = TokeniseResult.text;
+
+    var params = Tokens[k+1];
+
+    if(params && params[1] === "Text") {
+        var paramNum = text.substring(params[2], params[3]);
+        if (paramNum.match(/^(\[[^\]]*\])+\s*$/)) {
+            return k + 1; // got it
+        };
+    };
     return null;
 };
 
+var readOptionalDef = function (TokeniseResult, k) {
+    var Tokens = TokeniseResult.tokens;
+    var text = TokeniseResult.text;
+
+    var defToken = Tokens[k];
+    var pos = defToken[3];
+
+    var openBrace = "{";
+    var nextToken = Tokens[k+1];
+    for (var i = pos, end = text.length; i < end; i++) {
+        var char = text[i];
+        if (nextToken && i >= nextToken[2]) { k++; nextToken = Tokens[k+1];};
+        if (char === openBrace) { return k - 1; }; // move back to the last token of the optional arguments
+        if (char  === '\r' || char === '\n') { return null; }
+    };
+
+    return null;
+
+};
+
 var readDefinition = function(TokeniseResult, k) {
     var Tokens = TokeniseResult.tokens;
     var text = TokeniseResult.text;
@@ -1726,10 +1792,27 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
     var Environments = new EnvHandler(ErrorReporter);
 
     var nextGroupMathMode = null; // if the next group should have math mode on or off (for \hbox)
+    var nextGroupMathModeStack = [] ; // tracking all nextGroupMathModes
+    var seenUserDefinedBeginEquation = false; // if we have seen macros like \beq
+    var seenUserDefinedEndEquation = false; // if we have seen macros like \eeq
 
     for (var i = 0, len = Tokens.length; i < len; i++) {
         var token = Tokens[i];
         var line = token[0], type = token[1], start = token[2], end = token[3], seq = token[4];
+
+        if (type === "{") {
+            Environments.push({command:"{", token:token, mathMode: nextGroupMathMode});
+            nextGroupMathModeStack.push(nextGroupMathMode);
+            nextGroupMathMode = null;
+            continue;
+        } else if (type === "}") {
+            Environments.push({command:"}", token:token});
+            nextGroupMathMode = nextGroupMathModeStack.pop();
+            continue;
+        } else {
+            nextGroupMathMode = null;
+        };
+
         if (type === "\\") {
             if (seq === "begin" || seq === "end") {
                 var open = Tokens[i+1];
@@ -1778,15 +1861,31 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
                     } else {
                         TokenError(token, "invalid environment command");
                     };
-                    }
-            } else if (seq === "newcommand" || seq === "renewcommand" || seq === "def" || seq === "DeclareRobustCommand") {
-                var newPos = read1arg(TokeniseResult, i, {allowStar: (seq != "def")});
+                }
+            } else if (typeof seq === "string" && seq.match(/^(be|beq|beqa|bea)$/i)) {
+                seenUserDefinedBeginEquation = true;
+            } else if (typeof seq === "string" && seq.match(/^(ee|eeq|eeqn|eeqa|eeqan|eea)$/i)) {
+                seenUserDefinedEndEquation = true;
+            } else if (seq === "newcommand" || seq === "renewcommand" || seq === "DeclareRobustCommand") {
+                var newPos = read1arg(TokeniseResult, i, {allowStar: true});
                 if (newPos === null) { continue; } else {i = newPos;};
                 newPos = readOptionalParams(TokeniseResult, i);
                 if (newPos === null) { /* do nothing */ } else {i = newPos;};
                 newPos = readDefinition(TokeniseResult, i);
                 if (newPos === null) { /* do nothing */ } else {i = newPos;};
 
+            } else if (seq === "def") {
+                newPos = read1arg(TokeniseResult, i);
+                if (newPos === null) { continue; } else {i = newPos;};
+                newPos = readOptionalDef(TokeniseResult, i);
+                if (newPos === null) { /* do nothing */ } else {i = newPos;};
+                newPos = readDefinition(TokeniseResult, i);
+                if (newPos === null) { /* do nothing */ } else {i = newPos;};
+
+            } else if (seq === "let") {
+                newPos = readLetDefinition(TokeniseResult, i);
+                if (newPos === null) { continue; } else {i = newPos;};
+
             } else if (seq === "newcolumntype") {
                 newPos = read1name(TokeniseResult, i);
                 if (newPos === null) { continue; } else {i = newPos;};
@@ -1820,7 +1919,7 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
                 } else if (nextToken && nextToken[1] === "\\") {
                     char = "unknown";
                 }
-                if (char === "" || (char !== "unknown" && "(){}[]<>|.".indexOf(char) === -1)) {
+                if (char === "" || (char !== "unknown" && "(){}[]<>/|\\.".indexOf(char) === -1)) {
                     TokenError(token, "invalid bracket command");
                 } else {
                     i = i + 1;
@@ -1831,25 +1930,50 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
             } else if (seq === "input") {
                 newPos = read1filename(TokeniseResult, i);
                 if (newPos === null) { continue; } else {i = newPos;};
-            } else if (seq === "hbox" || seq === "text" || seq === "mbox") {
+            } else if (seq === "hbox" || seq === "text" || seq === "mbox" || seq === "footnote" || seq === "intertext" || seq === "shortintertext" || seq === "textnormal" || seq === "tag" || seq === "reflectbox" || seq === "textrm") {
                 nextGroupMathMode = false;
+            } else if (seq === "rotatebox" || seq === "scalebox") {
+                newPos = readOptionalGeneric(TokeniseResult, i);
+                if (newPos === null) { /* do nothing */ } else {i = newPos;};
+                newPos = readDefinition(TokeniseResult, i);
+                if (newPos === null) { /* do nothing */ } else {i = newPos;};
+                nextGroupMathMode = false;
+            } else if (seq === "resizebox") {
+                newPos = readOptionalGeneric(TokeniseResult, i);
+                if (newPos === null) { /* do nothing */ } else {i = newPos;};
+                newPos = readDefinition(TokeniseResult, i);
+                if (newPos === null) { /* do nothing */ } else {i = newPos;};
+                newPos = readDefinition(TokeniseResult, i);
+                if (newPos === null) { /* do nothing */ } else {i = newPos;};
+
+                nextGroupMathMode = false;
+            } else if (seq === "DeclareMathOperator") {
+                newPos = readDefinition(TokeniseResult, i);
+                if (newPos === null) { /* do nothing */ } else {i = newPos;};
+                newPos = readDefinition(TokeniseResult, i);
+                if (newPos === null) { /* do nothing */ } else {i = newPos;};
+            } else if (seq === "DeclarePairedDelimiter") {
+                newPos = readDefinition(TokeniseResult, i);
+                if (newPos === null) { /* do nothing */ } else {i = newPos;};
+                newPos = readDefinition(TokeniseResult, i);
+                if (newPos === null) { /* do nothing */ } else {i = newPos;};
+                newPos = readDefinition(TokeniseResult, i);
+                if (newPos === null) { /* do nothing */ } else {i = newPos;};
             } else if (typeof seq === "string" && seq.match(/^(alpha|beta|gamma|delta|epsilon|varepsilon|zeta|eta|theta|vartheta|iota|kappa|lambda|mu|nu|xi|pi|varpi|rho|varrho|sigma|varsigma|tau|upsilon|phi|varphi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)$/)) {
                 var currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
-                if (currentMathMode === null && !insideGroup) {
-                    TokenError(token, type + seq + " must be inside math mode");
+                if (currentMathMode === null) {
+                    TokenError(token, type + seq + " must be inside math mode", {mathMode:true});
                 };
-            } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection|cite|ref)/)) {
+            } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection)$/)) {
                 currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
-                if (currentMathMode && !insideGroup) {
-                    TokenError(token, type + seq + " used inside math mode");
+                if (currentMathMode) {
+                    TokenError(token, type + seq + " used inside math mode", {mathMode:true});
                     Environments.resetMathMode();
                 };
+            } else if (typeof seq === "string" && seq.match(/^[a-z]+$/)) {
+                nextGroupMathMode = undefined;
             };
-        } else if (type === "{") {
-            Environments.push({command:"{", token:token, mathMode: nextGroupMathMode});
-            nextGroupMathMode = null;
-        } else if (type === "}") {
-            Environments.push({command:"}", token:token});
+
         } else if (type === "$") {
             var lookAhead = Tokens[i+1];
             var nextIsDollar = lookAhead && lookAhead[1] === "$";
@@ -1864,12 +1988,15 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
             currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
             var insideGroup = Environments.insideGroup();  // true if inside {....}
             if (currentMathMode === null && !insideGroup) {
-                TokenError(token, type + " must be inside math mode");
+                TokenError(token, type + " must be inside math mode", {mathMode:true});
             };
-        } else {
-            nextGroupMathMode = null;
         }
     };
+
+    if (seenUserDefinedBeginEquation && seenUserDefinedEndEquation) {
+        ErrorReporter.filterMath = true;
+    };
+
     return Environments;
 };
 
@@ -1920,7 +2047,7 @@ var EnvHandler = function (ErrorReporter) {
                 if (documentClosed) {
                     ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
                 } else {
-                    ErrorTo(thisEnv, "unexpected \\end{" + thisEnv.name + "}");
+                    ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
                 }
             } else if (invalidEnvs.length > 0 && (i = indexOfClosingEnvInArray(invalidEnvs, thisEnv) > -1)) {
                 invalidEnvs.splice(i, 1);
@@ -2054,7 +2181,7 @@ var EnvHandler = function (ErrorReporter) {
         var currentMathMode = this.getMathMode(); // undefined, null, $, $$, name of mathmode env
         if (currentMathMode) {
             ErrorFrom(thisEnv, thisEnv.name + " used inside existing math mode " + getName(currentMathMode),
-                      {suppressIfEditing:true, errorAtStart: true});
+                      {suppressIfEditing:true, errorAtStart: true, mathMode:true});
         };
         thisEnv.mathMode = thisEnv;
         state.push(thisEnv);
@@ -2118,28 +2245,28 @@ var EnvHandler = function (ErrorReporter) {
             }
         } else if (thisEnv.command === "left") {
             if (currentMathMode === null) {
-                ErrorFrom(thisEnv, "\\left can only be used in math mode");
+                ErrorFrom(thisEnv, "\\left can only be used in math mode", {mathMode: true});
             };
             newMathMode = currentMathMode;
         } else if (thisEnv.command === "begin") {
             var name = thisEnv.name;
             if (name) {
-                if (name.match(/^(document|figure|center|tabular|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) {
+                if (name.match(/^(document|figure|center|enumerate|itemize|table|abstract|proof|lemma|theorem|definition|proposition|corollary|remark|notation|thebibliography)$/)) {
                     if (currentMathMode) {
                         ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
-                                    {suppressIfEditing:true, errorAtStart: true});
+                                    {suppressIfEditing:true, errorAtStart: true, mathMode: true});
                         resetMathMode();
                     };
                     newMathMode = null;
-                } else if (name.match(/^(array|gathered|split|aligned|alignedat)/)) {
-                    if (!currentMathMode) {
-                        ErrorFrom(thisEnv, thisEnv.name + " not inside math mode");
+                } else if (name.match(/^(array|gathered|split|aligned|alignedat)\*?$/)) {
+                    if (currentMathMode === null) {
+                        ErrorFrom(thisEnv, thisEnv.name + " not inside math mode", {mathMode: true});
                     };
                     newMathMode = currentMathMode;
                 } else if (name.match(/^(math|displaymath|equation|eqnarray|multline|align|gather|flalign|alignat)\*?$/)) {
                     if (currentMathMode) {
                         ErrorFromTo(currentMathMode, thisEnv, thisEnv.name + " used inside " + getName(currentMathMode),
-                                    {suppressIfEditing:true, errorAtStart: true});
+                                    {suppressIfEditing:true, errorAtStart: true, mathMode: true});
                         resetMathMode();
                     };
                     newMathMode = thisEnv;
@@ -2220,13 +2347,36 @@ var ErrorReporter = function (TokeniseResult) {
     var errors = [], tokenErrors = [];
     this.errors = errors;
     this.tokenErrors = tokenErrors;
+    this.filterMath = false;
 
     this.getErrors = function () {
         var returnedErrors = [];
         for (var i = 0, len = tokenErrors.length; i < len; i++) {
             if (!tokenErrors[i].ignore) { returnedErrors.push(tokenErrors[i]); }
         }
-        return returnedErrors.concat(errors);
+        var allErrors = returnedErrors.concat(errors);
+        var result = [];
+
+        var mathErrorCount = 0;
+        for (i = 0, len = allErrors.length; i < len; i++) {
+            if (allErrors[i].mathMode) {
+                mathErrorCount++;
+            }
+            if (mathErrorCount > 10) {
+                return [];
+            }
+        }
+
+        if (this.filterMath && mathErrorCount > 0) {
+            for (i = 0, len = allErrors.length; i < len; i++) {
+                if (!allErrors[i].mathMode) {
+                    result.push(allErrors[i]);
+                }
+            }
+            return result;
+        } else {
+            return allErrors;
+        }
     };
 
     this.TokenError = function (token, message, options) {
@@ -2245,7 +2395,8 @@ var ErrorReporter = function (TokeniseResult) {
                           text:message,
                           startPos: start,
                           endPos: end,
-                          suppressIfEditing:options.suppressIfEditing});
+                          suppressIfEditing:options.suppressIfEditing,
+                          mathMode: options.mathMode});
     };
 
     this.TokenErrorFromTo = function (fromToken, toToken, message, options) {
@@ -2266,7 +2417,8 @@ var ErrorReporter = function (TokeniseResult) {
                           text:message,
                           startPos: fromStart,
                           endPos: toEnd,
-                          suppressIfEditing:options.suppressIfEditing});
+                          suppressIfEditing:options.suppressIfEditing,
+                          mathMode: options.mathMode});
     };
 
 
@@ -2287,7 +2439,8 @@ var ErrorReporter = function (TokeniseResult) {
                      end_col: end_col,
                      type: options.type ? options.type : "error",
                      text:message,
-                     suppressIfEditing:options.suppressIfEditing});
+                     suppressIfEditing:options.suppressIfEditing,
+                     mathMode: options.mathMode});
     };
 
     this.EnvErrorTo = function (toEnv, message, options) {
@@ -2303,7 +2456,8 @@ var ErrorReporter = function (TokeniseResult) {
                    end_row: line,
                    end_col: end_col,
                    type: options.type ? options.type : "error",
-                   text:message};
+                   text:message,
+                   mathMode: options.mathMode};
         errors.push(err);
     };
 
@@ -2320,7 +2474,8 @@ var ErrorReporter = function (TokeniseResult) {
                      end_row: lineNumber,
                      end_col: end_col,
                      type: options.type ? options.type : "error",
-                     text:message});
+                     text:message,
+                     mathMode: options.mathMode});
     };
 };
 

+ 7 - 1
services/web/public/stylesheets/app/editor/share.less

@@ -47,4 +47,10 @@
 			}
 		}
 	}
-}
+}
+.modal-footer-share {
+	.modal-footer-left {
+		max-width: 70%;
+		text-align: left;
+	}
+}

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

@@ -387,6 +387,10 @@ describe "AuthenticationController", ->
 		beforeEach ->
 			@req.headers = {}
 			@AuthenticationController.httpAuth = sinon.stub()
+			@_setRedirect = sinon.spy(@AuthenticationController, '_setRedirectInSession')
+
+		afterEach ->
+			@_setRedirect.restore()
 
 		describe "with white listed url", ->
 			beforeEach ->
@@ -431,6 +435,9 @@ describe "AuthenticationController", ->
 				@req.session = {}
 				@AuthenticationController.requireGlobalLogin @req, @res, @next
 
+			it 'should have called setRedirectInSession', ->
+				@_setRedirect.callCount.should.equal 1
+
 			it "should redirect to the /login page", ->
 				@res.redirectedTo.should.equal "/login"
 

+ 158 - 0
services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee

@@ -27,6 +27,7 @@ describe "CollaboratorsInviteController", ->
 			"../Notifications/NotificationsBuilder": @NotificationsBuilder = {}
 			"../Analytics/AnalyticsManager": @AnalyticsManger
 			'../Authentication/AuthenticationController': @AuthenticationController
+			'settings-sharelatex': @settings = {}
 		@res = new MockResponse()
 		@req = new MockRequest()
 
@@ -103,9 +104,15 @@ describe "CollaboratorsInviteController", ->
 		describe 'when all goes well', ->
 
 			beforeEach ->
+				@_checkShouldInviteEmail = sinon.stub(
+					@CollaboratorsInviteController, '_checkShouldInviteEmail'
+				).callsArgWith(1, null, true)
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 
+			afterEach ->
+				@_checkShouldInviteEmail.restore()
+
 			it 'should produce json response', ->
 				@res.json.callCount.should.equal 1
 				({invite: @invite}).should.deep.equal(@res.json.firstCall.args[0])
@@ -114,6 +121,10 @@ describe "CollaboratorsInviteController", ->
 				@LimitationsManager.canAddXCollaborators.callCount.should.equal 1
 				@LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
 
+			it 'should have called _checkShouldInviteEmail', ->
+				@_checkShouldInviteEmail.callCount.should.equal 1
+				@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
+
 			it 'should have called inviteToProject', ->
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
 				@CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true
@@ -125,37 +136,63 @@ describe "CollaboratorsInviteController", ->
 		describe 'when the user is not allowed to add more collaborators', ->
 
 			beforeEach ->
+				@_checkShouldInviteEmail = sinon.stub(
+					@CollaboratorsInviteController, '_checkShouldInviteEmail'
+				).callsArgWith(1, null, true)
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false)
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 
+			afterEach ->
+				@_checkShouldInviteEmail.restore()
+
 			it 'should produce json response without an invite', ->
 				@res.json.callCount.should.equal 1
 				({invite: null}).should.deep.equal(@res.json.firstCall.args[0])
 
+			it 'should not have called _checkShouldInviteEmail', ->
+				@_checkShouldInviteEmail.callCount.should.equal 0
+				@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal false
+
 			it 'should not have called inviteToProject', ->
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
 
 		describe 'when canAddXCollaborators produces an error', ->
 
 			beforeEach ->
+				@_checkShouldInviteEmail = sinon.stub(
+					@CollaboratorsInviteController, '_checkShouldInviteEmail'
+				).callsArgWith(1, null, true)
 				@err = new Error('woops')
 				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err)
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 
+			afterEach ->
+				@_checkShouldInviteEmail.restore()
+
 			it 'should call next with an error', ->
 				@next.callCount.should.equal 1
 				@next.calledWith(@err).should.equal true
 
+			it 'should not have called _checkShouldInviteEmail', ->
+				@_checkShouldInviteEmail.callCount.should.equal 0
+				@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal false
+
 			it 'should not have called inviteToProject', ->
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
 
 		describe 'when inviteToProject produces an error', ->
 
 			beforeEach ->
+				@_checkShouldInviteEmail = sinon.stub(
+					@CollaboratorsInviteController, '_checkShouldInviteEmail'
+				).callsArgWith(1, null, true)
 				@err = new Error('woops')
 				@CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err)
 				@CollaboratorsInviteController.inviteToProject @req, @res, @next
 
+			afterEach ->
+				@_checkShouldInviteEmail.restore()
+
 			it 'should call next with an error', ->
 				@next.callCount.should.equal 1
 				@next.calledWith(@err).should.equal true
@@ -164,10 +201,60 @@ describe "CollaboratorsInviteController", ->
 				@LimitationsManager.canAddXCollaborators.callCount.should.equal 1
 				@LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
 
+			it 'should have called _checkShouldInviteEmail', ->
+				@_checkShouldInviteEmail.callCount.should.equal 1
+				@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
+
 			it 'should have called inviteToProject', ->
 				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
 				@CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true
 
+		describe 'when _checkShouldInviteEmail disallows the invite', ->
+
+			beforeEach ->
+				@_checkShouldInviteEmail = sinon.stub(
+					@CollaboratorsInviteController, '_checkShouldInviteEmail'
+				).callsArgWith(1, null, false)
+				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
+				@CollaboratorsInviteController.inviteToProject @req, @res, @next
+
+			afterEach ->
+				@_checkShouldInviteEmail.restore()
+
+			it 'should produce json response with no invite, and an error property', ->
+				@res.json.callCount.should.equal 1
+				({invite: null, error: 'cannot_invite_non_user'}).should.deep.equal(@res.json.firstCall.args[0])
+
+			it 'should have called _checkShouldInviteEmail', ->
+				@_checkShouldInviteEmail.callCount.should.equal 1
+				@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
+
+			it 'should not have called inviteToProject', ->
+				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
+
+		describe 'when _checkShouldInviteEmail produces an error', ->
+
+			beforeEach ->
+				@_checkShouldInviteEmail = sinon.stub(
+					@CollaboratorsInviteController, '_checkShouldInviteEmail'
+				).callsArgWith(1, new Error('woops'))
+				@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
+				@CollaboratorsInviteController.inviteToProject @req, @res, @next
+
+			afterEach ->
+				@_checkShouldInviteEmail.restore()
+
+			it 'should call next with an error', ->
+				@next.callCount.should.equal 1
+				@next.calledWith(@err).should.equal true
+
+			it 'should have called _checkShouldInviteEmail', ->
+				@_checkShouldInviteEmail.callCount.should.equal 1
+				@_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
+
+			it 'should not have called inviteToProject', ->
+				@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
+
 	describe "viewInvite", ->
 
 		beforeEach ->
@@ -579,3 +666,74 @@ describe "CollaboratorsInviteController", ->
 
 			it 'should have called acceptInvite', ->
 				@CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1
+
+	describe '_checkShouldInviteEmail', ->
+
+		beforeEach ->
+			@email = 'user@example.com'
+			@call = (callback) =>
+				@CollaboratorsInviteController._checkShouldInviteEmail @email, callback
+
+		describe 'when we should be restricting to existing accounts', ->
+
+			beforeEach ->
+				@settings.restrictInvitesToExistingAccounts = true
+
+			describe 'when user account is present', ->
+
+				beforeEach ->
+					@user = {_id: ObjectId().toString()}
+					@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
+
+				it 'should callback with `true`', (done) ->
+					@call (err, shouldAllow) =>
+						expect(err).to.equal null
+						expect(shouldAllow).to.equal true
+						done()
+
+			describe 'when user account is absent', ->
+
+				beforeEach ->
+					@user = null
+					@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
+
+				it 'should callback with `false`', (done) ->
+					@call (err, shouldAllow) =>
+						expect(err).to.equal null
+						expect(shouldAllow).to.equal false
+						done()
+
+				it 'should have called getUser', (done) ->
+					@call (err, shouldAllow) =>
+						@UserGetter.getUser.callCount.should.equal 1
+						@UserGetter.getUser.calledWith({email: @email}, {_id: 1}).should.equal true
+						done()
+
+			describe 'when getUser produces an error', ->
+
+				beforeEach ->
+					@user = null
+					@UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops'))
+
+				it 'should callback with an error', (done) ->
+					@call (err, shouldAllow) =>
+						expect(err).to.not.equal null
+						expect(err).to.be.instanceof Error
+						expect(shouldAllow).to.equal undefined
+						done()
+
+		describe 'when we should not be restricting', ->
+
+			beforeEach ->
+				@settings.restrictInvitesToExistingAccounts = false
+
+			it 'should callback with `true`', (done) ->
+				@call (err, shouldAllow) =>
+					expect(err).to.equal null
+					expect(shouldAllow).to.equal true
+					done()
+
+			it 'should not have called getUser', (done) ->
+				@call (err, shouldAllow) =>
+					@UserGetter.getUser.callCount.should.equal 0
+					done()