From 47a5eb538202994e09f0c66804a6d42b9f022554 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 15 Dec 2016 17:22:39 +0000 Subject: [PATCH 01/17] Add a new layout and body template for e-mails. --- .../Email/Bodies/SingleCTAEmailBody.coffee | 26 ++ .../coffee/Features/Email/EmailBuilder.coffee | 30 +- .../Layouts/BaseWithHeaderEmailLayout.coffee | 378 ++++++++++++++++++ 3 files changed, 420 insertions(+), 14 deletions(-) create mode 100644 services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee create mode 100644 services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee diff --git a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee new file mode 100644 index 0000000000..98a0ffe30d --- /dev/null +++ b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee @@ -0,0 +1,26 @@ +_ = require("underscore") +settings = require "settings-sharelatex" + +module.exports = _.template """ + + +
+
+

+ <%= title %> +

+
 
+

+ <%= greeting %> +

+

+ <%= message %> +

+
 
+
+
+ View project +
+
+
+""" diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 70d11e219b..908aac53f1 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/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") @@ -107,9 +113,9 @@ If you didn't request a password reset, let us know. templates.projectInvite = subject: _.template "<%= project.name %> - shared by <%= owner.email %>" - layout: NotificationEmailLayout + layout: BaseWithHeaderEmailLayout type:"notification" - plainTextTemplate: _.template """ + plainTextTemplate: plainTextTpl: """ Hi, <%= owner.email %> wants to share '<%= project.name %>' with you. Follow this link to view the project: <%= inviteUrl %> @@ -118,18 +124,14 @@ Thank you #{settings.appName} - <%= siteUrl %> """ - compiledTemplate: _.template """ -

Hi, <%= owner.email %> wants to share '<%= project.name %>' with you

-
- - - View Project - - -
-

Thank you

-

#{settings.appName}

-""" + compiledTemplate: (opts) -> + SingleCTAEmailBody({ + title: "#{ opts.project.name } – shared by #{ opts.owner.email }" + greeting: "Hi," + message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” with you." + ctaURL: opts.inviteUrl + }) + templates.completeJoinGroupAccount = diff --git a/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee b/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee new file mode 100644 index 0000000000..7b68d3bf49 --- /dev/null +++ b/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee @@ -0,0 +1,378 @@ +_ = require("underscore") +settings = require "settings-sharelatex" + +module.exports = _.template """ + + + + + + + + + Project invite + + + + + + + + +
+
+ +
+
+ + +
+
+

+ SHARELATEX +

+
+
+
+
 
+
+
 
+ + <%= body %> + +
+
 
+

ShareLaTeX • www.sharelatex.com

+
+
+ +
+
+ +
                                                           
+ + + +""" From 7dfc2c61a6b3137f95e8d1e7d7668778b7122b7e Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 16 Dec 2016 10:06:56 +0000 Subject: [PATCH 02/17] Make the CTA button text configurable. --- .../coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee | 4 +++- services/web/app/coffee/Features/Email/EmailBuilder.coffee | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee index 98a0ffe30d..07954e2162 100644 --- a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee +++ b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee @@ -17,7 +17,9 @@ module.exports = _.template """
 
- View project + + <%= ctaText %> +
diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 908aac53f1..dd908c9d42 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -115,7 +115,7 @@ templates.projectInvite = subject: _.template "<%= project.name %> - shared by <%= owner.email %>" layout: BaseWithHeaderEmailLayout type:"notification" - plainTextTemplate: plainTextTpl: """ + plainTextTemplate: """ Hi, <%= owner.email %> wants to share '<%= project.name %>' with you. Follow this link to view the project: <%= inviteUrl %> @@ -129,6 +129,7 @@ Thank you title: "#{ opts.project.name } – shared by #{ opts.owner.email }" greeting: "Hi," message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” with you." + ctaText: "View project" ctaURL: opts.inviteUrl }) From 2234c438a165d299baa754f456e8036958fc28dc Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 16 Dec 2016 12:16:33 +0000 Subject: [PATCH 03/17] Support a secondary message in the single CTA email template. --- .../coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee index 07954e2162..6d0756596c 100644 --- a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee +++ b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee @@ -22,6 +22,12 @@ module.exports = _.template """ + <% if (secondaryMessage) { %> +
 
+

+ <%= secondaryMessage %> +

+ <% } %> From 0fe3664a820a173de07c05a30a130570475ed4be Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 16 Dec 2016 12:16:45 +0000 Subject: [PATCH 04/17] Use the new template in the reset pwd email. --- .../coffee/Features/Email/EmailBuilder.coffee | 35 ++++++------------- .../Layouts/BaseWithHeaderEmailLayout.coffee | 4 ++- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index dd908c9d42..97a1023e3b 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -84,31 +84,15 @@ Thank you #{settings.appName} - <%= siteUrl %> """ - compiledTemplate: _.template """ -

Password Reset

-

-We got a request to reset your #{settings.appName} password. -

-

- -
- -If you ignore this message, your password won't be changed. -

-If you didn't request a password reset, let us know. - -

-

Thank you

-

#{settings.appName}

-""" + 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.
If you didn't request a password reset, let us know." + ctaText: "Reset password" + ctaURL: opts.setNewPasswordUrl + }) templates.projectInvite = @@ -129,6 +113,7 @@ Thank you title: "#{ opts.project.name } – shared by #{ opts.owner.email }" greeting: "Hi," message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” with you." + secondaryMessage: null ctaText: "View project" ctaURL: opts.inviteUrl }) diff --git a/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee b/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee index 7b68d3bf49..6d25df2197 100644 --- a/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee +++ b/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee @@ -362,7 +362,9 @@ module.exports = _.template """
 
-

ShareLaTeX • www.sharelatex.com

+

+ #{ settings.appName} • #{ settings.siteUrl } +

From 3a8a12fcb32d8e0f96bc80bbe7d67caf64710fbe Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 16 Dec 2016 14:07:47 +0000 Subject: [PATCH 05/17] Revert "rollout linter math mode to all users" This reverts commit 0ac0a11d3028cabac53d1e347b2509cb9e785f9f. --- .../web/public/js/ace-1.2.5/mode-latex.js | 14 +- .../web/public/js/ace-1.2.5/worker-latex.js | 507 ++++-------------- 2 files changed, 107 insertions(+), 414 deletions(-) diff --git a/services/web/public/js/ace-1.2.5/mode-latex.js b/services/web/public/js/ace-1.2.5/mode-latex.js index 8e7bbe4802..f183d7c263 100644 --- a/services/web/public/js/ace-1.2.5/mode-latex.js +++ b/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 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 + "}"); - } - } 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); - } + 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 (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(); }; From ca5040882f5134c436a4460997ab2629cbc2f21d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 16 Dec 2016 14:57:59 +0000 Subject: [PATCH 06/17] Use the new template in the join group email. --- .../coffee/Features/Email/EmailBuilder.coffee | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 97a1023e3b..c083334bed 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -67,7 +67,7 @@ ShareLaTeX Co-founder templates.passwordResetRequested = subject: _.template "Password Reset - #{settings.appName}" - layout: NotificationEmailLayout + layout: BaseWithHeaderEmailLayout type:"notification" plainTextTemplate: _.template """ Password Reset @@ -122,7 +122,7 @@ Thank you 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 @@ -133,23 +133,15 @@ Thank You #{settings.appName} - <%= siteUrl %> """ - compiledTemplate: _.template """ -

Hi, please verify your email to join the <%= group_name %> and get your free premium account

-
- -
-

Thank you

-

#{settings.appName}

-""" - + 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 + }) module.exports = templates: templates @@ -165,4 +157,4 @@ module.exports = html: template.layout(opts) text: template?.plainTextTemplate?(opts) type:template.type - } + } \ No newline at end of file From 869f729132ff170fb2b5c1d3d904cb307ba1aba8 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 16 Dec 2016 15:15:06 +0000 Subject: [PATCH 07/17] Fix forgotten templating function. --- services/web/app/coffee/Features/Email/EmailBuilder.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index c083334bed..0a5f653178 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -99,7 +99,7 @@ templates.projectInvite = subject: _.template "<%= project.name %> - shared by <%= owner.email %>" layout: BaseWithHeaderEmailLayout type:"notification" - plainTextTemplate: """ + plainTextTemplate: _.template """ Hi, <%= owner.email %> wants to share '<%= project.name %>' with you. Follow this link to view the project: <%= inviteUrl %> From d6fcc21ab571267d61827e1f0f6f0c6f0c8e67cb Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 16 Dec 2016 17:04:26 +0000 Subject: [PATCH 08/17] Add support for GMail go-to actions in the single CTA template. --- .../Email/Bodies/SingleCTAEmailBody.coffee | 15 +++++++++++++++ .../app/coffee/Features/Email/EmailBuilder.coffee | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee index 6d0756596c..00a878c276 100644 --- a/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee +++ b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee @@ -31,4 +31,19 @@ module.exports = _.template """ +<% if (gmailGoToAction) { %> + +<% } %> """ diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 0a5f653178..306aad3d2a 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -92,6 +92,7 @@ Thank you secondaryMessage: "If you ignore this message, your password won't be changed.
If you didn't request a password reset, let us know." ctaText: "Reset password" ctaURL: opts.setNewPasswordUrl + gmailGoToAction: null }) @@ -116,6 +117,10 @@ Thank you secondaryMessage: null ctaText: "View project" ctaURL: opts.inviteUrl + gmailGoToAction: + target: opts.inviteUrl + name: "View project" + description: "Join #{ opts.project.name } at ShareLaTeX" }) @@ -141,6 +146,7 @@ Thank You secondaryMessage: null ctaText: "Verify now" ctaURL: opts.completeJoinUrl + gmailGoToAction: null }) module.exports = From 259c589076a6b2dc736f1f118071e3313b45ff34 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 20 Dec 2016 09:54:42 +0000 Subject: [PATCH 09/17] Add option to restrict invites to existing user accounts. --- .../CollaboratorsInviteController.coffee | 28 +++- .../web/app/views/project/editor/share.jade | 9 +- services/web/config/settings.defaults.coffee | 4 + .../ShareProjectModalController.coffee | 28 ++-- .../public/stylesheets/app/editor/share.less | 8 +- .../CollaboratorsInviteControllerTests.coffee | 158 ++++++++++++++++++ 6 files changed, 217 insertions(+), 18 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index b88ded33b2..9d9f4d2a5e 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/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 diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index fd13ccb240..62de414064 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/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")} diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index e3b842a0d2..ccfec59235 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -276,6 +276,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 diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 2d27f96970..6ab15de766 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/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 diff --git a/services/web/public/stylesheets/app/editor/share.less b/services/web/public/stylesheets/app/editor/share.less index cd06a15313..9efa8fdbad 100644 --- a/services/web/public/stylesheets/app/editor/share.less +++ b/services/web/public/stylesheets/app/editor/share.less @@ -47,4 +47,10 @@ } } } -} \ No newline at end of file +} +.modal-footer-share { + .modal-footer-left { + max-width: 70%; + text-align: left; + } +} diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index f01e2c7015..28bf1ab6a2 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/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() From 7bbbfe20b9c6cfd6ba8e74926c43a61d783fb240 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 21 Dec 2016 13:50:13 +0000 Subject: [PATCH 10/17] If external auth is used, remove `/register` items from header nav. (logic moved from docker-image settings file) --- .../app/coffee/infrastructure/ExpressLocals.coffee | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index e469df9422..d976a1d23a 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -244,6 +244,17 @@ module.exports = (app, webRouter, apiRouter)-> for key, value of Settings.nav res.locals.nav[key] = _.clone(Settings.nav[key]) res.locals.templates = Settings.templateLinks + try + externalAuth = res.locals.externalAuthenticationSystemUsed() + if externalAuth and res.locals.nav.header? + # filter out '/register' link + res.locals.nav.header = _.filter( + res.locals.nav.header, + (h) -> + h.url != '/register' + ) + catch error + logger.error {error}, "error while trying to filter out '/register' links from header" next() webRouter.use (req, res, next) -> From 862e15b8429a3a78fc9d5bc7b1bdf6a35484a985 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 5 Jan 2017 15:02:10 +0000 Subject: [PATCH 11/17] log out user id and anonymous when loading editor --- .../web/app/coffee/Features/Project/ProjectController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 1d975ea5b3..44933fdcbc 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/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)-> From 84ce2d0e14b720f4a2c74894295c3e8ad1bdf0ed Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 6 Jan 2017 11:00:21 +0000 Subject: [PATCH 12/17] change default nav to use translations for login and register --- services/web/config/settings.defaults.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index ccfec59235..708892e2fa 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -336,11 +336,11 @@ module.exports = settings = }] header: [{ - text: "Register" + text: "register" url: "/register" only_when_logged_out: true }, { - text: "Log In" + text: "log_in" url: "/login" only_when_logged_out: true }, { From 0b67265eb61b1f57da4450c9bbef9dd2eb5c7a8e Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 6 Jan 2017 11:32:57 +0000 Subject: [PATCH 13/17] use admin email for closed site --- services/web/app/views/general/closed.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/general/closed.jade b/services/web/app/views/general/closed.jade index 9f21372c81..4a27e84681 100644 --- a/services/web/app/views/general/closed.jade +++ b/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} From f5ced0307479d6f22d9bb98be3b02ba9039dc46b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 10 Jan 2017 15:42:36 +0000 Subject: [PATCH 14/17] Set redirect when sending user to `login` page. Allows smart redirecting to work when public access is turned off. --- .../Authentication/AuthenticationController.coffee | 1 + .../Authentication/AuthenticationControllerTests.coffee | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index e406296730..e8e3db4f93 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/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)-> diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee index 72265eac11..515b888911 100644 --- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/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" From 731f280e2e6902eb2462b5b3c83b0fc848e4c1df Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 11 Jan 2017 10:27:38 +0000 Subject: [PATCH 15/17] Move auth parts of top menu out of config and into web templates. Move the remaining configuration into a new config var: `nav.header_extras`. Add a `nav.showSubscriptionLink` var to control visibility of subscription link in the Account menu. This will allow admins to more easily configure extra links in the top navigation bar, without the danger of overwriting the important auth menus. --- .../infrastructure/ExpressLocals.coffee | 13 +----- services/web/app/views/layout/navbar.jade | 42 +++++++++++++++---- services/web/config/settings.defaults.coffee | 34 +++------------ 3 files changed, 42 insertions(+), 47 deletions(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index d976a1d23a..867583468b 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -244,17 +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 - try - externalAuth = res.locals.externalAuthenticationSystemUsed() - if externalAuth and res.locals.nav.header? - # filter out '/register' link - res.locals.nav.header = _.filter( - res.locals.nav.header, - (h) -> - h.url != '/register' - ) - catch error - logger.error {error}, "error while trying to filter out '/register' links from header" + 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) -> diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.jade index 3cd6587592..8ec5b5dbf3 100644 --- a/services/web/app/views/layout/navbar.jade +++ b/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="/projects") #{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')} diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 708892e2fa..44e4a75867 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -335,35 +335,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: {} From 411cb4330c788065669b1a20187841828a77463e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 11 Jan 2017 10:39:34 +0000 Subject: [PATCH 16/17] Fix typo in hyperlink --- services/web/app/views/layout/navbar.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.jade index 8ec5b5dbf3..4d78d02fba 100644 --- a/services/web/app/views/layout/navbar.jade +++ b/services/web/app/views/layout/navbar.jade @@ -65,7 +65,7 @@ nav.navbar.navbar-default // projects link and account menu if getSessionUser() li - a(href="/projects") #{translate('Projects')} + a(href="/project") #{translate('Projects')} li.dropdown(dropdown) a.dropbodw-toggle(href, dropdown-toggle) | #{translate('Account')} From df4b5c1b3777764c5404978bb2a1b035e9e8b382 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 12 Jan 2017 13:25:19 +0000 Subject: [PATCH 17/17] math mode syntax checking improvements for beta users from our ace commit 442a1c522c58b1e511d2fd2c6f03909488d41e60 --- .../public/js/ace-1.2.5/worker-latex_beta.js | 221 +++++++++++++++--- 1 file changed, 188 insertions(+), 33 deletions(-) diff --git a/services/web/public/js/ace-1.2.5/worker-latex_beta.js b/services/web/public/js/ace-1.2.5/worker-latex_beta.js index b47d8f0a46..720c3e5009 100644 --- a/services/web/public/js/ace-1.2.5/worker-latex_beta.js +++ b/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}); }; };