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/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/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee new file mode 100644 index 0000000000..00a878c276 --- /dev/null +++ b/services/web/app/coffee/Features/Email/Bodies/SingleCTAEmailBody.coffee @@ -0,0 +1,49 @@ +_ = require("underscore") +settings = require "settings-sharelatex" + +module.exports = _.template """ + + +
+
+

+ <%= title %> +

+
 
+

+ <%= greeting %> +

+

+ <%= message %> +

+
 
+
+
+ + <%= ctaText %> + +
+
+ <% if (secondaryMessage) { %> +
 
+

+ <%= secondaryMessage %> +

+ <% } %> +
+<% if (gmailGoToAction) { %> + +<% } %> +""" diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 70d11e219b..306aad3d2a 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") @@ -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 """ -

Password Reset

-

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

-

-
-
- - - Reset 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 + 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 """ -

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." + 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 """ -

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

-
-
-
- - - Verify now - - -
-
-
-

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 + gmailGoToAction: null + }) module.exports = templates: templates @@ -177,4 +163,4 @@ module.exports = html: template.layout(opts) text: template?.plainTextTemplate?(opts) type:template.type - } + } \ No newline at end of file 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..6d25df2197 --- /dev/null +++ b/services/web/app/coffee/Features/Email/Layouts/BaseWithHeaderEmailLayout.coffee @@ -0,0 +1,380 @@ +_ = require("underscore") +settings = require "settings-sharelatex" + +module.exports = _.template """ + + + + + + + + + Project invite + + + + + + + + +
+
+ +
+
+ + +
+
+

+ SHARELATEX +

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

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

+
+
+ +
+
+ +
                                                           
+ + + +""" 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)-> diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index e469df9422..867583468b 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/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) -> 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} diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.jade index 3cd6587592..4d78d02fba 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="/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')} 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 145da0b997..8e503801f9 100644 --- a/services/web/config/settings.defaults.coffee +++ b/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: {} 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/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(); }; 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}); }; }; 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/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" 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()