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

This commit is contained in:
Shane Kilkelly 2017-01-13 14:17:18 +00:00
commit 525e871d55
18 changed files with 1037 additions and 561 deletions

View file

@ -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)->

View file

@ -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

View file

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

View file

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

View file

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

View file

@ -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)->

View file

@ -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) ->

View file

@ -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}

View file

@ -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')}

View file

@ -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")}

View file

@ -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: {}

View file

@ -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

View file

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

View file

@ -1490,7 +1490,7 @@ var Tokenise = function (text) {
var controlSequence = NEXTCS.exec(text);
var nextSpecialPos = controlSequence === null ? idx : controlSequence.index;
if (nextSpecialPos === idx) {
Tokens.push([lineNumber, code, pos, idx + 1, text[idx], "control-symbol"]);
Tokens.push([lineNumber, code, pos, idx + 1, text[idx]]);
idx = SPECIAL.lastIndex = idx + 1;
char = text[nextSpecialPos];
if (char === '\n') { lineNumber++; linePosition[lineNumber] = nextSpecialPos;};
@ -1508,7 +1508,12 @@ var Tokenise = function (text) {
} else if (code === "}") { // close group
Tokens.push([lineNumber, code, pos]);
} else if (code === "$") { // math mode
Tokens.push([lineNumber, code, pos]);
if (text[idx] === "$") {
idx = SPECIAL.lastIndex = idx + 1;
Tokens.push([lineNumber, "$$", pos]);
} else {
Tokens.push([lineNumber, code, pos]);
}
} else if (code === "&") { // tabalign
Tokens.push([lineNumber, code, pos]);
} else if (code === "#") { // macro parameter
@ -1589,29 +1594,6 @@ var read1name = function (TokeniseResult, k) {
}
};
var read1filename = function (TokeniseResult, k) {
var Tokens = TokeniseResult.tokens;
var text = TokeniseResult.text;
var fileName = "";
for (var j = k + 1, tok; (tok = Tokens[j]); j++) {
if (tok[1] === "Text") {
var str = text.substring(tok[2], tok[3]);
if (!str.match(/^\S*$/)) { break; }
fileName = fileName + str;
} else if (tok[1] === "_") {
fileName = fileName + "_";
} else {
break;
}
}
if (fileName.length > 0) {
return j; // advance past these tokens
} else {
return null;
}
};
var readOptionalParams = function(TokeniseResult, k) {
var Tokens = TokeniseResult.tokens;
var text = TokeniseResult.text;
@ -1715,6 +1697,7 @@ var readUrl = function(TokeniseResult, k) {
return null;
};
var InterpretTokens = function (TokeniseResult, ErrorReporter) {
var Tokens = TokeniseResult.tokens;
var linePosition = TokeniseResult.linePosition;
@ -1723,9 +1706,7 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo;
var TokenError = ErrorReporter.TokenError;
var Environments = new EnvHandler(ErrorReporter);
var nextGroupMathMode = null; // if the next group should have math mode on or off (for \hbox)
var Environments = [];
for (var i = 0, len = Tokens.length; i < len; i++) {
var token = Tokens[i];
@ -1810,407 +1791,128 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
} else if (seq === "url") {
newPos = readUrl(TokeniseResult, i);
if (newPos === null) { TokenError(token, "invalid url command"); } else {i = newPos;};
} else if (seq === "left" || seq === "right") {
var nextToken = Tokens[i+1];
char = "";
if (nextToken && nextToken[1] === "Text") {
char = text.substring(nextToken[2], nextToken[2] + 1);
} else if (nextToken && nextToken[1] === "\\" && nextToken[5] == "control-symbol") {
char = nextToken[4];
} else if (nextToken && nextToken[1] === "\\") {
char = "unknown";
}
if (char === "" || (char !== "unknown" && "(){}[]<>|.".indexOf(char) === -1)) {
TokenError(token, "invalid bracket command");
} else {
i = i + 1;
Environments.push({command:seq, token:token});
};
} else if (seq === "(" || seq === ")" || seq === "[" || seq === "]") {
Environments.push({command:seq, token:token});
} else if (seq === "input") {
newPos = read1filename(TokeniseResult, i);
if (newPos === null) { continue; } else {i = newPos;};
} else if (seq === "hbox" || seq === "text" || seq === "mbox") {
nextGroupMathMode = false;
} else if (typeof seq === "string" && seq.match(/^(alpha|beta|gamma|delta|epsilon|varepsilon|zeta|eta|theta|vartheta|iota|kappa|lambda|mu|nu|xi|pi|varpi|rho|varrho|sigma|varsigma|tau|upsilon|phi|varphi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)$/)) {
var currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
if (currentMathMode === null && !insideGroup) {
TokenError(token, type + seq + " must be inside math mode");
};
} else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection|cite|ref)/)) {
currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
if (currentMathMode && !insideGroup) {
TokenError(token, type + seq + " used inside math mode");
Environments.resetMathMode();
};
};
}
} else if (type === "{") {
Environments.push({command:"{", token:token, mathMode: nextGroupMathMode});
nextGroupMathMode = null;
Environments.push({command:"{", token:token});
} else if (type === "}") {
Environments.push({command:"}", token:token});
} else if (type === "$") {
var lookAhead = Tokens[i+1];
var nextIsDollar = lookAhead && lookAhead[1] === "$";
currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
if (nextIsDollar && (!currentMathMode || currentMathMode.command == "$$")) {
Environments.push({command:"$$", token:token});
i = i + 1;
} else {
Environments.push({command:"$", token:token});
}
} else if (type === "^" || type === "_") {
currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
var insideGroup = Environments.insideGroup(); // true if inside {....}
if (currentMathMode === null && !insideGroup) {
TokenError(token, type + " must be inside math mode");
};
} else {
nextGroupMathMode = null;
}
};
};
return Environments;
};
var EnvHandler = function (ErrorReporter) {
var CheckEnvironments = function (Environments, ErrorReporter) {
var ErrorTo = ErrorReporter.EnvErrorTo;
var ErrorFromTo = ErrorReporter.EnvErrorFromTo;
var ErrorFrom = ErrorReporter.EnvErrorFrom;
var envs = [];
var state = [];
var documentClosed = null;
var inVerbatim = false;
var verbatimRanges = [];
this.Environments = envs;
this.push = function (newEnv) {
this.setEnvProps(newEnv);
this.checkAndUpdateState(newEnv);
envs.push(newEnv);
};
this._endVerbatim = function (thisEnv) {
var lastEnv = state.pop();
if (lastEnv && lastEnv.name === thisEnv.name) {
inVerbatim = false;
verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]});
} else {
if(lastEnv) { state.push(lastEnv); } ;
for (var i = 0, len = Environments.length; i < len; i++) {
var name = Environments[i].name ;
if (name && name.match(/^(verbatim|boxedverbatim|lstlisting|minted)$/)) {
Environments[i].verbatim = true;
}
};
var invalidEnvs = [];
this._end = function (thisEnv) {
do {
}
for (i = 0, len = Environments.length; i < len; i++) {
var thisEnv = Environments[i];
if(thisEnv.command === "begin" || thisEnv.command === "{") {
if (inVerbatim) { continue; } // ignore anything in verbatim environments
if (thisEnv.verbatim) {inVerbatim = true;};
state.push(thisEnv);
} else if (thisEnv.command === "end" || thisEnv.command === "}") {
var lastEnv = state.pop();
var retry = false;
var i;
if (closedBy(lastEnv, thisEnv)) {
if (thisEnv.command === "end" && thisEnv.name === "document" && !documentClosed) {
if (inVerbatim) {
if (lastEnv && lastEnv.name === thisEnv.name) {
inVerbatim = false;
verbatimRanges.push({start: lastEnv.token[2], end: thisEnv.token[2]});
continue;
} else {
if(lastEnv) { state.push(lastEnv); } ;
continue; // ignore all other commands
}
};
if (lastEnv && lastEnv.command === "{" && thisEnv.command === "}") {
continue;
} else if (lastEnv && lastEnv.name === thisEnv.name) {
if (thisEnv.name === "document" && !documentClosed) {
documentClosed = thisEnv;
};
return;
continue;
} else if (!lastEnv) {
if (documentClosed) {
ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
} else {
ErrorTo(thisEnv, "unexpected \\end{" + thisEnv.name + "}");
}
} 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();
};

View file

@ -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});
};
};

View file

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

View file

@ -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"

View file

@ -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()