diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee
index 2444c8cb0d..f2ce93f671 100644
--- a/services/web/Gruntfile.coffee
+++ b/services/web/Gruntfile.coffee
@@ -1,5 +1,6 @@
fs = require "fs"
PackageVersions = require "./app/coffee/infrastructure/PackageVersions"
+require('es6-promise').polyfill()
module.exports = (grunt) ->
grunt.loadNpmTasks 'grunt-contrib-coffee'
@@ -18,6 +19,7 @@ module.exports = (grunt) ->
grunt.loadNpmTasks 'grunt-contrib-watch'
grunt.loadNpmTasks 'grunt-parallel'
grunt.loadNpmTasks 'grunt-exec'
+ grunt.loadNpmTasks 'grunt-postcss'
# grunt.loadNpmTasks 'grunt-contrib-imagemin'
# grunt.loadNpmTasks 'grunt-sprity'
@@ -136,8 +138,14 @@ module.exports = (grunt) ->
files:
"public/stylesheets/style.css": "public/stylesheets/style.less"
-
-
+ postcss:
+ options:
+ map: true,
+ processors: [
+ require('autoprefixer')({browsers: [ 'last 2 versions', 'ie >= 10' ]})
+ ]
+ dist:
+ src: 'public/stylesheets/style.css'
env:
run:
@@ -222,11 +230,11 @@ module.exports = (grunt) ->
sed:
version:
- path: "app/views/sentry.jade"
+ path: "app/views/sentry.pug"
pattern: '@@COMMIT@@',
replacement: '<%= commit %>',
release:
- path: "app/views/sentry.jade"
+ path: "app/views/sentry.pug"
pattern: "@@RELEASE@@"
replacement: process.env.BUILD_NUMBER || "(unknown build)"
@@ -366,7 +374,7 @@ module.exports = (grunt) ->
grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir', 'compile:modules:server']
grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs', "compile:modules:client", 'compile:modules:inject_clientside_includes']
- grunt.registerTask 'compile:css', 'Compile the less files to css', ['less']
+ grunt.registerTask 'compile:css', 'Compile the less files to css', ['less', 'postcss:dist']
grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append", "exec:cssmin",]
grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests']
grunt.registerTask 'compile:acceptance_tests', 'Compile the acceptance tests', ['clean:acceptance_tests', 'coffee:acceptance_tests']
@@ -389,5 +397,5 @@ module.exports = (grunt) ->
grunt.registerTask 'default', 'run'
- grunt.registerTask 'version', "Write the version number into sentry.jade", ['git-rev-parse', 'sed']
+ grunt.registerTask 'version', "Write the version number into sentry.pug", ['git-rev-parse', 'sed']
diff --git a/services/web/app/coffee/Features/Announcements/AnnouncementsController.coffee b/services/web/app/coffee/Features/Announcements/AnnouncementsController.coffee
index 65013eae46..9c3a9f4deb 100644
--- a/services/web/app/coffee/Features/Announcements/AnnouncementsController.coffee
+++ b/services/web/app/coffee/Features/Announcements/AnnouncementsController.coffee
@@ -9,11 +9,11 @@ module.exports =
if !settings?.apis?.analytics?.url? or !settings.apis.blog.url?
return res.json []
- user_id = AuthenticationController.getLoggedInUserId(req)
- logger.log {user_id}, "getting unread announcements"
- AnnouncementsHandler.getUnreadAnnouncements user_id, (err, announcements)->
+ user = AuthenticationController.getSessionUser(req)
+ logger.log {user_id:user?._id}, "getting unread announcements"
+ AnnouncementsHandler.getUnreadAnnouncements user, (err, announcements)->
if err?
- logger.err {err, user_id}, "unable to get unread announcements"
+ logger.err {err:err, user_id:user._id}, "unable to get unread announcements"
next(err)
else
res.json announcements
diff --git a/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee b/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee
index ce41e3b96c..9934a8bf69 100644
--- a/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee
+++ b/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee
@@ -1,24 +1,46 @@
AnalyticsManager = require("../Analytics/AnalyticsManager")
BlogHandler = require("../Blog/BlogHandler")
-async = require("async")
-_ = require("lodash")
logger = require("logger-sharelatex")
settings = require("settings-sharelatex")
+async = require("async")
+_ = require("lodash")
-module.exports =
+module.exports = AnnouncementsHandler =
+
+ _domainSpecificAnnouncements : (email)->
+ domainSpecific = _.filter settings?.domainAnnouncements, (domainAnnouncment)->
+ matches = _.filter domainAnnouncment.domains, (domain)->
+ return email.indexOf(domain) != -1
+ return matches.length > 0 and domainAnnouncment.id?
+ return domainSpecific or []
+
+
+ getUnreadAnnouncements : (user, callback = (err, announcements)->)->
+ if !user? and !user._id?
+ return callback("user not supplied")
- getUnreadAnnouncements : (user_id, callback = (err, announcements)->)->
async.parallel {
lastEvent: (cb)->
- AnalyticsManager.getLastOccurance user_id, "announcement-alert-dismissed", cb
+ AnalyticsManager.getLastOccurance user._id, "announcement-alert-dismissed", cb
announcements: (cb)->
BlogHandler.getLatestAnnouncements cb
}, (err, results)->
if err?
- logger.err err:err, user_id:user_id, "error getting unread announcements"
+ logger.err err:err, user_id:user._id, "error getting unread announcements"
return callback(err)
- announcements = _.sortBy(results.announcements, "date").reverse()
+ domainSpecific = AnnouncementsHandler._domainSpecificAnnouncements(user?.email)
+
+ domainSpecific = _.map domainSpecific, (domainAnnouncment)->
+ try
+ domainAnnouncment.date = new Date(domainAnnouncment.date)
+ return domainAnnouncment
+ catch e
+ return callback(e)
+
+ announcements = results.announcements
+ announcements = _.union announcements, domainSpecific
+ announcements = _.sortBy(announcements, "date").reverse()
lastSeenBlogId = results?.lastEvent?.segmentation?.blogPostId
@@ -35,6 +57,6 @@ module.exports =
announcement.read = read
return announcement
- logger.log announcementsLength:announcements?.length, user_id:user_id, "returning announcements"
+ logger.log announcementsLength:announcements?.length, user_id:user?._id, "returning announcements"
callback null, announcements
diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index e406296730..485b046a85 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)->
@@ -193,8 +194,8 @@ module.exports = AuthenticationController =
_setRedirectInSession: (req, value) ->
if !value?
- value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else req.path
- if req.session?
+ value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else "#{req.path}"
+ if req.session? && !value.match(new RegExp('^\/(socket.io|js|stylesheets|img)\/.*$'))
req.session.postLoginRedirect = value
_getRedirectFromSession: (req) ->
diff --git a/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee b/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee
new file mode 100644
index 0000000000..3cae19b7f3
--- /dev/null
+++ b/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee
@@ -0,0 +1,82 @@
+request = require("request")
+settings = require("settings-sharelatex")
+logger = require("logger-sharelatex")
+
+module.exports = ChatApiHandler =
+ _apiRequest: (opts, callback = (error, data) ->) ->
+ request opts, (error, response, data) ->
+ return callback(error) if error?
+ if 200 <= response.statusCode < 300
+ return callback null, data
+ else
+ error = new Error("chat api returned non-success code: #{response.statusCode}")
+ error.statusCode = response.statusCode
+ logger.error {err: error, opts}, "error sending request to chat api"
+ return callback error
+
+ sendGlobalMessage: (project_id, user_id, content, callback)->
+ ChatApiHandler._apiRequest {
+ url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages"
+ method: "POST"
+ json: {user_id, content}
+ }, callback
+
+ getGlobalMessages: (project_id, limit, before, callback)->
+ qs = {}
+ qs.limit = limit if limit?
+ qs.before = before if before?
+
+ ChatApiHandler._apiRequest {
+ url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages"
+ method: "GET"
+ qs: qs
+ json: true
+ }, callback
+
+ sendComment: (project_id, thread_id, user_id, content, callback = (error) ->) ->
+ ChatApiHandler._apiRequest {
+ url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages"
+ method: "POST"
+ json: {user_id, content}
+ }, callback
+
+ getThreads: (project_id, callback = (error) ->) ->
+ ChatApiHandler._apiRequest {
+ url: "#{settings.apis.chat.internal_url}/project/#{project_id}/threads"
+ method: "GET"
+ json: true
+ }, callback
+
+ resolveThread: (project_id, thread_id, user_id, callback = (error) ->) ->
+ ChatApiHandler._apiRequest {
+ url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/resolve"
+ method: "POST"
+ json: {user_id}
+ }, callback
+
+ reopenThread: (project_id, thread_id, callback = (error) ->) ->
+ ChatApiHandler._apiRequest {
+ url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/reopen"
+ method: "POST"
+ }, callback
+
+ deleteThread: (project_id, thread_id, callback = (error) ->) ->
+ ChatApiHandler._apiRequest {
+ url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}"
+ method: "DELETE"
+ }, callback
+
+ editMessage: (project_id, thread_id, message_id, content, callback = (error) ->) ->
+ ChatApiHandler._apiRequest {
+ url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}/edit"
+ method: "POST"
+ json:
+ content: content
+ }, callback
+
+ deleteMessage: (project_id, thread_id, message_id, callback = (error) ->) ->
+ ChatApiHandler._apiRequest {
+ url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}"
+ method: "DELETE"
+ }, callback
+
\ No newline at end of file
diff --git a/services/web/app/coffee/Features/Chat/ChatController.coffee b/services/web/app/coffee/Features/Chat/ChatController.coffee
index 35c280712a..3090f4f108 100644
--- a/services/web/app/coffee/Features/Chat/ChatController.coffee
+++ b/services/web/app/coffee/Features/Chat/ChatController.coffee
@@ -1,33 +1,34 @@
-ChatHandler = require("./ChatHandler")
+ChatApiHandler = require("./ChatApiHandler")
EditorRealTimeController = require("../Editor/EditorRealTimeController")
logger = require("logger-sharelatex")
AuthenticationController = require('../Authentication/AuthenticationController')
+UserInfoManager = require('../User/UserInfoManager')
+UserInfoController = require('../User/UserInfoController')
+CommentsController = require('../Comments/CommentsController')
module.exports =
-
-
sendMessage: (req, res, next)->
- project_id = req.params.Project_id
- messageContent = req.body.content
+ project_id = req.params.project_id
+ content = req.body.content
user_id = AuthenticationController.getLoggedInUserId(req)
if !user_id?
err = new Error('no logged-in user')
return next(err)
- ChatHandler.sendMessage project_id, user_id, messageContent, (err, builtMessge)->
- if err?
- logger.err err:err, project_id:project_id, user_id:user_id, messageContent:messageContent, "problem sending message to chat api"
- return res.sendStatus(500)
- EditorRealTimeController.emitToRoom project_id, "new-chat-message", builtMessge, (err)->
- res.send()
+ ChatApiHandler.sendGlobalMessage project_id, user_id, content, (err, message) ->
+ return next(err) if err?
+ UserInfoManager.getPersonalInfo message.user_id, (err, user) ->
+ return next(err) if err?
+ message.user = UserInfoController.formatPersonalInfo(user)
+ EditorRealTimeController.emitToRoom project_id, "new-chat-message", message, (err)->
+ res.send(204)
- getMessages: (req, res)->
- project_id = req.params.Project_id
+ getMessages: (req, res, next)->
+ project_id = req.params.project_id
query = req.query
logger.log project_id:project_id, query:query, "getting messages"
- ChatHandler.getMessages project_id, query, (err, messages)->
- if err?
- logger.err err:err, query:query, "problem getting messages from chat api"
- return res.sendStatus 500
- logger.log length:messages?.length, "sending messages to client"
- res.set 'Content-Type', 'application/json'
- res.send messages
+ ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) ->
+ return next(err) if err?
+ CommentsController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) ->
+ return next(err) if err?
+ logger.log length: messages?.length, "sending messages to client"
+ res.json messages
diff --git a/services/web/app/coffee/Features/Chat/ChatHandler.coffee b/services/web/app/coffee/Features/Chat/ChatHandler.coffee
deleted file mode 100644
index b77652bc39..0000000000
--- a/services/web/app/coffee/Features/Chat/ChatHandler.coffee
+++ /dev/null
@@ -1,32 +0,0 @@
-request = require("request")
-settings = require("settings-sharelatex")
-logger = require("logger-sharelatex")
-
-module.exports =
-
- sendMessage: (project_id, user_id, messageContent, callback)->
- opts =
- method:"post"
- json:
- content:messageContent
- user_id:user_id
- uri:"#{settings.apis.chat.internal_url}/room/#{project_id}/messages"
- request opts, (err, response, body)->
- if err?
- logger.err err:err, "problem sending new message to chat"
- callback(err, body)
-
-
-
- getMessages: (project_id, query, callback)->
- qs = {}
- qs.limit = query.limit if query?.limit?
- qs.before = query.before if query?.before?
-
- opts =
- uri:"#{settings.apis.chat.internal_url}/room/#{project_id}/messages"
- method:"get"
- qs: qs
-
- request opts, (err, response, body)->
- callback(err, body)
\ No newline at end of file
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
index bc7eb90c3f..913562f417 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
@@ -11,7 +11,7 @@ module.exports = CollaboratorsEmailHandler =
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
].join("&")
- notifyUserOfProjectInvite: (project_id, email, invite, callback)->
+ notifyUserOfProjectInvite: (project_id, email, invite, sendingUser, callback)->
Project
.findOne(_id: project_id )
.select("name owner_ref")
@@ -24,4 +24,5 @@ module.exports = CollaboratorsEmailHandler =
name: project.name
inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
owner: project.owner_ref
+ sendingUser_id: sendingUser._id
EmailHandler.sendEmail "projectInvite", emailOptions, callback
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 9d9f4d2a5e..a2314da57f 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -10,6 +10,7 @@ EditorRealTimeController = require("../Editor/EditorRealTimeController")
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
AnalyticsManger = require("../Analytics/AnalyticsManager")
AuthenticationController = require("../Authentication/AuthenticationController")
+rateLimiter = require("../../infrastructure/RateLimiter")
module.exports = CollaboratorsInviteController =
@@ -31,12 +32,28 @@ module.exports = CollaboratorsInviteController =
callback(null, userExists)
else
callback(null, true)
+
+ _checkRateLimit: (user_id, callback = (error) ->) ->
+ LimitationsManager.allowedNumberOfCollaboratorsForUser user_id, (err, collabLimit = 1)->
+ return callback(err) if err?
+ if collabLimit == -1
+ collabLimit = 20
+ collabLimit = collabLimit * 10
+ opts =
+ endpointName: "invite-to-project-by-user-id"
+ timeInterval: 60 * 30
+ subjectName: user_id
+ throttle: collabLimit
+ rateLimiter.addCount opts, callback
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
email = req.body.email
sendingUser = AuthenticationController.getSessionUser(req)
sendingUserId = sendingUser._id
+ if email == sendingUser.email
+ logger.log {projectId, email, sendingUserId}, "cannot invite yourself to project"
+ return res.json {invite: null, error: 'cannot_invite_self'}
logger.log {projectId, email, sendingUserId}, "inviting to project"
LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
return next(error) if error?
@@ -48,20 +65,24 @@ module.exports = CollaboratorsInviteController =
if !email? or email == ""
logger.log {projectId, email, sendingUserId}, "invalid email address"
return res.sendStatus(400)
- CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)->
- if err?
- logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address"
- return next(err)
- 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) ->
+ CollaboratorsInviteController._checkRateLimit sendingUserId, (error, underRateLimit) ->
+ return next(error) if error?
+ if !underRateLimit
+ return res.sendStatus(429)
+ 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/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 5ed6570c3a..ecca8ab86f 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -53,7 +53,7 @@ module.exports = CollaboratorsInviteHandler =
_sendMessages: (projectId, sendingUser, invite, callback=(err)->) ->
logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite"
- CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, (err)->
+ CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, sendingUser, (err)->
return callback(err) if err?
CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
return callback(err) if err?
@@ -80,7 +80,7 @@ module.exports = CollaboratorsInviteHandler =
# Send email and notification in background
CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
if err?
- logger.err {projectId, email}, "error sending messages for invite"
+ logger.err {err, projectId, email}, "error sending messages for invite"
callback(null, invite)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
index 4c7cc8c76a..ea7e1f89f8 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
@@ -22,9 +22,15 @@ module.exports =
webRouter.post(
'/project/:Project_id/invite',
RateLimiterMiddlewear.rateLimit({
- endpointName: "invite-to-project"
+ endpointName: "invite-to-project-by-project-id"
params: ["Project_id"]
- maxRequests: 200
+ maxRequests: 100
+ timeInterval: 60 * 10
+ }),
+ RateLimiterMiddlewear.rateLimit({
+ endpointName: "invite-to-project-by-ip"
+ ipOnly:true
+ maxRequests: 100
timeInterval: 60 * 10
}),
AuthenticationController.requireLogin(),
diff --git a/services/web/app/coffee/Features/Comments/CommentsController.coffee b/services/web/app/coffee/Features/Comments/CommentsController.coffee
new file mode 100644
index 0000000000..bda006eb8f
--- /dev/null
+++ b/services/web/app/coffee/Features/Comments/CommentsController.coffee
@@ -0,0 +1,111 @@
+ChatApiHandler = require("../Chat/ChatApiHandler")
+EditorRealTimeController = require("../Editor/EditorRealTimeController")
+logger = require("logger-sharelatex")
+AuthenticationController = require('../Authentication/AuthenticationController')
+UserInfoManager = require('../User/UserInfoManager')
+UserInfoController = require('../User/UserInfoController')
+DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
+async = require "async"
+
+module.exports = CommentsController =
+ sendComment: (req, res, next) ->
+ {project_id, thread_id} = req.params
+ content = req.body.content
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ if !user_id?
+ err = new Error('no logged-in user')
+ return next(err)
+ logger.log {project_id, thread_id, user_id, content}, "sending comment"
+ ChatApiHandler.sendComment project_id, thread_id, user_id, content, (err, comment) ->
+ return next(err) if err?
+ UserInfoManager.getPersonalInfo comment.user_id, (err, user) ->
+ return next(err) if err?
+ comment.user = UserInfoController.formatPersonalInfo(user)
+ EditorRealTimeController.emitToRoom project_id, "new-comment", thread_id, comment, (err) ->
+ res.send 204
+
+ getThreads: (req, res, next) ->
+ {project_id} = req.params
+ logger.log {project_id}, "getting comment threads for project"
+ ChatApiHandler.getThreads project_id, (err, threads) ->
+ return next(err) if err?
+ CommentsController._injectUserInfoIntoThreads threads, (error, threads) ->
+ return next(err) if err?
+ res.json threads
+
+ resolveThread: (req, res, next) ->
+ {project_id, thread_id} = req.params
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ logger.log {project_id, thread_id, user_id}, "resolving comment thread"
+ ChatApiHandler.resolveThread project_id, thread_id, user_id, (err) ->
+ return next(err) if err?
+ UserInfoManager.getPersonalInfo user_id, (err, user) ->
+ return next(err) if err?
+ EditorRealTimeController.emitToRoom project_id, "resolve-thread", thread_id, UserInfoController.formatPersonalInfo(user), (err)->
+ res.send 204
+
+ reopenThread: (req, res, next) ->
+ {project_id, thread_id} = req.params
+ logger.log {project_id, thread_id}, "reopening comment thread"
+ ChatApiHandler.reopenThread project_id, thread_id, (err, threads) ->
+ return next(err) if err?
+ EditorRealTimeController.emitToRoom project_id, "reopen-thread", thread_id, (err)->
+ res.send 204
+
+ deleteThread: (req, res, next) ->
+ {project_id, doc_id, thread_id} = req.params
+ logger.log {project_id, doc_id, thread_id}, "deleting comment thread"
+ DocumentUpdaterHandler.deleteThread project_id, doc_id, thread_id, (err) ->
+ return next(err) if err?
+ ChatApiHandler.deleteThread project_id, thread_id, (err, threads) ->
+ return next(err) if err?
+ EditorRealTimeController.emitToRoom project_id, "delete-thread", thread_id, (err)->
+ res.send 204
+
+ editMessage: (req, res, next) ->
+ {project_id, thread_id, message_id} = req.params
+ {content} = req.body
+ logger.log {project_id, thread_id, message_id}, "editing message thread"
+ ChatApiHandler.editMessage project_id, thread_id, message_id, content, (err) ->
+ return next(err) if err?
+ EditorRealTimeController.emitToRoom project_id, "edit-message", thread_id, message_id, content, (err)->
+ res.send 204
+
+ deleteMessage: (req, res, next) ->
+ {project_id, thread_id, message_id} = req.params
+ logger.log {project_id, thread_id, message_id}, "deleting message"
+ ChatApiHandler.deleteMessage project_id, thread_id, message_id, (err, threads) ->
+ return next(err) if err?
+ EditorRealTimeController.emitToRoom project_id, "delete-message", thread_id, message_id, (err)->
+ res.send 204
+
+ _injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) ->
+ userCache = {}
+ getUserDetails = (user_id, callback = (error, user) ->) ->
+ return callback(null, userCache[user_id]) if userCache[user_id]?
+ UserInfoManager.getPersonalInfo user_id, (err, user) ->
+ return callback(error) if error?
+ user = UserInfoController.formatPersonalInfo user
+ userCache[user_id] = user
+ callback null, user
+
+ jobs = []
+ for thread_id, thread of threads
+ do (thread) ->
+ if thread.resolved
+ jobs.push (cb) ->
+ getUserDetails thread.resolved_by_user_id, (error, user) ->
+ cb(error) if error?
+ thread.resolved_by_user = user
+ cb()
+ for message in thread.messages
+ do (message) ->
+ jobs.push (cb) ->
+ getUserDetails message.user_id, (error, user) ->
+ cb(error) if error?
+ message.user = user
+ cb()
+
+ async.series jobs, (error) ->
+ return callback(error) if error?
+ return callback null, threads
\ No newline at end of file
diff --git a/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee b/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee
index 772d927d78..06dd14c17b 100644
--- a/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee
+++ b/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee
@@ -29,6 +29,21 @@ module.exports = DocstoreManager =
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
logger.error err: error, project_id: project_id, "error getting all docs from docstore"
callback(error)
+
+ getAllRanges: (project_id, callback = (error) ->) ->
+ logger.log { project_id }, "getting all doc ranges for project in docstore api"
+ url = "#{settings.apis.docstore.url}/project/#{project_id}/ranges"
+ request.get {
+ url: url
+ json: true
+ }, (error, res, docs) ->
+ return callback(error) if error?
+ if 200 <= res.statusCode < 300
+ callback(null, docs)
+ else
+ error = new Error("docstore api responded with non-success code: #{res.statusCode}")
+ logger.error err: error, project_id: project_id, "error getting all doc ranges from docstore"
+ callback(error)
getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev, version) ->) ->
if typeof(options) == "function"
@@ -45,13 +60,13 @@ module.exports = DocstoreManager =
return callback(error) if error?
if 200 <= res.statusCode < 300
logger.log doc_id: doc_id, project_id: project_id, version: doc.version, rev: doc.rev, "got doc from docstore api"
- callback(null, doc.lines, doc.rev, doc.version)
+ callback(null, doc.lines, doc.rev, doc.version, doc.ranges)
else
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
logger.error err: error, project_id: project_id, doc_id: doc_id, "error getting doc from docstore"
callback(error)
- updateDoc: (project_id, doc_id, lines, version, callback = (error, modified, rev) ->) ->
+ updateDoc: (project_id, doc_id, lines, version, ranges, callback = (error, modified, rev) ->) ->
logger.log project_id: project_id, doc_id: doc_id, "updating doc in docstore api"
url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}"
request.post {
@@ -59,6 +74,7 @@ module.exports = DocstoreManager =
json:
lines: lines
version: version
+ ranges: ranges
}, (error, res, result) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee
index dcf0615b25..5c15735410 100644
--- a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee
+++ b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee
@@ -95,7 +95,7 @@ module.exports = DocumentUpdaterHandler =
logger.error err: error, project_id: project_id, doc_id: doc_id, "document updater returned failure status code: #{res.statusCode}"
return callback(error)
- getDocument: (project_id, doc_id, fromVersion, callback = (error, exists, doclines, version) ->) ->
+ getDocument: (project_id, doc_id, fromVersion, callback = (error, doclines, version, ranges, ops) ->) ->
timer = new metrics.Timer("get-document")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}"
logger.log project_id:project_id, doc_id: doc_id, "getting doc from document updater"
@@ -110,7 +110,7 @@ module.exports = DocumentUpdaterHandler =
body = JSON.parse(body)
catch error
return callback(error)
- callback null, body.lines, body.version, body.ops
+ callback null, body.lines, body.version, body.ranges, body.ops
else
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
@@ -137,15 +137,37 @@ module.exports = DocumentUpdaterHandler =
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
- getNumberOfDocsInMemory : (callback)->
- request.get "#{settings.apis.documentupdater.url}/total", (err, req, body)->
- try
- body = JSON.parse body
- catch err
- logger.err err:err, "error parsing response from doc updater about the total number of docs"
- callback(err, body?.total)
-
+ acceptChange: (project_id, doc_id, change_id, callback = (error) ->) ->
+ timer = new metrics.Timer("accept-change")
+ url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/change/#{change_id}/accept"
+ logger.log {project_id, doc_id, change_id}, "accepting change in document updater"
+ request.post url, (error, res, body)->
+ timer.done()
+ if error?
+ logger.error {err:error, project_id, doc_id, change_id}, "error accepting change in doc updater"
+ return callback(error)
+ if res.statusCode >= 200 and res.statusCode < 300
+ logger.log {project_id, doc_id, change_id}, "accepted change in document updater"
+ return callback(null)
+ else
+ logger.error {project_id, doc_id, change_id}, "doc updater returned a non-success status code: #{res.statusCode}"
+ callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
+ deleteThread: (project_id, doc_id, thread_id, callback = (error) ->) ->
+ timer = new metrics.Timer("delete-thread")
+ url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/comment/#{thread_id}"
+ logger.log {project_id, doc_id, thread_id}, "deleting comment range in document updater"
+ request.del url, (error, res, body)->
+ timer.done()
+ if error?
+ logger.error {err:error, project_id, doc_id, thread_id}, "error deleting comment range in doc updater"
+ return callback(error)
+ if res.statusCode >= 200 and res.statusCode < 300
+ logger.log {project_id, doc_id, thread_id}, "deleted comment rangee in document updater"
+ return callback(null)
+ else
+ logger.error {project_id, doc_id, thread_id}, "doc updater returned a non-success status code: #{res.statusCode}"
+ callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
PENDINGUPDATESKEY = "PendingUpdates"
DOCLINESKEY = "doclines"
diff --git a/services/web/app/coffee/Features/Documents/DocumentController.coffee b/services/web/app/coffee/Features/Documents/DocumentController.coffee
index 560f232ba1..2042f6a218 100644
--- a/services/web/app/coffee/Features/Documents/DocumentController.coffee
+++ b/services/web/app/coffee/Features/Documents/DocumentController.coffee
@@ -7,7 +7,7 @@ module.exports =
doc_id = req.params.doc_id
plain = req?.query?.plain == 'true'
logger.log doc_id:doc_id, project_id:project_id, "receiving get document request from api (docupdater)"
- ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev, version) ->
+ ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev, version, ranges) ->
if error?
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
return next(error)
@@ -19,14 +19,15 @@ module.exports =
res.send JSON.stringify {
lines: lines
version: version
+ ranges: ranges
}
setDocument: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
doc_id = req.params.doc_id
- {lines, version} = req.body
+ {lines, version, ranges} = req.body
logger.log doc_id:doc_id, project_id:project_id, "receiving set document request from api (docupdater)"
- ProjectEntityHandler.updateDocLines project_id, doc_id, lines, version, (error) ->
+ ProjectEntityHandler.updateDocLines project_id, doc_id, lines, version, ranges, (error) ->
if error?
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
return next(error)
diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee
index 476ba96174..b5abab3bf9 100644
--- a/services/web/app/coffee/Features/Editor/EditorController.coffee
+++ b/services/web/app/coffee/Features/Editor/EditorController.coffee
@@ -7,7 +7,6 @@ ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
ProjectDeleter = require("../Project/ProjectDeleter")
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
EditorRealTimeController = require("./EditorRealTimeController")
-TrackChangesManager = require("../TrackChanges/TrackChangesManager")
async = require('async')
LockManager = require("../../infrastructure/LockManager")
_ = require('underscore')
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 %>
+
+
+
+
+
+ <% 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..0a06a2a175 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.
-
-
-
-
-
-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,23 @@ 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,22 +136,39 @@ Thank You
#{settings.appName} - <%= siteUrl %>
"""
- compiledTemplate: _.template """
-Hi, please verify your email to join the <%= group_name %> and get your free premium account
-
-
-
- Thank you
- #{settings.appName}
+ compiledTemplate: (opts) ->
+ SingleCTAEmailBody({
+ title: "Verify Email to join #{ opts.group_name } group"
+ greeting: "Hi,"
+ message: "please verify your email to join the #{ opts.group_name } group and get your free premium account."
+ secondaryMessage: null
+ ctaText: "Verify now"
+ ctaURL: opts.completeJoinUrl
+ gmailGoToAction: null
+ })
+
+
+templates.testEmail =
+ subject: _.template "A Test Email from ShareLaTeX"
+ layout: BaseWithHeaderEmailLayout
+ type:"notification"
+ plainTextTemplate: _.template """
+Hi,
+
+This is a test email sent from ShareLaTeX.
+
+#{settings.appName} - <%= siteUrl %>
"""
+ compiledTemplate: (opts) ->
+ SingleCTAEmailBody({
+ title: "A Test Email from ShareLaTeX"
+ greeting: "Hi,"
+ message: "This is a test email sent from ShareLaTeX"
+ secondaryMessage: null
+ ctaText: "Open ShareLaTeX"
+ ctaURL: "/"
+ gmailGoToAction: null
+ })
module.exports =
diff --git a/services/web/app/coffee/Features/Email/EmailSender.coffee b/services/web/app/coffee/Features/Email/EmailSender.coffee
index a7bcc82ed7..69574c8276 100644
--- a/services/web/app/coffee/Features/Email/EmailSender.coffee
+++ b/services/web/app/coffee/Features/Email/EmailSender.coffee
@@ -4,7 +4,7 @@ Settings = require('settings-sharelatex')
nodemailer = require("nodemailer")
sesTransport = require('nodemailer-ses-transport')
sgTransport = require('nodemailer-sendgrid-transport')
-
+rateLimiter = require('../../infrastructure/RateLimiter')
_ = require("underscore")
if Settings.email? and Settings.email.fromAddress?
@@ -39,24 +39,39 @@ if nm_client?
else
logger.warn "Failed to create email transport. Please check your settings. No email will be sent."
+checkCanSendEmail = (options, callback)->
+ if !options.sendingUser_id? #email not sent from user, not rate limited
+ return callback(null, true)
+ opts =
+ endpointName: "send_email"
+ timeInterval: 60 * 60 * 3
+ subjectName: options.sendingUser_id
+ throttle: 100
+ rateLimiter.addCount opts, callback
module.exports =
sendEmail : (options, callback = (error) ->)->
logger.log receiver:options.to, subject:options.subject, "sending email"
- metrics.inc "email"
- options =
- to: options.to
- from: defaultFromAddress
- subject: options.subject
- html: options.html
- text: options.text
- replyTo: options.replyTo || Settings.email.replyToAddress
- socketTimeout: 30 * 1000
- if Settings.email.textEncoding?
- opts.textEncoding = textEncoding
- client.sendMail options, (err, res)->
+ checkCanSendEmail options, (err, canContinue)->
if err?
- logger.err err:err, "error sending message"
- else
- logger.log "Message sent to #{options.to}"
- callback(err)
+ return callback(err)
+ if !canContinue
+ logger.log sendingUser_id:options.sendingUser_id, to:options.to, subject:options.subject, canContinue:canContinue, "rate limit hit for sending email, not sending"
+ return callback("rate limit hit sending email")
+ metrics.inc "email"
+ options =
+ to: options.to
+ from: defaultFromAddress
+ subject: options.subject
+ html: options.html
+ text: options.text
+ replyTo: options.replyTo || Settings.email.replyToAddress
+ socketTimeout: 30 * 1000
+ if Settings.email.textEncoding?
+ opts.textEncoding = textEncoding
+ client.sendMail options, (err, res)->
+ if err?
+ logger.err err:err, "error sending message"
+ else
+ logger.log "Message sent to #{options.to}"
+ callback(err)
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
+
+
+
+
+
+
+
+
+
+
+"""
diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee
new file mode 100644
index 0000000000..d4f42b38b1
--- /dev/null
+++ b/services/web/app/coffee/Features/History/HistoryController.coffee
@@ -0,0 +1,20 @@
+logger = require "logger-sharelatex"
+request = require "request"
+settings = require "settings-sharelatex"
+AuthenticationController = require "../Authentication/AuthenticationController"
+
+module.exports = HistoryController =
+ proxyToHistoryApi: (req, res, next = (error) ->) ->
+ user_id = AuthenticationController.getLoggedInUserId req
+ url = settings.apis.trackchanges.url + req.url
+ logger.log url: url, "proxying to track-changes api"
+ getReq = request(
+ url: url
+ method: req.method
+ headers:
+ "X-User-Id": user_id
+ )
+ getReq.pipe(res)
+ getReq.on "error", (error) ->
+ logger.error err: error, "track-changes API error"
+ next(error)
diff --git a/services/web/app/coffee/Features/History/HistoryManager.coffee b/services/web/app/coffee/Features/History/HistoryManager.coffee
new file mode 100644
index 0000000000..ea3f492613
--- /dev/null
+++ b/services/web/app/coffee/Features/History/HistoryManager.coffee
@@ -0,0 +1,28 @@
+settings = require "settings-sharelatex"
+request = require "request"
+logger = require "logger-sharelatex"
+
+module.exports = HistoryManager =
+ flushProject: (project_id, callback = (error) ->) ->
+ logger.log project_id: project_id, "flushing project in track-changes api"
+ url = "#{settings.apis.trackchanges.url}/project/#{project_id}/flush"
+ request.post url, (error, res, body) ->
+ return callback(error) if error?
+ if 200 <= res.statusCode < 300
+ callback(null)
+ else
+ error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
+ logger.error err: error, project_id: project_id, "error flushing project in track-changes api"
+ callback(error)
+
+ archiveProject: (project_id, callback = ()->)->
+ logger.log project_id: project_id, "archving project in track-changes api"
+ url = "#{settings.apis.trackchanges.url}/project/#{project_id}/archive"
+ request.post url, (error, res, body) ->
+ return callback(error) if error?
+ if 200 <= res.statusCode < 300
+ callback(null)
+ else
+ error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
+ logger.error err: error, project_id: project_id, "error archving project in track-changes api"
+ callback(error)
\ No newline at end of file
diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee
index a2afee0573..5c984dcb5d 100644
--- a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee
+++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee
@@ -5,8 +5,6 @@ DocstoreManager = require("../Docstore/DocstoreManager")
ProjectGetter = require("../Project/ProjectGetter")
ProjectUpdateHandler = require("../Project/ProjectUpdateHandler")
Project = require("../../models/Project").Project
-TrackChangesManager = require("../TrackChanges/TrackChangesManager")
-
MILISECONDS_IN_DAY = 86400000
module.exports = InactiveProjectManager =
@@ -52,7 +50,6 @@ module.exports = InactiveProjectManager =
logger.log project_id:project_id, "deactivating inactive project"
jobs = [
(cb)-> DocstoreManager.archiveProject project_id, cb
- # (cb)-> TrackChangesManager.archiveProject project_id, cb
(cb)-> ProjectUpdateHandler.markAsInactive project_id, cb
]
async.series jobs, (err)->
diff --git a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee
index ec5371f0f2..618e9e0a7d 100644
--- a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee
+++ b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee
@@ -53,7 +53,11 @@ module.exports =
if req.body.login_after
UserGetter.getUser user_id, {email: 1}, (err, user) ->
return next(err) if err?
- AuthenticationController.doLogin {email:user.email, password: password}, req, res, next
+ AuthenticationController.afterLoginSessionSetup req, user, (err) ->
+ if err?
+ logger.err {err, email: user.email}, "Error setting up session after setting password"
+ return next(err)
+ res.json {redir: AuthenticationController._getRedirectFromSession(req) || "/project"}
else
res.sendStatus 200
else
diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee
index 1d975ea5b3..3e6beaa0fb 100644
--- a/services/web/app/coffee/Features/Project/ProjectController.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectController.coffee
@@ -197,11 +197,11 @@ 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)->
- ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1}, cb
+ ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1, track_changes: 1 }, cb
user: (cb)->
if !user_id?
cb null, defaultSettingsForAnonymousUser(user_id)
@@ -267,6 +267,7 @@ module.exports = ProjectController =
pdfViewer : user.ace.pdfViewer
syntaxValidation: user.ace.syntaxValidation
}
+ trackChangesEnabled: !!project.track_changes
privilegeLevel: privilegeLevel
chatUrl: Settings.apis.chat.url
anonymous: anonymous
diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
index 1a10321544..5da582aa4c 100644
--- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
@@ -19,6 +19,11 @@ module.exports = ProjectEditorHandler =
if !result.invites?
result.invites = []
+
+ trackChangesVisible = false
+ for member in members
+ if member.privilegeLevel == "owner" and member.user?.featureSwitches?.track_changes
+ trackChangesVisible = true
{owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
result.owner = owner
@@ -32,6 +37,8 @@ module.exports = ProjectEditorHandler =
compileGroup:"standard"
templates: false
references: false
+ trackChanges: false
+ trackChangesVisible: trackChangesVisible
})
return result
diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
index 21932cefc9..eefaeab6ab 100644
--- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
@@ -126,7 +126,7 @@ module.exports = ProjectEntityHandler =
doc = new Doc name: docName
# Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
# which hasn't been created in docstore.
- DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, 0, (err, modified, rev) ->
+ DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, 0, {}, (err, modified, rev) ->
return callback(err) if err?
ProjectEntityHandler._putElement project, folder_id, doc, "doc", (err, result)=>
@@ -292,7 +292,7 @@ module.exports = ProjectEntityHandler =
return callback(err)
callback(err, folder, parentFolder_id)
- updateDocLines : (project_id, doc_id, lines, version, callback = (error) ->)->
+ updateDocLines : (project_id, doc_id, lines, version, ranges, callback = (error) ->)->
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
return callback(err) if err?
return callback(new Errors.NotFoundError("project not found")) if !project?
@@ -307,7 +307,7 @@ module.exports = ProjectEntityHandler =
return callback(error)
logger.log project_id: project_id, doc_id: doc_id, "telling docstore manager to update doc"
- DocstoreManager.updateDoc project_id, doc_id, lines, version, (err, modified, rev) ->
+ DocstoreManager.updateDoc project_id, doc_id, lines, version, ranges, (err, modified, rev) ->
if err?
logger.error err: err, doc_id: doc_id, project_id:project_id, lines: lines, "error sending doc to docstore"
return callback(err)
diff --git a/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee b/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee
index a8453f9b81..20943628ed 100644
--- a/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee
+++ b/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee
@@ -1,23 +1,21 @@
-Settings = require('settings-sharelatex')
-redis = require("redis-sharelatex")
-rclient = redis.createClient(Settings.redis.web)
+RateLimiter = require('../../infrastructure/RateLimiter')
-buildKey = (k)->
- return "LoginRateLimit:#{k}"
ONE_MIN = 60
ATTEMPT_LIMIT = 10
+
module.exports =
- processLoginRequest: (email, callback)->
- multi = rclient.multi()
- multi.incr(buildKey(email))
- multi.get(buildKey(email))
- multi.expire(buildKey(email), ONE_MIN * 2)
- multi.exec (err, results)->
- loginCount = results[1]
- allow = loginCount <= ATTEMPT_LIMIT
- callback err, allow
+
+ processLoginRequest: (email, callback) ->
+ opts =
+ endpointName: 'login'
+ throttle: ATTEMPT_LIMIT
+ timeInterval: ONE_MIN * 2
+ subjectName: email
+ RateLimiter.addCount opts, (err, shouldAllow) ->
+ callback(err, shouldAllow)
recordSuccessfulLogin: (email, callback = ->)->
- rclient.del buildKey(email), callback
\ No newline at end of file
+ RateLimiter.clearRateLimit 'login', email, callback
+
diff --git a/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee b/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee
index f486e94493..04b81581bf 100644
--- a/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee
+++ b/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee
@@ -19,12 +19,15 @@ module.exports = RateLimiterMiddlewear =
user_id = AuthenticationController.getLoggedInUserId(req) || req.ip
params = (opts.params or []).map (p) -> req.params[p]
params.push user_id
+ subjectName = params.join(":")
+ if opts.ipOnly
+ subjectName = req.ip
if !opts.endpointName?
throw new Error("no endpointName provided")
options = {
endpointName: opts.endpointName
timeInterval: opts.timeInterval or 60
- subjectName: params.join(":")
+ subjectName: subjectName
throttle: opts.maxRequests or 6
}
RateLimiter.addCount options, (error, canContinue)->
diff --git a/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee b/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee
index 005f2d23d3..b813878335 100755
--- a/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee
+++ b/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee
@@ -6,8 +6,6 @@ Project = require('../../models/Project').Project
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
Settings = require('settings-sharelatex')
util = require('util')
-redis = require("redis-sharelatex")
-rclient = redis.createClient(Settings.redis.web)
RecurlyWrapper = require('../Subscription/RecurlyWrapper')
SubscriptionHandler = require('../Subscription/SubscriptionHandler')
projectEntityHandler = require('../Project/ProjectEntityHandler')
diff --git a/services/web/app/coffee/Features/StaticPages/HomeController.coffee b/services/web/app/coffee/Features/StaticPages/HomeController.coffee
index 6675d55333..c1a8c46323 100755
--- a/services/web/app/coffee/Features/StaticPages/HomeController.coffee
+++ b/services/web/app/coffee/Features/StaticPages/HomeController.coffee
@@ -7,7 +7,7 @@ fs = require "fs"
ErrorController = require "../Errors/ErrorController"
AuthenticationController = require('../Authentication/AuthenticationController')
-homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.jade")
+homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.pug")
module.exports = HomeController =
index : (req,res)->
@@ -28,10 +28,10 @@ module.exports = HomeController =
externalPage: (page, title) ->
return (req, res, next = (error) ->) ->
- path = Path.resolve(__dirname + "/../../../views/external/#{page}.jade")
+ path = Path.resolve(__dirname + "/../../../views/external/#{page}.pug")
fs.exists path, (exists) -> # No error in this callback - old method in Node.js!
if exists
- res.render "external/#{page}.jade",
+ res.render "external/#{page}.pug",
title: title
else
ErrorController.notFound(req, res, next)
diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee
index 59a0748f36..ec29b9257a 100644
--- a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee
+++ b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee
@@ -1,20 +1,25 @@
logger = require("logger-sharelatex")
Project = require("../../models/Project").Project
-User = require("../../models/User").User
+UserGetter = require("../User/UserGetter")
SubscriptionLocator = require("./SubscriptionLocator")
Settings = require("settings-sharelatex")
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler")
module.exports =
-
allowedNumberOfCollaboratorsInProject: (project_id, callback) ->
- getOwnerOfProject project_id, (error, owner)->
+ Project.findById project_id, 'owner_ref', (error, project) =>
return callback(error) if error?
- if owner.features? and owner.features.collaborators?
- callback null, owner.features.collaborators
+ @allowedNumberOfCollaboratorsForUser project.owner_ref, callback
+
+ allowedNumberOfCollaboratorsForUser: (user_id, callback) ->
+ UserGetter.getUser user_id, {features: 1}, (error, user) ->
+ return callback(error) if error?
+ if user.features? and user.features.collaborators?
+ callback null, user.features.collaborators
else
callback null, Settings.defaultPlanCode.collaborators
+
canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) ->
@allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) =>
@@ -63,8 +68,4 @@ module.exports =
logger.log user_id:user_id, limitReached:limitReached, currentTotal: subscription.member_ids.length, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached"
callback(err, limitReached, subscription)
-getOwnerOfProject = (project_id, callback)->
- Project.findById project_id, 'owner_ref', (error, project) ->
- return callback(error) if error?
- User.findById project.owner_ref, (error, owner) ->
- callback(error, owner)
+getOwnerIdOfProject = (project_id, callback)->
diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee
index 5e20c2b515..bce0befe22 100644
--- a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee
+++ b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee
@@ -4,7 +4,7 @@ editorController = require('../Editor/EditorController')
logger = require('logger-sharelatex')
Settings = require('settings-sharelatex')
FileTypeManager = require('../Uploads/FileTypeManager')
-uuid = require('node-uuid')
+uuid = require('uuid')
fs = require('fs')
module.exports =
diff --git a/services/web/app/coffee/Features/TrackChanges/RangesManager.coffee b/services/web/app/coffee/Features/TrackChanges/RangesManager.coffee
new file mode 100644
index 0000000000..09e6b52ed1
--- /dev/null
+++ b/services/web/app/coffee/Features/TrackChanges/RangesManager.coffee
@@ -0,0 +1,23 @@
+DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
+DocstoreManager = require "../Docstore/DocstoreManager"
+UserInfoManager = require "../User/UserInfoManager"
+async = require "async"
+
+module.exports = RangesManager =
+ getAllRanges: (project_id, callback = (error, docs) ->) ->
+ DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
+ return callback(error) if error?
+ DocstoreManager.getAllRanges project_id, callback
+
+ getAllChangesUsers: (project_id, callback = (error, users) ->) ->
+ user_ids = {}
+ RangesManager.getAllRanges project_id, (error, docs) ->
+ return callback(error) if error?
+ jobs = []
+ for doc in docs
+ for change in doc.ranges?.changes or []
+ user_ids[change.metadata.user_id] = true
+
+ async.mapSeries Object.keys(user_ids), (user_id, cb) ->
+ UserInfoManager.getPersonalInfo user_id, cb
+ , callback
\ No newline at end of file
diff --git a/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee b/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee
index bc6e00a29a..d71481a7fd 100644
--- a/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee
+++ b/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee
@@ -1,20 +1,42 @@
+RangesManager = require "./RangesManager"
logger = require "logger-sharelatex"
-request = require "request"
-settings = require "settings-sharelatex"
-AuthenticationController = require "../Authentication/AuthenticationController"
+UserInfoController = require "../User/UserInfoController"
+DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
+EditorRealTimeController = require("../Editor/EditorRealTimeController")
+TrackChangesManager = require "./TrackChangesManager"
module.exports = TrackChangesController =
- proxyToTrackChangesApi: (req, res, next = (error) ->) ->
- user_id = AuthenticationController.getLoggedInUserId req
- url = settings.apis.trackchanges.url + req.url
- logger.log url: url, "proxying to track-changes api"
- getReq = request(
- url: url
- method: req.method
- headers:
- "X-User-Id": user_id
- )
- getReq.pipe(res)
- getReq.on "error", (error) ->
- logger.error err: error, "track-changes API error"
- next(error)
+ getAllRanges: (req, res, next) ->
+ project_id = req.params.project_id
+ logger.log {project_id}, "request for project ranges"
+ RangesManager.getAllRanges project_id, (error, docs = []) ->
+ return next(error) if error?
+ docs = ({id: d._id, ranges: d.ranges} for d in docs)
+ res.json docs
+
+ getAllChangesUsers: (req, res, next) ->
+ project_id = req.params.project_id
+ logger.log {project_id}, "request for project range users"
+ RangesManager.getAllChangesUsers project_id, (error, users) ->
+ return next(error) if error?
+ users = (UserInfoController.formatPersonalInfo(user) for user in users)
+ # Get rid of any anonymous/deleted user objects
+ users = users.filter (u) -> u?.id?
+ res.json users
+
+ acceptChange: (req, res, next) ->
+ {project_id, doc_id, change_id} = req.params
+ logger.log {project_id, doc_id, change_id}, "request to accept change"
+ DocumentUpdaterHandler.acceptChange project_id, doc_id, change_id, (error) ->
+ return next(error) if error?
+ EditorRealTimeController.emitToRoom project_id, "accept-change", doc_id, change_id, (err)->
+ res.send 204
+
+ toggleTrackChanges: (req, res, next) ->
+ {project_id} = req.params
+ track_changes_on = !!req.body.on
+ logger.log {project_id, track_changes_on}, "request to toggle track changes"
+ TrackChangesManager.toggleTrackChanges project_id, track_changes_on, (error) ->
+ return next(error) if error?
+ EditorRealTimeController.emitToRoom project_id, "toggle-track-changes", track_changes_on, (err)->
+ res.send 204
diff --git a/services/web/app/coffee/Features/TrackChanges/TrackChangesManager.coffee b/services/web/app/coffee/Features/TrackChanges/TrackChangesManager.coffee
index ddcfe3e44a..8eb7c10c29 100644
--- a/services/web/app/coffee/Features/TrackChanges/TrackChangesManager.coffee
+++ b/services/web/app/coffee/Features/TrackChanges/TrackChangesManager.coffee
@@ -1,28 +1,5 @@
-settings = require "settings-sharelatex"
-request = require "request"
-logger = require "logger-sharelatex"
+Project = require("../../models/Project").Project
module.exports = TrackChangesManager =
- flushProject: (project_id, callback = (error) ->) ->
- logger.log project_id: project_id, "flushing project in track-changes api"
- url = "#{settings.apis.trackchanges.url}/project/#{project_id}/flush"
- request.post url, (error, res, body) ->
- return callback(error) if error?
- if 200 <= res.statusCode < 300
- callback(null)
- else
- error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
- logger.error err: error, project_id: project_id, "error flushing project in track-changes api"
- callback(error)
-
- archiveProject: (project_id, callback = ()->)->
- logger.log project_id: project_id, "archving project in track-changes api"
- url = "#{settings.apis.trackchanges.url}/project/#{project_id}/archive"
- request.post url, (error, res, body) ->
- return callback(error) if error?
- if 200 <= res.statusCode < 300
- callback(null)
- else
- error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
- logger.error err: error, project_id: project_id, "error archving project in track-changes api"
- callback(error)
\ No newline at end of file
+ toggleTrackChanges: (project_id, track_changes_on, callback = (error) ->) ->
+ Project.update {_id: project_id}, {track_changes: track_changes_on}, callback
diff --git a/services/web/app/coffee/Features/User/UserInfoController.coffee b/services/web/app/coffee/Features/User/UserInfoController.coffee
index 92f111bda7..8054f48afe 100644
--- a/services/web/app/coffee/Features/User/UserInfoController.coffee
+++ b/services/web/app/coffee/Features/User/UserInfoController.coffee
@@ -26,17 +26,14 @@ module.exports = UserController =
UserController.sendFormattedPersonalInfo(user, res, next)
sendFormattedPersonalInfo: (user, res, next = (error) ->) ->
- UserController._formatPersonalInfo user, (error, info) ->
- return next(error) if error?
- res.send JSON.stringify(info)
+ info = UserController.formatPersonalInfo(user)
+ res.send JSON.stringify(info)
- _formatPersonalInfo: (user, callback = (error, info) ->) ->
- callback null, {
- id: user._id.toString()
- first_name: user.first_name
- last_name: user.last_name
- email: user.email
- signUpDate: user.signUpDate
- role: user.role
- institution: user.institution
- }
+ formatPersonalInfo: (user, callback = (error, info) ->) ->
+ if !user?
+ return {}
+ formatted_user = { id: user._id.toString() }
+ for key in ["first_name", "last_name", "email", "signUpDate", "role", "institution"]
+ if user[key]?
+ formatted_user[key] = user[key]
+ return formatted_user
diff --git a/services/web/app/coffee/Features/User/UserInfoManager.coffee b/services/web/app/coffee/Features/User/UserInfoManager.coffee
new file mode 100644
index 0000000000..90971e31a5
--- /dev/null
+++ b/services/web/app/coffee/Features/User/UserInfoManager.coffee
@@ -0,0 +1,5 @@
+UserGetter = require "./UserGetter"
+
+module.exports = UserInfoManager =
+ getPersonalInfo: (user_id, callback = (error) ->) ->
+ UserGetter.getUser user_id, { _id: true, first_name: true, last_name: true, email: true }, callback
\ No newline at end of file
diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee
index 78016e8a09..2cd3b17e7f 100644
--- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee
+++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee
@@ -1,5 +1,4 @@
Settings = require('settings-sharelatex')
-redis = require('redis-sharelatex')
logger = require("logger-sharelatex")
Async = require('async')
_ = require('underscore')
diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index e469df9422..8124d13d93 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -189,6 +189,7 @@ module.exports = (app, webRouter, apiRouter)->
return AuthenticationController.isUserLoggedIn(req)
res.locals.getSessionUser = ->
return AuthenticationController.getSessionUser(req)
+
next()
webRouter.use (req, res, next) ->
@@ -244,6 +245,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/coffee/infrastructure/Modules.coffee b/services/web/app/coffee/infrastructure/Modules.coffee
index 1b3c2ea9a5..e7d521fa56 100644
--- a/services/web/app/coffee/infrastructure/Modules.coffee
+++ b/services/web/app/coffee/infrastructure/Modules.coffee
@@ -1,6 +1,6 @@
fs = require "fs"
Path = require "path"
-jade = require "jade"
+pug = require "pug"
async = require "async"
MODULE_BASE_PATH = Path.resolve(__dirname + "/../../../modules")
@@ -29,7 +29,7 @@ module.exports = Modules =
for module in @modules
for view, partial of module.viewIncludes or {}
@viewIncludes[view] ||= []
- @viewIncludes[view].push jade.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade")), doctype: "html")
+ @viewIncludes[view].push pug.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".pug")), doctype: "html")
moduleIncludes: (view, locals) ->
compiledPartials = Modules.viewIncludes[view] or []
diff --git a/services/web/app/coffee/infrastructure/RateLimiter.coffee b/services/web/app/coffee/infrastructure/RateLimiter.coffee
index 7c84fc9db7..c749fa7e83 100644
--- a/services/web/app/coffee/infrastructure/RateLimiter.coffee
+++ b/services/web/app/coffee/infrastructure/RateLimiter.coffee
@@ -1,15 +1,27 @@
settings = require("settings-sharelatex")
-redis = require("redis-sharelatex")
-rclient = redis.createClient(settings.redis.web)
-redback = require("redback").use(rclient)
+RedisWrapper = require('./RedisWrapper')
+rclient = RedisWrapper.client('ratelimiter')
+RollingRateLimiter = require('rolling-rate-limiter')
-module.exports =
- addCount: (opts, callback = (opts, shouldProcess)->)->
- ratelimit = redback.createRateLimit(opts.endpointName)
- ratelimit.addCount opts.subjectName, opts.timeInterval, (err, callCount)->
- shouldProcess = callCount < opts.throttle
- callback(err, shouldProcess)
-
+module.exports = RateLimiter =
+
+ addCount: (opts, callback = (err, shouldProcess)->)->
+ namespace = "RateLimit:#{opts.endpointName}:"
+ k = "{#{opts.subjectName}}"
+ limiter = RollingRateLimiter({
+ redis: rclient,
+ namespace: namespace,
+ interval: opts.timeInterval * 1000,
+ maxInInterval: opts.throttle
+ })
+ limiter k, (err, timeLeft, actionsLeft) ->
+ if err?
+ return callback(err)
+ allowed = timeLeft == 0
+ callback(null, allowed)
+
clearRateLimit: (endpointName, subject, callback) ->
- rclient.del "#{endpointName}:#{subject}", callback
\ No newline at end of file
+ # same as the key which will be built by RollingRateLimiter (namespace+k)
+ keyName = "RateLimit:#{endpointName}:{#{subject}}"
+ rclient.del keyName, callback
diff --git a/services/web/app/coffee/infrastructure/RedisWrapper.coffee b/services/web/app/coffee/infrastructure/RedisWrapper.coffee
new file mode 100644
index 0000000000..5d8b5836b5
--- /dev/null
+++ b/services/web/app/coffee/infrastructure/RedisWrapper.coffee
@@ -0,0 +1,28 @@
+Settings = require 'settings-sharelatex'
+redis = require 'redis-sharelatex'
+ioredis = require 'ioredis'
+logger = require 'logger-sharelatex'
+
+
+# A per-feature interface to Redis,
+# looks up the feature in `settings.redis`
+# and returns an appropriate client.
+# Necessary because we don't want to migrate web over
+# to redis-cluster all at once.
+
+# TODO: consider merging into `redis-sharelatex`
+
+
+module.exports = Redis =
+
+ # feature = 'websessions' | 'ratelimiter' | ...
+ client: (feature) ->
+ redisFeatureSettings = Settings.redis[feature] or Settings.redis.web
+ if redisFeatureSettings?.cluster?
+ logger.log {feature}, "creating redis-cluster client"
+ rclient = new ioredis.Cluster(redisFeatureSettings.cluster)
+ rclient.__is_redis_cluster = true
+ else
+ logger.log {feature}, "creating redis client"
+ rclient = redis.createClient(redisFeatureSettings)
+ return rclient
diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee
index 43683bdd4e..2218b72ecb 100644
--- a/services/web/app/coffee/infrastructure/Server.coffee
+++ b/services/web/app/coffee/infrastructure/Server.coffee
@@ -7,7 +7,6 @@ crawlerLogger = require('./CrawlerLogger')
expressLocals = require('./ExpressLocals')
Router = require('../router')
metrics.inc("startup")
-redis = require("redis-sharelatex")
UserSessionsRedis = require('../Features/User/UserSessionsRedis')
sessionsRedisClient = UserSessionsRedis.client()
@@ -62,7 +61,7 @@ if Settings.behindProxy
webRouter.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge })
app.set 'views', __dirname + '/../../views'
-app.set 'view engine', 'jade'
+app.set 'view engine', 'pug'
Modules.loadViewIncludes app
diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee
index 1d53999bd9..18387bdc0b 100644
--- a/services/web/app/coffee/models/Project.coffee
+++ b/services/web/app/coffee/models/Project.coffee
@@ -32,6 +32,7 @@ ProjectSchema = new Schema
archived : { type: Boolean }
deletedDocs : [DeletedDocSchema]
imageName : { type: String }
+ track_changes : { type: Boolean }
ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
if project_or_id._id?
diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee
index f120422128..e4097aaa67 100644
--- a/services/web/app/coffee/models/User.coffee
+++ b/services/web/app/coffee/models/User.coffee
@@ -2,7 +2,7 @@ Project = require('./Project').Project
Settings = require 'settings-sharelatex'
_ = require('underscore')
mongoose = require('mongoose')
-uuid = require('node-uuid')
+uuid = require('uuid')
Schema = mongoose.Schema
ObjectId = Schema.ObjectId
@@ -37,9 +37,10 @@ UserSchema = new Schema
compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup }
templates: { type:Boolean, default: Settings.defaultFeatures.templates }
references: { type:Boolean, default: Settings.defaultFeatures.references }
+ trackChanges: { type:Boolean, default: Settings.defaultFeatures.trackChanges }
}
featureSwitches : {
- pdfng: { type: Boolean }
+ track_changes: { type: Boolean }
}
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
refered_users: [ type:ObjectId, ref:'User' ]
diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee
index 14ac3b8d22..62d5ec0865 100644
--- a/services/web/app/coffee/router.coffee
+++ b/services/web/app/coffee/router.coffee
@@ -25,7 +25,7 @@ ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")
HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
FileStoreController = require("./Features/FileStore/FileStoreController")
-TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
+HistoryController = require("./Features/History/HistoryController")
PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter")
StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter")
ChatController = require("./Features/Chat/ChatController")
@@ -40,6 +40,8 @@ AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlew
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
+TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
+CommentsController = require "./Features/Comments/CommentsController"
logger = require("logger-sharelatex")
_ = require("underscore")
@@ -171,9 +173,14 @@ module.exports = class Router
webRouter.post '/project/:Project_id/rename', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.renameProject
- webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi
- webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi
- webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi
+ webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
+ webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
+ webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
+
+ webRouter.get "/project/:project_id/ranges", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllRanges
+ webRouter.get "/project/:project_id/changes/users", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllChangesUsers
+ webRouter.post "/project/:project_id/doc/:doc_id/changes/:change_id/accept", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.acceptChange
+ webRouter.post "/project/:project_id/track_changes", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.toggleTrackChanges
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
@@ -223,8 +230,17 @@ module.exports = class Router
webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
- webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
- webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
+ webRouter.get "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
+ webRouter.post "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
+
+ # Note: Read only users can still comment
+ webRouter.post "/project/:project_id/thread/:thread_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.sendComment
+ webRouter.get "/project/:project_id/threads", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.getThreads
+ webRouter.post "/project/:project_id/thread/:thread_id/resolve", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.resolveThread
+ webRouter.post "/project/:project_id/thread/:thread_id/reopen", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.reopenThread
+ webRouter.delete "/project/:project_id/doc/:doc_id/thread/:thread_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteThread
+ webRouter.post "/project/:project_id/thread/:thread_id/messages/:message_id/edit", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.editMessage
+ webRouter.delete "/project/:project_id/thread/:thread_id/messages/:message_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteMessage
webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll
diff --git a/services/web/app/views/admin/index.jade b/services/web/app/views/admin/index.pug
similarity index 97%
rename from services/web/app/views/admin/index.jade
rename to services/web/app/views/admin/index.pug
index 0fa9017cc1..050829e9d7 100644
--- a/services/web/app/views/admin/index.jade
+++ b/services/web/app/views/admin/index.pug
@@ -28,10 +28,10 @@ block content
tab(heading="Open Sockets")
.row-spaced
ul
- -each agents, url in openSockets
+ each agents, url in openSockets
li #{url} - total : #{agents.length}
ul
- -each agent in agents
+ each agent in agents
li #{agent}
tab(heading="Close Editor")
diff --git a/services/web/app/views/admin/register.jade b/services/web/app/views/admin/register.pug
similarity index 100%
rename from services/web/app/views/admin/register.jade
rename to services/web/app/views/admin/register.pug
diff --git a/services/web/app/views/beta_program/opt_in.jade b/services/web/app/views/beta_program/opt_in.pug
similarity index 100%
rename from services/web/app/views/beta_program/opt_in.jade
rename to services/web/app/views/beta_program/opt_in.pug
diff --git a/services/web/app/views/blog/blog_holder.jade b/services/web/app/views/blog/blog_holder.pug
similarity index 100%
rename from services/web/app/views/blog/blog_holder.jade
rename to services/web/app/views/blog/blog_holder.pug
diff --git a/services/web/app/views/contact-us-modal.jade b/services/web/app/views/contact-us-modal.pug
similarity index 82%
rename from services/web/app/views/contact-us-modal.jade
rename to services/web/app/views/contact-us-modal.pug
index f4fd8938b7..d3e5aa0e87 100644
--- a/services/web/app/views/contact-us-modal.jade
+++ b/services/web/app/views/contact-us-modal.pug
@@ -24,10 +24,10 @@ script(type='text/ng-template', id='supportModalTemplate')
a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
span(ng-bind-html="suggestion.name")
i.fa.fa-angle-right
- label.desc(ng-show="'#{getUserEmail()}'.length < 1")
+ label.desc(ng-show="'"+getUserEmail()+"'.length < 1")
| #{translate("email")}
- .form-group(ng-show="'#{getUserEmail()}'.length < 1")
- input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '#{getUserEmail()}'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
+ .form-group(ng-show="'"+getUserEmail()+"'.length < 1")
+ input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '"+getUserEmail()+"'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
label#title12.desc
| #{translate("project_url")} (#{translate("optional")})
.form-group
@@ -37,6 +37,6 @@ script(type='text/ng-template', id='supportModalTemplate')
.form-group
textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='')
.form-group.text-center
- input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value='#{translate("contact_us")}')
+ input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value=translate("contact_us"))
span(ng-show="sent")
- p #{translate("request_sent_thank_you")}
\ No newline at end of file
+ p #{translate("request_sent_thank_you")}
diff --git a/services/web/app/views/general/404.jade b/services/web/app/views/general/404.pug
similarity index 100%
rename from services/web/app/views/general/404.jade
rename to services/web/app/views/general/404.pug
diff --git a/services/web/app/views/general/500.jade b/services/web/app/views/general/500.pug
similarity index 100%
rename from services/web/app/views/general/500.jade
rename to services/web/app/views/general/500.pug
diff --git a/services/web/app/views/general/closed.jade b/services/web/app/views/general/closed.pug
similarity index 91%
rename from services/web/app/views/general/closed.jade
rename to services/web/app/views/general/closed.pug
index 9f21372c81..4a27e84681 100644
--- a/services/web/app/views/general/closed.jade
+++ b/services/web/app/views/general/closed.pug
@@ -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.jade b/services/web/app/views/layout.pug
similarity index 97%
rename from services/web/app/views/layout.jade
rename to services/web/app/views/layout.pug
index 8f4d1263db..75c96ff276 100644
--- a/services/web/app/views/layout.jade
+++ b/services/web/app/views/layout.pug
@@ -21,6 +21,8 @@ html(itemscope, itemtype='http://schema.org/Product')
link(rel="icon", href="/favicon.ico")
link(rel='stylesheet', href=buildCssPath('/style.css'))
+ block _headLinks
+
if settings.i18n.subdomainLang
each subdomainDetails in settings.i18n.subdomainLang
if !subdomainDetails.hide
@@ -30,7 +32,7 @@ html(itemscope, itemtype='http://schema.org/Product')
meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor")
-if (typeof(meta) == "undefined")
- meta(itemprop="description", name="description", content='#{translate("site_description")}')
+ meta(itemprop="description", name="description", content=translate("site_description"))
-else
meta(itemprop="description", name="description" , content=meta)
diff --git a/services/web/app/views/layout/footer.jade b/services/web/app/views/layout/footer.pug
similarity index 87%
rename from services/web/app/views/layout/footer.jade
rename to services/web/app/views/layout/footer.pug
index efd64b6f6e..62a98ecfaa 100644
--- a/services/web/app/views/layout/footer.jade
+++ b/services/web/app/views/layout/footer.pug
@@ -13,9 +13,9 @@ footer.site-footer
data-toggle="dropdown",
aria-haspopup="true",
aria-expanded="false",
- tooltip="#{translate('language')}"
+ tooltip=translate('language')
)
- figure(class="sprite-icon sprite-icon-lang sprite-icon-#{currentLngCode}")
+ figure(class="sprite-icon sprite-icon-lang sprite-icon-"+currentLngCode)
ul.dropdown-menu(role="menu")
li.dropdown-header #{translate("language")}
@@ -23,7 +23,7 @@ footer.site-footer
if !subdomainDetails.hide
li.lngOption
a.menu-indent(href=subdomainDetails.url+currentUrl)
- figure(class="sprite-icon sprite-icon-lang sprite-icon-#{subdomainDetails.lngCode}")
+ figure(class="sprite-icon sprite-icon-lang sprite-icon-"+subdomainDetails.lngCode)
| #{translate(subdomainDetails.lngCode)}
//- img(src="/img/flags/24/.png")
each item in nav.left_footer
diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.pug
similarity index 60%
rename from services/web/app/views/layout/navbar.jade
rename to services/web/app/views/layout/navbar.pug
index 3cd6587592..54509d6565 100644
--- a/services/web/app/views/layout/navbar.jade
+++ b/services/web/app/views/layout/navbar.pug
@@ -4,7 +4,7 @@ nav.navbar.navbar-default
button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}")
i.fa.fa-bars
if settings.nav.custom_logo
- a(href='/', style='background-image:url("#{settings.nav.custom_logo}")').navbar-brand
+ a(href='/', style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
else if (nav.title)
a(href='/').navbar-title #{nav.title}
else
@@ -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.jade b/services/web/app/views/project/editor.pug
similarity index 98%
rename from services/web/app/views/project/editor.jade
rename to services/web/app/views/project/editor.pug
index 01e1a8b88f..54c742fd15 100644
--- a/services/web/app/views/project/editor.jade
+++ b/services/web/app/views/project/editor.pug
@@ -107,6 +107,7 @@ block requirejs
window.csrfToken = "!{csrfToken}";
window.anonymous = #{anonymous};
window.maxDocLength = #{maxDocLength};
+ window.trackChangesEnabled = #{trackChangesEnabled};
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
window.requirejs = {
"paths" : {
diff --git a/services/web/app/views/project/editor/binary-file.jade b/services/web/app/views/project/editor/binary-file.pug
similarity index 100%
rename from services/web/app/views/project/editor/binary-file.jade
rename to services/web/app/views/project/editor/binary-file.pug
diff --git a/services/web/app/views/project/editor/chat.jade b/services/web/app/views/project/editor/chat.pug
similarity index 97%
rename from services/web/app/views/project/editor/chat.jade
rename to services/web/app/views/project/editor/chat.pug
index 47a1752834..fcd47a81e3 100644
--- a/services/web/app/views/project/editor/chat.jade
+++ b/services/web/app/views/project/editor/chat.pug
@@ -50,7 +50,7 @@ aside.chat(
.new-message
textarea(
- placeholder="#{translate('your_message')}...",
+ placeholder=translate('your_message')+"...",
on-enter="sendMessage()",
ng-model="newMessageContent",
ng-click="resetUnreadMessages()"
diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.pug
similarity index 86%
rename from services/web/app/views/project/editor/editor.jade
rename to services/web/app/views/project/editor/editor.pug
index 50da35d08a..9924fe1221 100644
--- a/services/web/app/views/project/editor/editor.jade
+++ b/services/web/app/views/project/editor/editor.pug
@@ -17,7 +17,9 @@ div.full-size(
'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\
'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\
'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\
- 'rp-size-expanded': ui.reviewPanelOpen\
+ 'rp-size-expanded': ui.reviewPanelOpen,\
+ 'rp-layout-left': reviewPanel.layoutToLeft,\
+ 'rp-loading-threads': reviewPanel.loadingThreads\
}"
)
.loading-panel(ng-show="!editor.sharejs_doc || editor.opening")
@@ -51,12 +53,12 @@ div.full-size(
syntax-validation="settings.syntaxValidation",
review-panel="reviewPanel",
events-bridge="reviewPanelEventsBridge"
- track-changes-enabled="trackChangesFeatureFlag",
- track-new-changes= "reviewPanel.trackNewChanges",
- changes-tracker="reviewPanel.changesTracker",
+ track-changes-enabled="project.features.trackChangesVisible",
+ track-changes= "editor.trackChanges",
doc-id="editor.open_doc_id"
+ renderer-data="reviewPanel.rendererData"
)
-
+
include ./review-panel
.ui-layout-east
@@ -68,7 +70,7 @@ div.full-size(
ng-controller="PdfSynctexController"
)
a.btn.btn-default.btn-xs(
- tooltip="#{translate('go_to_code_location_in_pdf')}"
+ tooltip=translate('go_to_code_location_in_pdf')
tooltip-placement="right"
tooltip-append-to-body="true"
ng-click="syncToPdf()"
@@ -76,7 +78,7 @@ div.full-size(
i.fa.fa-long-arrow-right
br
a.btn.btn-default.btn-xs(
- tooltip-html="'#{translate('go_to_pdf_location_in_code')}'"
+ tooltip-html="'"+translate('go_to_pdf_location_in_code')+"'"
tooltip-placement="right"
tooltip-append-to-body="true"
ng-click="syncToCode()"
@@ -88,4 +90,4 @@ div.full-size(
ng-show="ui.view == 'pdf'"
)
include ./pdf
-
\ No newline at end of file
+
diff --git a/services/web/app/views/project/editor/feature-onboarding.jade b/services/web/app/views/project/editor/feature-onboarding.pug
similarity index 100%
rename from services/web/app/views/project/editor/feature-onboarding.jade
rename to services/web/app/views/project/editor/feature-onboarding.pug
diff --git a/services/web/app/views/project/editor/file-tree.jade b/services/web/app/views/project/editor/file-tree.pug
similarity index 97%
rename from services/web/app/views/project/editor/file-tree.jade
rename to services/web/app/views/project/editor/file-tree.pug
index 92af8b627d..03c2bd79b7 100644
--- a/services/web/app/views/project/editor/file-tree.jade
+++ b/services/web/app/views/project/editor/file-tree.pug
@@ -3,21 +3,21 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
a(
href,
ng-click="openNewDocModal()",
- tooltip-html="'#{translate('new_file').replace(' ', '
')}'",
+ tooltip-html="'"+translate('new_file').replace(' ', '
')+"'",
tooltip-placement="bottom"
)
i.fa.fa-file
a(
href,
ng-click="openNewFolderModal()",
- tooltip-html="'#{translate('new_folder').replace(' ', '
')}'",
+ tooltip-html="'"+translate('new_folder').replace(' ', '
')+"'",
tooltip-placement="bottom"
)
i.fa.fa-folder
a(
href,
ng-click="openUploadFileModal()",
- tooltip="#{translate('upload')}",
+ tooltip=translate('upload'),
tooltip-placement="bottom"
)
i.fa.fa-upload
@@ -26,7 +26,7 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
a(
href,
ng-click="startRenamingSelected()",
- tooltip="#{translate('rename')}",
+ tooltip=translate('rename'),
tooltip-placement="bottom",
ng-show="multiSelectedCount == 0"
)
@@ -34,7 +34,7 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
a(
href,
ng-click="openDeleteModalForSelected()",
- tooltip="#{translate('delete')}",
+ tooltip=translate('delete'),
tooltip-placement="bottom",
tooltip-append-to-body="true"
)
@@ -431,4 +431,4 @@ script(type='text/ng-template', id='invalidFileNameModalTemplate')
.modal-footer
button.btn.btn-default(
ng-click="$close()"
- ) #{translate('ok')}
\ No newline at end of file
+ ) #{translate('ok')}
diff --git a/services/web/app/views/project/editor/header.jade b/services/web/app/views/project/editor/header.pug
similarity index 95%
rename from services/web/app/views/project/editor/header.jade
rename to services/web/app/views/project/editor/header.pug
index d1305d28b9..85397fa83f 100644
--- a/services/web/app/views/project/editor/header.jade
+++ b/services/web/app/views/project/editor/header.pug
@@ -45,7 +45,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
ng-if="permissions.admin",
href='#',
tooltip-placement="bottom",
- tooltip="#{translate('rename')}",
+ tooltip=translate('rename'),
tooltip-append-to-body="true",
ng-click="startRenaming()",
ng-show="!state.renaming"
@@ -71,7 +71,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
span.online-user.online-user-multi(
dropdown-toggle,
- tooltip="#{translate('connected_users')}",
+ tooltip=translate('connected_users'),
tooltip-placement="left"
)
strong {{ onlineUsersArray.length }}
@@ -87,7 +87,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
a.btn.btn-full-height(
href,
- ng-if="trackChangesFeatureFlag",
+ ng-if="project.features.trackChangesVisible",
ng-class="{ active: ui.reviewPanelOpen }"
ng-click="toggleReviewPanel()"
)
@@ -121,4 +121,4 @@ header.toolbar.toolbar-header.toolbar-with-labels(
span.label.label-info(
ng-show="unreadMessages > 0"
) {{ unreadMessages }}
- p.toolbar-label #{translate("chat")}
\ No newline at end of file
+ p.toolbar-label #{translate("chat")}
diff --git a/services/web/app/views/project/editor/history.jade b/services/web/app/views/project/editor/history.pug
similarity index 100%
rename from services/web/app/views/project/editor/history.jade
rename to services/web/app/views/project/editor/history.pug
diff --git a/services/web/app/views/project/editor/hotkeys.jade b/services/web/app/views/project/editor/hotkeys.pug
similarity index 100%
rename from services/web/app/views/project/editor/hotkeys.jade
rename to services/web/app/views/project/editor/hotkeys.pug
diff --git a/services/web/app/views/project/editor/left-menu.jade b/services/web/app/views/project/editor/left-menu.pug
similarity index 96%
rename from services/web/app/views/project/editor/left-menu.jade
rename to services/web/app/views/project/editor/left-menu.pug
index 80d5c606ab..4e86b31620 100644
--- a/services/web/app/views/project/editor/left-menu.jade
+++ b/services/web/app/views/project/editor/left-menu.pug
@@ -24,7 +24,7 @@ aside#left-menu.full-size(
| PDF
div.link-disabled(
ng-if="!pdf.url"
- tooltip="#{translate('please_compile_pdf_before_download')}"
+ tooltip=translate('please_compile_pdf_before_download')
tooltip-placement="bottom"
)
i.fa.fa-file-pdf-o.fa-2x
@@ -47,7 +47,7 @@ aside#left-menu.full-size(
a(href, ng-if="pdf.url" ,ng-click="openWordCountModal()")
i.fa.fa-fw.fa-eye
span #{translate("word_count")}
- a.link-disabled(href, ng-if="!pdf.url" , tooltip="#{translate('please_compile_pdf_before_word_count')}")
+ a.link-disabled(href, ng-if="!pdf.url" , tooltip=translate('please_compile_pdf_before_word_count'))
i.fa.fa-fw.fa-eye
span.link-disabled #{translate("word_count")}
@@ -200,7 +200,7 @@ script(type='text/ng-template', id='wordCountModalTemplate')
span #{translate("loading")}...
div.pdf-disabled(
ng-if="!pdf.url"
- tooltip="#{translate('please_compile_pdf_before_word_count')}"
+ tooltip=translate('please_compile_pdf_before_word_count')
tooltip-placement="bottom"
)
div(ng-if="!status.loading")
diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.pug
similarity index 97%
rename from services/web/app/views/project/editor/pdf.jade
rename to services/web/app/views/project/editor/pdf.pug
index 074856bd7c..ba03f068a6 100644
--- a/services/web/app/views/project/editor/pdf.jade
+++ b/services/web/app/views/project/editor/pdf.pug
@@ -2,7 +2,7 @@ div.full-size.pdf(ng-controller="PdfController")
.toolbar.toolbar-tall
.btn-group(
dropdown,
- tooltip-html="'#{translate('recompile_pdf')} ({{modifierKey}} + Enter)'"
+ tooltip-html="'"+translate('recompile_pdf')+" ({{modifierKey}} + Enter)'"
tooltip-class="keyboard-tooltip"
tooltip-popup-delay="500"
tooltip-append-to-body="true"
@@ -53,7 +53,7 @@ div.full-size.pdf(ng-controller="PdfController")
href
ng-click="stop()"
ng-show="pdf.compiling",
- tooltip="#{translate('stop_compile')}"
+ tooltip=translate('stop_compile')
tooltip-placement="bottom"
)
i.fa.fa-stop()
@@ -61,7 +61,7 @@ div.full-size.pdf(ng-controller="PdfController")
href
ng-click="toggleLogs()"
ng-class="{ 'active': shouldShowLogs == true }"
- tooltip="#{translate('logs_and_output_files')}"
+ tooltip=translate('logs_and_output_files')
tooltip-placement="bottom"
)
i.fa.fa-file-text-o
@@ -77,7 +77,7 @@ div.full-size.pdf(ng-controller="PdfController")
ng-href="{{pdf.downloadUrl || pdf.url}}"
target="_blank"
ng-if="pdf.url"
- tooltip="#{translate('download_pdf')}"
+ tooltip=translate('download_pdf')
tooltip-placement="bottom"
)
i.fa.fa-download
@@ -87,7 +87,7 @@ div.full-size.pdf(ng-controller="PdfController")
href,
ng-click="switchToFlatLayout()"
ng-show="ui.pdfLayout == 'sideBySide'"
- tooltip="#{translate('full_screen')}"
+ tooltip=translate('full_screen')
tooltip-placement="bottom"
tooltip-append-to-body="true"
)
@@ -96,7 +96,7 @@ div.full-size.pdf(ng-controller="PdfController")
href,
ng-click="switchToSideBySideLayout()"
ng-show="ui.pdfLayout == 'flat'"
- tooltip="#{translate('split_screen')}"
+ tooltip=translate('split_screen')
tooltip-placement="bottom"
tooltip-append-to-body="true"
)
@@ -233,7 +233,7 @@ div.full-size.pdf(ng-controller="PdfController")
.files-dropdown-container
a.btn.btn-default.btn-sm(
href,
- tooltip="#{translate('clear_cached_files')}",
+ tooltip=translate('clear_cached_files'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="openClearCacheModal()"
diff --git a/services/web/app/views/project/editor/publish-template.jade b/services/web/app/views/project/editor/publish-template.pug
similarity index 100%
rename from services/web/app/views/project/editor/publish-template.jade
rename to services/web/app/views/project/editor/publish-template.pug
diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade
deleted file mode 100644
index dbb3a34631..0000000000
--- a/services/web/app/views/project/editor/review-panel.jade
+++ /dev/null
@@ -1,219 +0,0 @@
-#review-panel
- .review-panel-toolbar
- span.review-panel-toolbar-label(ng-click="reviewPanel.trackNewChanges = true;", ng-if="reviewPanel.trackNewChanges === false") Track Changes is
- strong off
- span.review-panel-toolbar-label(ng-click="reviewPanel.trackNewChanges = false;", ng-if="reviewPanel.trackNewChanges === true") Track Changes is
- strong on
- review-panel-toggle(ng-model="reviewPanel.trackNewChanges")
-
- .rp-entry-list(
- review-panel-sorted
- ng-if="reviewPanel.subView === SubViews.CUR_FILE"
- )
- .rp-entry-list-inner
- .rp-entry-wrapper(
- ng-repeat="(entry_id, entry) in reviewPanel.entries[editor.open_doc_id]"
- )
- div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
- change-entry(
- entry="entry"
- user="users[entry.metadata.user_id]"
- on-reject="rejectChange(entry_id);"
- on-accept="acceptChange(entry_id);"
- on-indicator-click="toggleReviewPanel();"
- )
-
- div(ng-if="entry.type === 'comment'")
- comment-entry(
- entry="entry"
- users="users"
- on-resolve="resolveComment(entry, entry_id)"
- on-unresolve="unresolveComment(entry_id)"
- on-show-thread="showThread(entry)"
- on-hide-thread="hideThread(entry)"
- on-delete="deleteComment(entry_id)"
- on-reply="submitReply(entry, entry_id);"
- on-indicator-click="toggleReviewPanel();"
- )
-
- div(ng-if="entry.type === 'add-comment'")
- add-comment-entry(
- on-start-new="startNewComment();"
- on-submit="submitNewComment(content);"
- on-cancel="cancelNewComment();"
- on-indicator-click="toggleReviewPanel();"
- )
-
- .rp-entry-list(
- ng-if="reviewPanel.subView === SubViews.OVERVIEW"
- )
- .rp-overview-file(
- ng-repeat="(doc_id, entries) in reviewPanel.entries"
- )
- .rp-overview-file-header
- | {{ getFileName(doc_id) }}
- .rp-entry-wrapper(
- ng-repeat="(entry_id, entry) in entries | orderOverviewEntries"
- )
- div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
- change-entry(
- entry="entry"
- user="users[entry.metadata.user_id]"
- on-reject="rejectChange(entry.id);"
- on-accept="acceptChange(entry.id);"
- on-indicator-click="toggleReviewPanel();"
- ng-click="gotoEntry(doc_id, entry)"
- )
-
- div(ng-if="entry.type === 'comment'")
- comment-entry(
- entry="entry"
- users="users"
- on-resolve="resolveComment(entry, entry.id)"
- on-unresolve="unresolveComment(entry.id)"
- on-delete="deleteComment(entry.id)"
- on-reply="submitReply(entry, entry_id);"
- on-indicator-click="toggleReviewPanel();"
- ng-click="gotoEntry(doc_id, entry)"
- )
-
- .rp-nav
- a.rp-nav-item(
- href
- ng-click="setSubView(SubViews.CUR_FILE);"
- ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.CUR_FILE }"
- )
- i.fa.fa-file-text-o
- span.rp-nav-label Current file
- a.rp-nav-item(
- href
- ng-click="setSubView(SubViews.OVERVIEW);"
- ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.OVERVIEW }"
- )
- i.fa.fa-list
- span.rp-nav-label Overview
-
-
-script(type='text/ng-template', id='changeEntryTemplate')
- div
- .rp-entry-callout(
- ng-class="'rp-entry-callout-' + entry.type"
- )
- .rp-entry-indicator(
- ng-switch="entry.type"
- ng-class="{ 'rp-entry-indicator-focused': entry.focused }"
- ng-click="onIndicatorClick();"
- )
- i.fa.fa-pencil(ng-switch-when="insert")
- i.rp-icon-delete(ng-switch-when="delete")
- .rp-entry(
- ng-class="[ 'rp-entry-' + entry.type, (entry.focused ? 'rp-entry-focused' : '')]"
- )
- .rp-entry-header
- .rp-entry-action-icon(ng-switch="entry.type")
- i.fa.fa-pencil(ng-switch-when="insert")
- i.rp-icon-delete(ng-switch-when="delete")
- .rp-entry-metadata
- p.rp-entry-metadata-line(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }}
- p.rp-entry-metadata-line {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }}
- .rp-avatar(style="background-color: hsl({{ user.hue }}, 70%, 50%);") {{ user.avatar_text | limitTo : 1 }}
- .rp-entry-body(ng-switch="entry.type")
- span(ng-switch-when="insert") Added
- ins.rp-content-highlight {{ entry.content }}
- span(ng-switch-when="delete") Deleted
- del.rp-content-highlight {{ entry.content }}
- .rp-entry-actions
- a.rp-entry-button(href, ng-click="onReject();")
- i.fa.fa-times
- | Reject
- a.rp-entry-button(href, ng-click="onAccept();")
- i.fa.fa-check
- | Accept
-
-script(type='text/ng-template', id='commentEntryTemplate')
- div
- .rp-entry-callout.rp-entry-callout-comment(ng-if="!entry.resolved")
- .rp-entry-indicator(
- ng-class="{ 'rp-entry-indicator-focused': entry.focused }"
- ng-click="onIndicatorClick();"
- )
- i.fa.fa-comment
- .rp-entry.rp-entry-comment(
- ng-class="{ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolved': entry.resolved}"
- )
- .rp-comment(
- ng-if="!entry.resolved || entry.showWhenResolved"
- ng-repeat="comment in entry.thread"
- ng-class="users[comment.user_id].isSelf ? 'rp-comment-self' : '';"
- )
- .rp-avatar(
- ng-if="!users[comment.user_id].isSelf;"
- style="background-color: hsl({{ users[comment.user_id].hue }}, 70%, 50%);"
- ) {{ users[comment.user_id].avatar_text | limitTo : 1 }}
- .rp-comment-body(style="color: hsl({{ users[comment.user_id].hue }}, 70%, 90%);")
- p.rp-comment-content {{ comment.content }}
- p.rp-comment-metadata
- | {{ comment.ts | date : 'MMM d, y h:mm a' }}
- | •
- span(style="color: hsl({{ users[comment.user_id].hue }}, 70%, 40%);") {{ users[comment.user_id].name }}
- .rp-comment-reply(ng-if="!entry.resolved || entry.showWhenResolved")
- textarea.rp-comment-input(
- ng-model="entry.replyContent"
- ng-keypress="handleCommentReplyKeyPress($event);"
- stop-propagation="click"
- placeholder="{{ 'Hit \"Enter\" to reply' + (entry.resolved ? ' and re-open' : '') }}"
- )
- .rp-comment-resolved-description(ng-if="entry.resolved && !entry.showWhenResolved")
- div
- | Comment resolved by
- span(style="color: hsl({{ users[entry.resolved_data.user_id].hue }}, 70%, 40%);") {{ users[entry.resolved_data.user_id].name }}
- div {{ entry.resolved_data.ts | date : 'MMM d, y h:mm a' }}
- .rp-entry-actions
- a.rp-entry-button(href, ng-click="onResolve();", ng-if="!entry.resolved")
- i.fa.fa-check
- | Mark as resolved
- a.rp-entry-button(href, ng-click="onShowThread();", ng-if="entry.resolved && !entry.showWhenResolved")
- | Show
- a.rp-entry-button(href, ng-click="onHideThread();", ng-if="entry.resolved && entry.showWhenResolved")
- | Hide
- a.rp-entry-button(href, ng-click="onUnresolve();", ng-if="entry.resolved")
- | Re-open
- a.rp-entry-button(href, ng-click="onDelete();", ng-if="entry.resolved")
- | Delete
-
-
-script(type='text/ng-template', id='addCommentEntryTemplate')
- div
- .rp-entry-callout.rp-entry-callout-add-comment
- .rp-entry-indicator(
- ng-if="!commentState.adding"
- ng-click="startNewComment(); onIndicatorClick();"
- tooltip="Add a comment"
- tooltip-placement="right"
- tooltip-append-to-body="true"
- )
- i.fa.fa-commenting
- .rp-entry.rp-entry-add-comment(
- ng-class="[ (state.isAdding ? 'rp-entry-adding-comment' : ''), (entry.focused ? 'rp-entry-focused' : '')]"
- )
- a.rp-add-comment-btn(
- href
- ng-if="!state.isAdding"
- ng-click="startNewComment();"
- )
- i.fa.fa-comment
- | Add comment
- div(ng-if="state.isAdding")
- .rp-new-comment
- textarea.rp-comment-input(
- ng-model="state.content"
- ng-keypress="handleCommentKeyPress($event);"
- placeholder="Add your comment here"
- )
- .rp-entry-actions
- a.rp-entry-button(href, ng-click="cancelNewComment();")
- i.fa.fa-times
- | Cancel
- a.rp-entry-button(href, ng-click="submitNewComment()")
- i.fa.fa-comment
- | Comment
\ No newline at end of file
diff --git a/services/web/app/views/project/editor/review-panel.pug b/services/web/app/views/project/editor/review-panel.pug
new file mode 100644
index 0000000000..25ac9afd79
--- /dev/null
+++ b/services/web/app/views/project/editor/review-panel.pug
@@ -0,0 +1,426 @@
+#review-panel
+ a.rp-track-changes-indicator(
+ href
+ ng-if="editor.wantTrackChanges"
+ ng-click="toggleReviewPanel();"
+ ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }"
+ ) Track changes is
+ strong on
+
+ .review-panel-toolbar
+ resolved-comments-dropdown(
+ class="rp-flex-block"
+ entries="reviewPanel.resolvedComments"
+ threads="reviewPanel.commentThreads"
+ resolved-ids="reviewPanel.resolvedThreadIds"
+ docs="docs"
+ on-open="refreshResolvedCommentsDropdown();"
+ on-unresolve="unresolveComment(threadId);"
+ on-delete="deleteThread(entryId, docId, threadId);"
+ is-loading="reviewPanel.dropdown.loading"
+ permissions="permissions"
+ )
+ span.review-panel-toolbar-label(ng-if="permissions.write")
+ span(ng-click="toggleTrackChanges(true)", ng-if="editor.wantTrackChanges === false") Track Changes is
+ strong off
+ span(ng-click="toggleTrackChanges(false)", ng-if="editor.wantTrackChanges === true") Track Changes is
+ strong on
+ review-panel-toggle(
+ ng-if="editor.wantTrackChanges == editor.trackChanges"
+ ng-model="editor.wantTrackChanges"
+ on-toggle="toggleTrackChanges"
+ disabled="!project.features.trackChanges"
+ on-disabled-click="openTrackChangesUpgradeModal"
+ )
+ span.review-panel-toolbar-label.review-panel-toolbar-label-disabled(ng-if="!permissions.write")
+ span(ng-if="editor.wantTrackChanges === false") Track Changes is
+ strong off
+ span(ng-if="editor.wantTrackChanges === true") Track Changes is
+ strong on
+ span.review-panel-toolbar-spinner(ng-if="editor.wantTrackChanges != editor.trackChanges")
+ i.fa.fa-spin.fa-spinner
+
+ .rp-entry-list(
+ review-panel-sorted
+ ng-if="reviewPanel.subView === SubViews.CUR_FILE"
+ )
+ .rp-entry-list-inner
+ .rp-entry-wrapper(
+ ng-repeat="(entry_id, entry) in reviewPanel.entries[editor.open_doc_id]"
+ )
+ div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
+ change-entry(
+ entry="entry"
+ user="users[entry.metadata.user_id]"
+ on-reject="rejectChange(entry_id);"
+ on-accept="acceptChange(entry_id);"
+ on-indicator-click="toggleReviewPanel();"
+ on-body-click="gotoEntry(editor.open_doc_id, entry)"
+ permissions="permissions"
+ )
+
+ div(ng-if="entry.type === 'comment'")
+ comment-entry(
+ entry="entry"
+ threads="reviewPanel.commentThreads"
+ on-resolve="resolveComment(entry, entry_id)"
+ on-reply="submitReply(entry, entry_id);"
+ on-indicator-click="toggleReviewPanel();"
+ on-save-edit="saveEdit(entry.thread_id, comment)"
+ on-delete="deleteComment(entry.thread_id, comment)"
+ on-body-click="gotoEntry(editor.open_doc_id, entry)"
+ permissions="permissions"
+ ng-if="!reviewPanel.loadingThreads"
+ )
+
+ div(ng-if="entry.type === 'add-comment' && permissions.comment")
+ add-comment-entry(
+ on-start-new="startNewComment();"
+ on-submit="submitNewComment(content);"
+ on-cancel="cancelNewComment();"
+ on-indicator-click="toggleReviewPanel();"
+ )
+
+ .rp-entry-list(
+ ng-if="reviewPanel.subView === SubViews.OVERVIEW"
+ )
+ .rp-loading(ng-if="reviewPanel.overview.loading")
+ i.fa.fa-spinner.fa-spin
+ .rp-overview-file(
+ ng-repeat="doc in docs"
+ ng-if="!reviewPanel.overview.loading"
+ )
+ .rp-overview-file-header(
+ ng-if="reviewPanel.entries[doc.doc.id] | notEmpty"
+ )
+ | {{ doc.path }}
+ .rp-entry-wrapper(
+ ng-repeat="(entry_id, entry) in reviewPanel.entries[doc.doc.id] | orderOverviewEntries"
+ ng-if="!(entry.type === 'comment' && reviewPanel.commentThreads[entry.thread_id].resolved === true)"
+ )
+ div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
+ change-entry(
+ entry="entry"
+ user="users[entry.metadata.user_id]"
+ on-indicator-click="toggleReviewPanel();"
+ ng-click="gotoEntry(doc.doc.id, entry)"
+ permissions="permissions"
+ )
+
+ div(ng-if="entry.type === 'comment'")
+ comment-entry(
+ entry="entry"
+ threads="reviewPanel.commentThreads"
+ on-reply="submitReply(entry, entry_id);"
+ on-save-edit="saveEdit(entry.thread_id, comment)"
+ on-delete="deleteComment(entry.thread_id, comment)"
+ on-indicator-click="toggleReviewPanel();"
+ ng-click="gotoEntry(doc.doc.id, entry)"
+ permissions="permissions"
+ )
+
+ .rp-nav
+ a.rp-nav-item(
+ href
+ ng-click="setSubView(SubViews.CUR_FILE);"
+ ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.CUR_FILE }"
+ )
+ i.fa.fa-file-text-o
+ span.rp-nav-label Current file
+ a.rp-nav-item(
+ href
+ ng-click="setSubView(SubViews.OVERVIEW);"
+ ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.OVERVIEW }"
+ )
+ i.fa.fa-list
+ span.rp-nav-label Overview
+
+
+script(type='text/ng-template', id='changeEntryTemplate')
+ div
+ .rp-entry-callout(
+ ng-class="'rp-entry-callout-' + entry.type"
+ )
+ .rp-entry-indicator(
+ ng-switch="entry.type"
+ ng-class="{ 'rp-entry-indicator-focused': entry.focused }"
+ ng-click="onIndicatorClick();"
+ )
+ i.fa.fa-pencil(ng-switch-when="insert")
+ i.rp-icon-delete(ng-switch-when="delete")
+ .rp-entry(
+ ng-class="[ 'rp-entry-' + entry.type, (entry.focused ? 'rp-entry-focused' : '')]"
+ )
+ .rp-entry-body
+ .rp-entry-action-icon(ng-switch="entry.type")
+ i.fa.fa-pencil(ng-switch-when="insert")
+ i.rp-icon-delete(ng-switch-when="delete")
+ .rp-entry-details
+ .rp-entry-description(ng-switch="entry.type")
+ span(ng-switch-when="insert") Added
+ ins.rp-content-highlight {{ entry.content | limitTo:(isCollapsed ? contentLimit : entry.content.length) }}
+ a.rp-collapse-toggle(
+ href
+ ng-if="needsCollapsing"
+ ng-click="toggleCollapse();"
+ ) {{ isCollapsed ? '... (show all)' : ' (show less)' }}
+ span(ng-switch-when="delete") Deleted
+ del.rp-content-highlight {{ entry.content | limitTo:(isCollapsed ? contentLimit : entry.content.length) }}
+ a.rp-collapse-toggle(
+ href
+ ng-if="needsCollapsing"
+ ng-click="toggleCollapse();"
+ ) {{ isCollapsed ? '... (show all)' : ' (show less)' }}
+ .rp-entry-metadata
+ | {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} •
+ span.rp-entry-user(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }}
+ .rp-entry-actions(ng-if="permissions.write")
+ a.rp-entry-button(href, ng-click="onReject();")
+ i.fa.fa-times
+ | Reject
+ a.rp-entry-button(href, ng-click="onAccept();")
+ i.fa.fa-check
+ | Accept
+
+script(type='text/ng-template', id='commentEntryTemplate')
+ .rp-comment-wrapper(
+ ng-class="{ 'rp-comment-wrapper-resolving': state.animating }"
+ )
+ .rp-entry-callout.rp-entry-callout-comment
+ .rp-entry-indicator(
+ ng-class="{ 'rp-entry-indicator-focused': entry.focused }"
+ ng-click="onIndicatorClick();"
+ )
+ i.fa.fa-comment
+ .rp-entry.rp-entry-comment(
+ ng-class="{ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolving': state.animating }"
+ )
+
+ .rp-loading(ng-if="!threads[entry.thread_id].submitting && (!threads[entry.thread_id] || threads[entry.thread_id].messages.length == 0)")
+ | No comments
+ .rp-comment-loaded
+ .rp-comment(
+ ng-repeat="comment in threads[entry.thread_id].messages track by comment.id"
+ )
+ p.rp-comment-content
+ span(ng-if="!comment.editing")
+ span.rp-entry-user(
+ style="color: hsl({{ comment.user.hue }}, 70%, 40%);",
+ ) {{ comment.user.name }}:
+ span(ng-bind-html="comment.content | linky:'_blank'")
+ textarea.rp-comment-input(
+ expandable-text-area
+ ng-if="comment.editing"
+ ng-model="comment.content"
+ ng-keypress="saveEditOnEnter($event, comment);"
+ ng-blur="saveEdit(comment)"
+ autofocus
+ stop-propagation="click"
+ )
+ .rp-entry-metadata(ng-if="!comment.editing")
+ span(ng-if="!comment.deleting") {{ comment.timestamp | date : 'MMM d, y h:mm a' }}
+ span.rp-comment-actions(ng-if="comment.user.isSelf && !comment.deleting")
+ | •
+ a(href, ng-click="startEditing(comment)") Edit
+ span(ng-if="threads[entry.thread_id].messages.length > 1")
+ | •
+ a(href, ng-click="confirmDelete(comment)") Delete
+ span.rp-confim-delete(ng-if="comment.user.isSelf && comment.deleting")
+ | Are you sure?
+ | •
+ a(href, ng-click="doDelete(comment)") Delete
+ | •
+ a(href, ng-click="cancelDelete(comment)") Cancel
+
+ .rp-loading(ng-if="threads[entry.thread_id].submitting")
+ i.fa.fa-spinner.fa-spin
+ .rp-comment-reply(ng-if="permissions.comment")
+ textarea.rp-comment-input(
+ expandable-text-area
+ ng-model="entry.replyContent"
+ ng-keypress="handleCommentReplyKeyPress($event);"
+ stop-propagation="click"
+ placeholder="{{ 'Hit \"Enter\" to reply' + (entry.resolved ? ' and re-open' : '') }}"
+ )
+ .rp-entry-actions
+ button.rp-entry-button(
+ ng-click="animateAndCallOnResolve();"
+ ng-if="permissions.comment && permissions.write"
+ )
+ i.fa.fa-inbox
+ | Resolve
+ button.rp-entry-button(
+ ng-click="onReply();"
+ ng-if="permissions.comment"
+ ng-disabled="!entry.replyContent.length"
+ )
+ i.fa.fa-reply
+ | Reply
+
+script(type='text/ng-template', id='resolvedCommentEntryTemplate')
+ .rp-resolved-comment
+ div
+ .rp-resolved-comment-context
+ | Quoted text on
+ span.rp-resolved-comment-context-file {{ thread.docName }}
+ p.rp-resolved-comment-context-quote
+ span {{ thread.content | limitTo:(isCollapsed ? contentLimit : thread.content.length)}}
+ a.rp-collapse-toggle(
+ href
+ ng-if="needsCollapsing"
+ ng-click="toggleCollapse();"
+ ) {{ isCollapsed ? '... (show all)' : ' (show less)' }}
+ .rp-comment(
+ ng-repeat="comment in thread.messages track by comment.id"
+ )
+ p.rp-comment-content
+ span.rp-entry-user(
+ style="color: hsl({{ comment.user.hue }}, 70%, 40%);"
+ ng-if="$first || comment.user.id !== thread.messages[$index - 1].user.id"
+ ) {{ comment.user.name }}:
+ span(ng-bind-html="comment.content | linky:'_blank'")
+ .rp-entry-metadata
+ | {{ comment.timestamp | date : 'MMM d, y h:mm a' }}
+ .rp-comment.rp-comment-resolver
+ p.rp-comment-resolver-content
+ span.rp-entry-user(
+ style="color: hsl({{ thread.resolved_by_user.hue }}, 70%, 40%);"
+ ) {{ thread.resolved_by_user.name }}:
+ | Marked as resolved.
+ .rp-entry-metadata
+ | {{ thread.resolved_at | date : 'MMM d, y h:mm a' }}
+
+ .rp-entry-actions(ng-if="permissions.comment && permissions.write")
+ a.rp-entry-button(
+ href
+ ng-click="onUnresolve({ 'threadId': thread.threadId });"
+ )
+ | Re-open
+ a.rp-entry-button(
+ href
+ ng-click="onDelete({ 'entryId': thread.entryId, 'docId': thread.docId, 'threadId': thread.threadId });"
+ )
+ | Delete
+
+
+script(type='text/ng-template', id='addCommentEntryTemplate')
+ div
+ .rp-entry-callout.rp-entry-callout-add-comment
+ .rp-entry-indicator(
+ ng-if="!commentState.adding"
+ ng-click="startNewComment(); onIndicatorClick();"
+ tooltip="Add a comment"
+ tooltip-placement="right"
+ tooltip-append-to-body="true"
+ )
+ i.fa.fa-commenting
+ .rp-entry.rp-entry-add-comment(
+ ng-class="[ (state.isAdding ? 'rp-entry-adding-comment' : ''), (entry.focused ? 'rp-entry-focused' : '')]"
+ )
+ a.rp-add-comment-btn(
+ href
+ ng-if="!state.isAdding"
+ ng-click="startNewComment();"
+ )
+ i.fa.fa-comment
+ | Add comment
+ div(ng-if="state.isAdding")
+ .rp-new-comment
+ textarea.rp-comment-input(
+ expandable-text-area
+ ng-model="state.content"
+ ng-keypress="handleCommentKeyPress($event);"
+ placeholder="Add your comment here"
+ focus-on="comment:new:open"
+ )
+ .rp-entry-actions
+ button.rp-entry-button(
+ ng-click="cancelNewComment();"
+ )
+ i.fa.fa-times
+ | Cancel
+ button.rp-entry-button(
+ ng-click="submitNewComment()"
+ ng-disabled="!state.content.length"
+ )
+ i.fa.fa-comment
+ | Comment
+
+script(type='text/ng-template', id='resolvedCommentsDropdownTemplate')
+ .resolved-comments
+ .resolved-comments-backdrop(
+ ng-class="{ 'resolved-comments-backdrop-visible' : state.isOpen }"
+ ng-click="state.isOpen = false"
+ )
+ a.resolved-comments-toggle(
+ href
+ ng-click="toggleOpenState();"
+ tooltip="Resolved Comments"
+ tooltip-placement="bottom"
+ tooltip-append-to-body="true"
+ )
+ i.fa.fa-inbox
+ .resolved-comments-dropdown(
+ ng-class="{ 'resolved-comments-dropdown-open' : state.isOpen }"
+ )
+ .rp-loading(ng-if="isLoading")
+ i.fa.fa-spinner.fa-spin
+ .resolved-comments-scroller(
+ ng-if="!isLoading"
+ )
+ resolved-comment-entry(
+ ng-repeat="thread in resolvedComments | orderBy:'resolved_at':true"
+ thread="thread"
+ on-unresolve="handleUnresolve(threadId);"
+ on-delete="handleDelete(entryId, docId, threadId);"
+ permissions="permissions"
+ )
+ .rp-loading(ng-if="!resolvedComments.length")
+ | No resolved threads.
+
+script(type="text/ng-template", id="trackChangesUpgradeModalTemplate")
+ .modal-header
+ button.close(
+ type="button"
+ data-dismiss="modal"
+ ng-click="cancel()"
+ ) ×
+ h3 Upgrade to Track Changes
+ .modal-body
+ .teaser-video-container
+ video.teaser-video(autoplay, loop)
+ source(src="/img/teasers/track-changes/teaser-track-changes.mp4", type="video/mp4")
+ img(src="/img/teasers/track-changes/teaser-track-changes.gif")
+
+ h4.teaser-title See changes in your documents, live
+
+ p.small(ng-show="startedFreeTrial")
+ | #{translate("refresh_page_after_starting_free_trial")}
+
+ .row
+ .col-md-10.col-md-offset-1
+ ul.list-unstyled
+ li
+ i.fa.fa-check
+ | Track any change, in real-time
+
+ li
+ i.fa.fa-check
+ | Review your peers' work
+
+ li
+ i.fa.fa-check
+ | Accept or reject each change individually
+
+
+ .row.text-center(ng-controller="FreeTrialModalController")
+ a.btn.btn-success(
+ href
+ ng-click="startFreeTrial('track-changes')"
+ ) Try it for free
+
+ .modal-footer()
+ button.btn.btn-default(
+ ng-click="cancel()"
+ )
+ span #{translate("close")}
\ No newline at end of file
diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.pug
similarity index 95%
rename from services/web/app/views/project/editor/share.jade
rename to services/web/app/views/project/editor/share.pug
index 62de414064..32e0404afc 100644
--- a/services/web/app/views/project/editor/share.jade
+++ b/services/web/app/views/project/editor/share.pug
@@ -38,7 +38,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
.col-xs-1
a(
href
- tooltip="#{translate('remove_collaborator')}"
+ tooltip=translate('remove_collaborator')
tooltip-placement="bottom"
ng-click="removeMember(member)"
)
@@ -55,7 +55,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
.col-xs-1
a(
href
- tooltip="#{translate('revoke_invite')}"
+ tooltip=translate('revoke_invite')
tooltip-placement="bottom"
ng-click="revokeInvite(invite)"
)
@@ -66,7 +66,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
.form-group
tags-input(
template="shareTagTemplate"
- placeholder="#{settings.customisation.shareProjectPlaceholder || 'joe@example.com, sue@example.com, ...'}"
+ placeholder=settings.customisation.shareProjectPlaceholder || 'joe@example.com, sue@example.com, ...'
ng-model="inputs.contacts"
focus-on="open"
display-property="display"
@@ -144,6 +144,8 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
span(ng-switch="state.errorReason")
span(ng-switch-when="cannot_invite_non_user")
| #{translate("cannot_invite_non_user")}
+ span(ng-switch-when="cannot_invite_self")
+ | #{translate("cannot_invite_self")}
span(ng-switch-default)
| #{translate("generic_something_went_wrong")}
button.btn.btn-default(
diff --git a/services/web/app/views/project/invite/not-valid.jade b/services/web/app/views/project/invite/not-valid.pug
similarity index 100%
rename from services/web/app/views/project/invite/not-valid.jade
rename to services/web/app/views/project/invite/not-valid.pug
diff --git a/services/web/app/views/project/invite/show.jade b/services/web/app/views/project/invite/show.pug
similarity index 83%
rename from services/web/app/views/project/invite/show.jade
rename to services/web/app/views/project/invite/show.pug
index eed30d3d19..d129fe015d 100644
--- a/services/web/app/views/project/invite/show.jade
+++ b/services/web/app/views/project/invite/show.pug
@@ -20,12 +20,12 @@ block content
form.form(
name="acceptForm",
method="POST",
- action="/project/#{invite.projectId}/invite/token/#{invite.token}/accept"
+ action="/project/"+invite.projectId+"/invite/token/"+invite.token+"/accept"
)
input(name='_csrf', type='hidden', value=csrfToken)
- input(name='token', type='hidden', value="#{invite.token}")
+ input(name='token', type='hidden', value=invite.token)
.form-group.text-center
button.btn.btn-lg.btn-primary(type="submit")
| #{translate("join_project")}
.form-group.text-center
-
\ No newline at end of file
+
diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.pug
similarity index 98%
rename from services/web/app/views/project/list.jade
rename to services/web/app/views/project/list.pug
index d9fbf0e6b1..f707cd9411 100644
--- a/services/web/app/views/project/list.jade
+++ b/services/web/app/views/project/list.pug
@@ -73,4 +73,4 @@ block content
.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8
include ./list/empty-project-list
- include ./list/modals
\ No newline at end of file
+ include ./list/modals
diff --git a/services/web/app/views/project/list/empty-project-list.jade b/services/web/app/views/project/list/empty-project-list.pug
similarity index 100%
rename from services/web/app/views/project/list/empty-project-list.jade
rename to services/web/app/views/project/list/empty-project-list.pug
diff --git a/services/web/app/views/project/list/modals.jade b/services/web/app/views/project/list/modals.pug
similarity index 100%
rename from services/web/app/views/project/list/modals.jade
rename to services/web/app/views/project/list/modals.pug
diff --git a/services/web/app/views/project/list/notifications.jade b/services/web/app/views/project/list/notifications.pug
similarity index 100%
rename from services/web/app/views/project/list/notifications.jade
rename to services/web/app/views/project/list/notifications.pug
diff --git a/services/web/app/views/project/list/project-list.jade b/services/web/app/views/project/list/project-list.pug
similarity index 96%
rename from services/web/app/views/project/list/project-list.jade
rename to services/web/app/views/project/list/project-list.pug
index 01007213d0..0bb93e8336 100644
--- a/services/web/app/views/project/list/project-list.jade
+++ b/services/web/app/views/project/list/project-list.pug
@@ -6,7 +6,7 @@
form.project-search.form-horizontal(role="form")
.form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12
input.form-control.col-md-7.col-xs-12(
- placeholder="#{translate('search_projects')}…",
+ placeholder=translate('search_projects')+"…",
autofocus='autofocus',
ng-model="searchText.value",
focus-on='search:clear',
@@ -25,7 +25,7 @@
.btn-group(ng-hide="selectedProjects.length < 1")
a.btn.btn-default(
href,
- tooltip="#{translate('download')}",
+ tooltip=translate('download'),
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="downloadSelectedProjects()"
@@ -33,7 +33,7 @@
i.fa.fa-cloud-download
a.btn.btn-default(
href,
- tooltip="#{translate('delete')}",
+ tooltip=translate('delete'),
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="openArchiveProjectsModal()"
@@ -45,7 +45,7 @@
href,
data-toggle="dropdown",
dropdown-toggle,
- tooltip="#{translate('add_to_folders')}",
+ tooltip=translate('add_to_folders'),
tooltip-append-to-body="true",
tooltip-placement="bottom"
)
diff --git a/services/web/app/views/project/list/side-bar.jade b/services/web/app/views/project/list/side-bar.pug
similarity index 100%
rename from services/web/app/views/project/list/side-bar.jade
rename to services/web/app/views/project/list/side-bar.pug
diff --git a/services/web/app/views/referal/bonus.jade b/services/web/app/views/referal/bonus.pug
similarity index 87%
rename from services/web/app/views/referal/bonus.jade
rename to services/web/app/views/referal/bonus.pug
index 5b0183d9c1..f3a65c6bc4 100644
--- a/services/web/app/views/referal/bonus.jade
+++ b/services/web/app/views/referal/bonus.pug
@@ -24,7 +24,7 @@ block content
.row
.col-md-8.col-md-offset-2.bonus-banner
.title
- a(href='https://twitter.com/share?text=is%20trying%20out%20the%20online%20LaTeX%20Editor%20ShareLaTeX&url=#{encodeURIComponent(buildReferalUrl("t"))}&counturl=https://www.sharelatex.com', target="_blank").twitter Tweet
+ a(href='https://twitter.com/share?text=is%20trying%20out%20the%20online%20LaTeX%20Editor%20ShareLaTeX&url='+encodeURIComponent(buildReferalUrl("t"))+'&counturl=https://www.sharelatex.com', target="_blank").twitter Tweet
.row
.col-md-8.col-md-offset-2.bonus-banner
@@ -34,12 +34,12 @@ block content
.row
.col-md-8.col-md-offset-2.bonus-banner
.title
- a(href="https://plus.google.com/share?url=#{encodeURIComponent(buildReferalUrl('gp'))}", onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;").google-plus #{translate("share_us_on_googleplus")}
+ a(href="https://plus.google.com/share?url="+encodeURIComponent(buildReferalUrl('gp')), onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;").google-plus #{translate("share_us_on_googleplus")}
.row
.col-md-8.col-md-offset-2.bonus-banner
.title
- a(href='mailto:?subject=Online LaTeX editor you may like &body=Hey, I have been using the online LaTeX editor ShareLaTeX recently and thought you might like to check it out. #{encodeURIComponent(buildReferalUrl("e"))}', title='Share by Email').email #{translate("email_us_to_your_friends")}
+ a(href='mailto:?subject=Online LaTeX editor you may like &body=Hey, I have been using the online LaTeX editor ShareLaTeX recently and thought you might like to check it out. '+encodeURIComponent(buildReferalUrl("e")), title='Share by Email').email #{translate("email_us_to_your_friends")}
.row
.col-md-8.col-md-offset-2.bonus-banner
@@ -58,9 +58,9 @@ block content
.col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 30px; margin-top: 20px;")
- for (var i = 0; i <= 10; i++) {
- if (refered_user_count == i)
- .number(style="left: #{i}0%").active #{i}
+ .number(style="left: "+i+"0%").active #{i}
- else
- .number(style="left: #{i}0%") #{i}
+ .number(style="left: "+i+"0%") #{i}
- }
.row.ab-bonus
@@ -68,7 +68,7 @@ block content
.progress
- if (refered_user_count == 0)
div(style="text-align: center; padding: 4px;") #{translate("spread_the_word_and_fill_bar")}
- .progress-bar.progress-bar-info(style="width: #{refered_user_count}0%")
+ .progress-bar.progress-bar-info(style="width: "+refered_user_count+"0%")
.row.ab-bonus
.col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 70px;")
diff --git a/services/web/app/views/restore.jade b/services/web/app/views/restore.pug
similarity index 84%
rename from services/web/app/views/restore.jade
rename to services/web/app/views/restore.pug
index 51c0ee03fc..1cd7ad458a 100644
--- a/services/web/app/views/restore.jade
+++ b/services/web/app/views/restore.pug
@@ -19,11 +19,11 @@ block content
.row-fluid
table.table
- -each project in projects
+ each project in projects
tr
- project_id = project._id.toString()
td(width="50%") #{project.name}
td(width="25%")
- a.btn(href="/project/#{project_id}/zip") Download latest version as Zip
+ a.btn(href="/project/"+project_id+"/zip") Download latest version as Zip
include general/small-footer
diff --git a/services/web/app/views/scribtex-modal.jade b/services/web/app/views/scribtex-modal.pug
similarity index 71%
rename from services/web/app/views/scribtex-modal.jade
rename to services/web/app/views/scribtex-modal.pug
index 1d4e2ce005..3efbf19314 100644
--- a/services/web/app/views/scribtex-modal.jade
+++ b/services/web/app/views/scribtex-modal.pug
@@ -5,4 +5,4 @@ script(type='text/ng-template', id='scribtexModalTemplate')
p ScribTeX has moved to https://scribtex.sharelatex.com. Please update your bookmarks.
p(style="text-align: center") You can find the page you were looking for here:
p(style="text-align: center")
- a(href="https://scribtex.sharelatex.com#{scribtexPath}", style="font-size: 16px") https://scribtex.sharelatex.com#{scribtexPath}
\ No newline at end of file
+ a(href="https://scribtex.sharelatex.com"+scribtexPath, style="font-size: 16px") https://scribtex.sharelatex.com#{scribtexPath}
diff --git a/services/web/app/views/sentry.jade b/services/web/app/views/sentry.pug
similarity index 96%
rename from services/web/app/views/sentry.jade
rename to services/web/app/views/sentry.pug
index 0a51686015..9e10d7837e 100644
--- a/services/web/app/views/sentry.jade
+++ b/services/web/app/views/sentry.pug
@@ -1,8 +1,8 @@
- if (typeof(sentrySrc) != "undefined")
- if (sentrySrc.match(/^([a-z]+:)?\/\//i))
- script(src="#{sentrySrc}")
+ script(src=sentrySrc)
- else
- script(src=buildJsPath("libs/#{sentrySrc}", {fingerprint:false}))
+ script(src=buildJsPath("libs/"+sentrySrc, {fingerprint:false}))
- if (typeof(sentrySrc) != "undefined")
script(type="text/javascript").
if (typeof(Raven) != "undefined" && Raven.config) {
diff --git a/services/web/app/views/subscriptions/custom_account.jade b/services/web/app/views/subscriptions/custom_account.pug
similarity index 100%
rename from services/web/app/views/subscriptions/custom_account.jade
rename to services/web/app/views/subscriptions/custom_account.pug
diff --git a/services/web/app/views/subscriptions/dashboard.jade b/services/web/app/views/subscriptions/dashboard.pug
similarity index 91%
rename from services/web/app/views/subscriptions/dashboard.jade
rename to services/web/app/views/subscriptions/dashboard.pug
index 3448dc9072..fb4f834c34 100644
--- a/services/web/app/views/subscriptions/dashboard.jade
+++ b/services/web/app/views/subscriptions/dashboard.pug
@@ -12,7 +12,7 @@ block scripts
mixin printPlan(plan)
-if (!plan.hideFromUsers)
- tr(ng-controller="ChangePlanFormController", ng-init="plan=#{JSON.stringify(plan)}", ng-show="shouldShowPlan(plan.planCode)")
+ tr(ng-controller="ChangePlanFormController", ng-init="plan="+JSON.stringify(plan), ng-show="shouldShowPlan(plan.planCode)")
td
strong #{plan.name}
td {{refreshPrice(plan.planCode)}}
@@ -22,18 +22,18 @@ mixin printPlan(plan)
| {{prices[plan.planCode]}} / #{translate("month")}
td
-if (subscription.state == "free-trial")
- a(href="/user/subscription/new?planCode=#{plan.planCode}").btn.btn-success #{translate("subscribe_to_this_plan")}
+ a(href="/user/subscription/new?planCode="+plan.planCode).btn.btn-success #{translate("subscribe_to_this_plan")}
-else if (typeof(subscription.planCode) != "undefined" && plan.planCode == subscription.planCode.split("_")[0])
button.btn.disabled #{translate("your_plan")}
-else
form
- input(type="hidden", ng-model="plan_code", name="plan_code", value="#{plan.planCode}")
+ input(type="hidden", ng-model="plan_code", name="plan_code", value=plan.planCode)
input(type="submit", ng-click="changePlan()", value=translate("change_to_this_plan")).btn.btn-success
mixin printPlans(plans)
- -each plan in plans
- mixin printPlan(plan)
+ each plan in plans
+ +printPlan(plan)
block content
.content.content-alt(ng-cloak)
@@ -46,7 +46,7 @@ block content
|
| #{translate("your_billing_details_were_saved")}
.card(ng-if="view == 'overview'")
- .page-header(x-current-plan="#{subscription.planCode}")
+ .page-header(x-current-plan=subscription.planCode)
h1 #{translate("your_subscription")}
- if (subscription && user._id+'' == subscription.admin_id+'')
@@ -97,9 +97,9 @@ block content
th !{translate("name")}
th !{translate("price")}
th
- mixin printPlans(plans.studentAccounts)
- mixin printPlans(plans.individualMonthlyPlans)
- mixin printPlans(plans.individualAnnualPlans)
+ +printPlans(plans.studentAccounts)
+ +printPlans(plans.individualMonthlyPlans)
+ +printPlans(plans.individualAnnualPlans)
each groupSubscription in groupSubscriptions
@@ -107,7 +107,7 @@ block content
div
p !{translate("member_of_group_subscription", {admin_email: "" + groupSubscription.admin_id.email + ""})}
span
- button.btn.btn-danger(ng-click="removeSelfFromGroup('#{groupSubscription.admin_id._id}')") #{translate("leave_group")}
+ button.btn.btn-danger(ng-click="removeSelfFromGroup('"+groupSubscription.admin_id._id+"')") #{translate("leave_group")}
-if(subscription.groupPlan && user._id+'' == subscription.admin_id+'')
div
diff --git a/services/web/app/views/subscriptions/edit-billing-details.jade b/services/web/app/views/subscriptions/edit-billing-details.pug
similarity index 100%
rename from services/web/app/views/subscriptions/edit-billing-details.jade
rename to services/web/app/views/subscriptions/edit-billing-details.pug
diff --git a/services/web/app/views/subscriptions/group/invite.jade b/services/web/app/views/subscriptions/group/invite.pug
similarity index 100%
rename from services/web/app/views/subscriptions/group/invite.jade
rename to services/web/app/views/subscriptions/group/invite.pug
diff --git a/services/web/app/views/subscriptions/group/successful_join.jade b/services/web/app/views/subscriptions/group/successful_join.pug
similarity index 100%
rename from services/web/app/views/subscriptions/group/successful_join.jade
rename to services/web/app/views/subscriptions/group/successful_join.pug
diff --git a/services/web/app/views/subscriptions/group_admin.jade b/services/web/app/views/subscriptions/group_admin.pug
similarity index 100%
rename from services/web/app/views/subscriptions/group_admin.jade
rename to services/web/app/views/subscriptions/group_admin.pug
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.pug
similarity index 99%
rename from services/web/app/views/subscriptions/new.jade
rename to services/web/app/views/subscriptions/new.pug
index 1465b24c82..2d410f8fba 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.pug
@@ -164,7 +164,7 @@ block content
ng-change="updateCountry()"
required
)
- mixin countries_options()
+ +countries_options()
span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
if (showVatField)
diff --git a/services/web/app/views/subscriptions/plans.jade b/services/web/app/views/subscriptions/plans.pug
similarity index 100%
rename from services/web/app/views/subscriptions/plans.jade
rename to services/web/app/views/subscriptions/plans.pug
diff --git a/services/web/app/views/subscriptions/successful_subscription.jade b/services/web/app/views/subscriptions/successful_subscription.pug
similarity index 100%
rename from services/web/app/views/subscriptions/successful_subscription.jade
rename to services/web/app/views/subscriptions/successful_subscription.pug
diff --git a/services/web/app/views/subscriptions/upgradeToAnnual.jade b/services/web/app/views/subscriptions/upgradeToAnnual.pug
similarity index 93%
rename from services/web/app/views/subscriptions/upgradeToAnnual.jade
rename to services/web/app/views/subscriptions/upgradeToAnnual.pug
index d92290ad0e..0ce41856f7 100644
--- a/services/web/app/views/subscriptions/upgradeToAnnual.jade
+++ b/services/web/app/views/subscriptions/upgradeToAnnual.pug
@@ -6,7 +6,7 @@ block content
.container(ng-controller="AnnualUpgradeController")
.row(ng-cloak)
.col-md-6.col-md-offset-3
- .card(ng-init="planName = #{JSON.stringify(planName)}")
+ .card(ng-init="planName = "+JSON.stringify(planName))
.page-header
h1.text-centered #{translate("move_to_annual_billing")}
div(ng-hide="upgradeComplete")
diff --git a/services/web/app/views/tests.jade b/services/web/app/views/tests.pug
similarity index 100%
rename from services/web/app/views/tests.jade
rename to services/web/app/views/tests.pug
diff --git a/services/web/app/views/translations/translation_message.jade b/services/web/app/views/translations/translation_message.pug
similarity index 85%
rename from services/web/app/views/translations/translation_message.jade
rename to services/web/app/views/translations/translation_message.pug
index 225ad3ea2c..635a8c9265 100644
--- a/services/web/app/views/translations/translation_message.jade
+++ b/services/web/app/views/translations/translation_message.pug
@@ -2,7 +2,7 @@
span(ng-controller="TranslationsPopupController", ng-cloak)
.translations-message(ng-hide="hidei18nNotification")
a(href=recomendSubdomain.url+currentUrl) !{translate("click_here_to_view_sl_in_lng", {lngName:"" + translate(recomendSubdomain.lngCode) + ""})}
- img(src=buildImgPath("flags/24/#{recomendSubdomain.lngCode}.png"))
+ img(src=buildImgPath("flags/24/" + recomendSubdomain.lngCode + ".png"))
button(ng-click="dismiss()").close.pull-right
span(aria-hidden="true") ×
span.sr-only #{translate("close")}
\ No newline at end of file
diff --git a/services/web/app/views/university/case_study.jade b/services/web/app/views/university/case_study.pug
similarity index 100%
rename from services/web/app/views/university/case_study.jade
rename to services/web/app/views/university/case_study.pug
diff --git a/services/web/app/views/university/university_holder.jade b/services/web/app/views/university/university_holder.pug
similarity index 100%
rename from services/web/app/views/university/university_holder.jade
rename to services/web/app/views/university/university_holder.pug
diff --git a/services/web/app/views/user/activate.jade b/services/web/app/views/user/activate.pug
similarity index 97%
rename from services/web/app/views/user/activate.jade
rename to services/web/app/views/user/activate.pug
index 7961876389..8b60b10471 100644
--- a/services/web/app/views/user/activate.jade
+++ b/services/web/app/views/user/activate.pug
@@ -36,7 +36,7 @@ block content
placeholder="email@example.com"
required,
ng-model="email",
- ng-init="email = #{JSON.stringify(email)}",
+ ng-init="email = "+JSON.stringify(email),
ng-model-options="{ updateOn: 'blur' }",
disabled
)
diff --git a/services/web/app/views/user/login.jade b/services/web/app/views/user/login.pug
similarity index 96%
rename from services/web/app/views/user/login.jade
rename to services/web/app/views/user/login.pug
index 8339f27189..823299d660 100644
--- a/services/web/app/views/user/login.jade
+++ b/services/web/app/views/user/login.pug
@@ -19,7 +19,7 @@ block content
placeholder='email@example.com',
ng-model="email",
ng-model-options="{ updateOn: 'blur' }",
- ng-init="email = #{JSON.stringify(email)}",
+ ng-init="email = "+JSON.stringify(email),
focus="true"
)
span.small.text-primary(ng-show="loginForm.email.$invalid && loginForm.email.$dirty")
diff --git a/services/web/app/views/user/passwordReset.jade b/services/web/app/views/user/passwordReset.pug
similarity index 100%
rename from services/web/app/views/user/passwordReset.jade
rename to services/web/app/views/user/passwordReset.pug
diff --git a/services/web/app/views/user/register.jade b/services/web/app/views/user/register.pug
similarity index 100%
rename from services/web/app/views/user/register.jade
rename to services/web/app/views/user/register.pug
diff --git a/services/web/app/views/user/restricted.jade b/services/web/app/views/user/restricted.pug
similarity index 100%
rename from services/web/app/views/user/restricted.jade
rename to services/web/app/views/user/restricted.pug
diff --git a/services/web/app/views/user/sessions.jade b/services/web/app/views/user/sessions.pug
similarity index 100%
rename from services/web/app/views/user/sessions.jade
rename to services/web/app/views/user/sessions.pug
diff --git a/services/web/app/views/user/setPassword.jade b/services/web/app/views/user/setPassword.pug
similarity index 100%
rename from services/web/app/views/user/setPassword.jade
rename to services/web/app/views/user/setPassword.pug
diff --git a/services/web/app/views/user/settings.jade b/services/web/app/views/user/settings.pug
similarity index 99%
rename from services/web/app/views/user/settings.jade
rename to services/web/app/views/user/settings.pug
index 310912cf07..a4217d8ca4 100644
--- a/services/web/app/views/user/settings.jade
+++ b/services/web/app/views/user/settings.pug
@@ -28,7 +28,7 @@ block content
placeholder="email@example.com"
required,
ng-model="email",
- ng-init="email = #{JSON.stringify(user.email)}",
+ ng-init="email = "+JSON.stringify(user.email),
ng-model-options="{ updateOn: 'blur' }"
)
span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty")
diff --git a/services/web/app/views/view_templates/bonus_templates.jade b/services/web/app/views/view_templates/bonus_templates.pug
similarity index 100%
rename from services/web/app/views/view_templates/bonus_templates.jade
rename to services/web/app/views/view_templates/bonus_templates.pug
diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee
index ccfec59235..b24c2568ab 100644
--- a/services/web/config/settings.defaults.coffee
+++ b/services/web/config/settings.defaults.coffee
@@ -48,6 +48,16 @@ module.exports = settings =
# {host: 'localhost', port: 7005}
# ]
+ # ratelimiter:
+ # cluster: [
+ # {host: 'localhost', port: 7000}
+ # {host: 'localhost', port: 7001}
+ # {host: 'localhost', port: 7002}
+ # {host: 'localhost', port: 7003}
+ # {host: 'localhost', port: 7004}
+ # {host: 'localhost', port: 7005}
+ # ]
+
api:
host: "localhost"
port: "6379"
@@ -169,6 +179,7 @@ module.exports = settings =
compileGroup: "standard"
references: true
templates: true
+ trackChanges: true
plans: plans = [{
planCode: "personal"
@@ -335,35 +346,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/modules/.gitignore b/services/web/modules/.gitignore
index b90beee9f7..1d30263cb3 100644
--- a/services/web/modules/.gitignore
+++ b/services/web/modules/.gitignore
@@ -2,3 +2,11 @@
*/test/unit/js
*/index.js
ldap
+admin-panel
+groovehq
+launchpad
+learn-wiki
+references-search
+sharelatex-saml
+templates
+tpr-webmodule
diff --git a/services/web/npm-shrinkwrap.json b/services/web/npm-shrinkwrap.json
index 196eec0a44..0dd0cab400 100644
--- a/services/web/npm-shrinkwrap.json
+++ b/services/web/npm-shrinkwrap.json
@@ -1,3209 +1,4 @@
{
"name": "web-sharelatex",
- "version": "0.1.4",
- "dependencies": {
- "abbrev": {
- "version": "1.0.9",
- "from": "abbrev@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz"
- },
- "accepts": {
- "version": "1.2.13",
- "from": "accepts@>=1.2.9 <1.3.0",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz"
- },
- "addressparser": {
- "version": "0.2.1",
- "from": "addressparser@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-0.2.1.tgz"
- },
- "amdefine": {
- "version": "1.0.1",
- "from": "amdefine@>=0.0.4",
- "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz"
- },
- "ansi-regex": {
- "version": "2.0.0",
- "from": "ansi-regex@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
- },
- "ansi-styles": {
- "version": "2.2.1",
- "from": "ansi-styles@>=2.2.1 <3.0.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz"
- },
- "aproba": {
- "version": "1.0.4",
- "from": "aproba@>=1.0.3 <2.0.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.0.4.tgz"
- },
- "archiver": {
- "version": "0.9.0",
- "from": "archiver@0.9.0",
- "resolved": "https://registry.npmjs.org/archiver/-/archiver-0.9.0.tgz",
- "dependencies": {
- "lodash": {
- "version": "2.4.2",
- "from": "lodash@>=2.4.1 <2.5.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
- }
- }
- },
- "are-we-there-yet": {
- "version": "1.1.2",
- "from": "are-we-there-yet@>=1.1.2 <1.2.0",
- "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz",
- "dependencies": {
- "isarray": {
- "version": "1.0.0",
- "from": "isarray@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
- },
- "readable-stream": {
- "version": "2.2.2",
- "from": "readable-stream@>=2.0.0 <3.0.0||>=1.1.13 <2.0.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.2.tgz"
- }
- }
- },
- "argparse": {
- "version": "0.1.16",
- "from": "argparse@>=0.1.11 <0.2.0",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz",
- "dependencies": {
- "underscore": {
- "version": "1.7.0",
- "from": "underscore@>=1.7.0 <1.8.0",
- "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz"
- },
- "underscore.string": {
- "version": "2.4.0",
- "from": "underscore.string@>=2.4.0 <2.5.0",
- "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz"
- }
- }
- },
- "array-flatten": {
- "version": "1.1.0",
- "from": "array-flatten@1.1.0",
- "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.0.tgz"
- },
- "asn1": {
- "version": "0.2.3",
- "from": "asn1@>=0.2.3 <0.3.0",
- "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz"
- },
- "assert-plus": {
- "version": "0.2.0",
- "from": "assert-plus@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz"
- },
- "assertion-error": {
- "version": "1.0.2",
- "from": "assertion-error@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz"
- },
- "async": {
- "version": "0.6.2",
- "from": "async@0.6.2",
- "resolved": "https://registry.npmjs.org/async/-/async-0.6.2.tgz"
- },
- "asynckit": {
- "version": "0.4.0",
- "from": "asynckit@>=0.4.0 <0.5.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
- },
- "aws-sdk": {
- "version": "2.7.16",
- "from": "aws-sdk@>=2.6.12 <3.0.0",
- "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.7.16.tgz",
- "dependencies": {
- "uuid": {
- "version": "3.0.0",
- "from": "uuid@3.0.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz"
- },
- "xml2js": {
- "version": "0.4.15",
- "from": "xml2js@0.4.15",
- "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.15.tgz"
- }
- }
- },
- "aws-sign2": {
- "version": "0.6.0",
- "from": "aws-sign2@>=0.6.0 <0.7.0",
- "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz"
- },
- "aws4": {
- "version": "1.5.0",
- "from": "aws4@>=1.2.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.5.0.tgz"
- },
- "axo": {
- "version": "0.0.2",
- "from": "axo@>=0.0.0 <0.1.0",
- "resolved": "https://registry.npmjs.org/axo/-/axo-0.0.2.tgz"
- },
- "babel-runtime": {
- "version": "6.3.19",
- "from": "babel-runtime@>=6.3.19 <6.4.0",
- "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.3.19.tgz"
- },
- "backoff": {
- "version": "2.5.0",
- "from": "backoff@>=2.5.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz"
- },
- "balanced-match": {
- "version": "0.4.2",
- "from": "balanced-match@>=0.4.1 <0.5.0",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz"
- },
- "base64-js": {
- "version": "1.2.0",
- "from": "base64-js@>=1.0.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz"
- },
- "base64-stream": {
- "version": "0.1.3",
- "from": "base64-stream@>=0.1.2 <0.2.0",
- "resolved": "https://registry.npmjs.org/base64-stream/-/base64-stream-0.1.3.tgz",
- "dependencies": {
- "isarray": {
- "version": "1.0.0",
- "from": "isarray@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
- },
- "readable-stream": {
- "version": "2.2.2",
- "from": "readable-stream@>=2.0.2 <3.0.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.2.tgz"
- }
- }
- },
- "base64-url": {
- "version": "1.3.3",
- "from": "base64-url@1.3.3",
- "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-1.3.3.tgz"
- },
- "basic-auth-connect": {
- "version": "1.0.0",
- "from": "basic-auth-connect@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz"
- },
- "bcrypt": {
- "version": "1.0.1",
- "from": "bcrypt@1.0.1",
- "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-1.0.1.tgz"
- },
- "bcrypt-pbkdf": {
- "version": "1.0.0",
- "from": "bcrypt-pbkdf@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz",
- "optional": true
- },
- "bcryptjs": {
- "version": "2.3.0",
- "from": "bcryptjs@2.3.0",
- "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.3.0.tgz"
- },
- "bindings": {
- "version": "1.2.1",
- "from": "bindings@1.2.1",
- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz"
- },
- "bl": {
- "version": "0.6.0",
- "from": "bl@>=0.6.0 <0.7.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-0.6.0.tgz"
- },
- "block-stream": {
- "version": "0.0.9",
- "from": "block-stream@*",
- "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz"
- },
- "bluebird": {
- "version": "3.4.6",
- "from": "bluebird@>=3.3.4 <4.0.0",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.6.tgz"
- },
- "body-parser": {
- "version": "1.15.2",
- "from": "body-parser@>=1.13.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.15.2.tgz",
- "dependencies": {
- "debug": {
- "version": "2.2.0",
- "from": "debug@>=2.2.0 <2.3.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
- },
- "iconv-lite": {
- "version": "0.4.13",
- "from": "iconv-lite@0.4.13",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz"
- },
- "ms": {
- "version": "0.7.1",
- "from": "ms@0.7.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
- },
- "qs": {
- "version": "6.2.0",
- "from": "qs@6.2.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.0.tgz"
- }
- }
- },
- "boom": {
- "version": "2.10.1",
- "from": "boom@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz"
- },
- "brace-expansion": {
- "version": "1.1.6",
- "from": "brace-expansion@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz"
- },
- "bson": {
- "version": "1.0.1",
- "from": "bson@>=1.0.1 <1.1.0",
- "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.1.tgz"
- },
- "bson-ext": {
- "version": "0.1.13",
- "from": "bson-ext@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/bson-ext/-/bson-ext-0.1.13.tgz",
- "optional": true,
- "dependencies": {
- "nan": {
- "version": "2.0.9",
- "from": "nan@>=2.0.9 <2.1.0",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.0.9.tgz",
- "optional": true
- }
- }
- },
- "buffer": {
- "version": "4.9.1",
- "from": "buffer@4.9.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
- "dependencies": {
- "isarray": {
- "version": "1.0.0",
- "from": "isarray@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
- }
- }
- },
- "buffer-crc32": {
- "version": "0.2.13",
- "from": "buffer-crc32@>=0.2.1 <0.3.0",
- "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz"
- },
- "buffer-shims": {
- "version": "1.0.0",
- "from": "buffer-shims@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz"
- },
- "bufferedstream": {
- "version": "1.6.0",
- "from": "bufferedstream@1.6.0",
- "resolved": "https://registry.npmjs.org/bufferedstream/-/bufferedstream-1.6.0.tgz"
- },
- "buildmail": {
- "version": "3.3.2",
- "from": "buildmail@3.3.2",
- "resolved": "https://registry.npmjs.org/buildmail/-/buildmail-3.3.2.tgz",
- "dependencies": {
- "addressparser": {
- "version": "1.0.0",
- "from": "addressparser@1.0.0",
- "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.0.tgz"
- }
- }
- },
- "bunyan": {
- "version": "0.22.1",
- "from": "bunyan@0.22.1",
- "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz"
- },
- "busboy": {
- "version": "0.2.13",
- "from": "busboy@>=0.2.9 <0.3.0",
- "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.13.tgz",
- "dependencies": {
- "readable-stream": {
- "version": "1.1.14",
- "from": "readable-stream@>=1.1.0 <1.2.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz"
- }
- }
- },
- "bytes": {
- "version": "2.4.0",
- "from": "bytes@2.4.0",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz"
- },
- "camelcase": {
- "version": "1.2.1",
- "from": "camelcase@>=1.0.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz"
- },
- "caseless": {
- "version": "0.11.0",
- "from": "caseless@>=0.11.0 <0.12.0",
- "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz"
- },
- "chai": {
- "version": "3.5.0",
- "from": "chai@latest",
- "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz"
- },
- "chai-spies": {
- "version": "0.7.1",
- "from": "chai-spies@latest",
- "resolved": "https://registry.npmjs.org/chai-spies/-/chai-spies-0.7.1.tgz"
- },
- "chalk": {
- "version": "1.1.3",
- "from": "chalk@>=1.1.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
- "dependencies": {
- "supports-color": {
- "version": "2.0.0",
- "from": "supports-color@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
- }
- }
- },
- "character-parser": {
- "version": "1.2.0",
- "from": "character-parser@1.2.0",
- "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.0.tgz"
- },
- "clone": {
- "version": "1.0.2",
- "from": "clone@1.0.2",
- "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz"
- },
- "cluster-key-slot": {
- "version": "1.0.8",
- "from": "cluster-key-slot@>=1.0.6 <2.0.0",
- "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.0.8.tgz"
- },
- "code-point-at": {
- "version": "1.1.0",
- "from": "code-point-at@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz"
- },
- "coffee-script": {
- "version": "1.3.3",
- "from": "coffee-script@>=1.3.3 <1.4.0",
- "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz"
- },
- "colors": {
- "version": "0.6.2",
- "from": "colors@>=0.6.2 <0.7.0",
- "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz"
- },
- "combined-stream": {
- "version": "1.0.5",
- "from": "combined-stream@>=1.0.5 <1.1.0",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz"
- },
- "commander": {
- "version": "2.9.0",
- "from": "commander@>=2.9.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz"
- },
- "concat-map": {
- "version": "0.0.1",
- "from": "concat-map@0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
- },
- "connect-redis": {
- "version": "3.1.0",
- "from": "connect-redis@>=3.1.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-3.1.0.tgz",
- "dependencies": {
- "debug": {
- "version": "2.4.5",
- "from": "debug@>=2.2.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.4.5.tgz"
- },
- "ms": {
- "version": "0.7.2",
- "from": "ms@0.7.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz"
- },
- "redis": {
- "version": "2.6.3",
- "from": "redis@>=2.1.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/redis/-/redis-2.6.3.tgz"
- }
- }
- },
- "console-control-strings": {
- "version": "1.1.0",
- "from": "console-control-strings@>=1.1.0 <1.2.0",
- "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz"
- },
- "constantinople": {
- "version": "2.0.1",
- "from": "constantinople@>=2.0.0 <2.1.0",
- "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-2.0.1.tgz"
- },
- "content-disposition": {
- "version": "0.5.0",
- "from": "content-disposition@0.5.0",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz"
- },
- "content-type": {
- "version": "1.0.2",
- "from": "content-type@>=1.0.2 <1.1.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz"
- },
- "contentful": {
- "version": "3.8.0",
- "from": "contentful@>=3.3.14 <4.0.0",
- "resolved": "https://registry.npmjs.org/contentful/-/contentful-3.8.0.tgz",
- "dependencies": {
- "lodash": {
- "version": "4.2.1",
- "from": "lodash@>=4.2.0 <4.3.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.2.1.tgz"
- }
- }
- },
- "contentful-sdk-core": {
- "version": "2.5.0",
- "from": "contentful-sdk-core@>=2.5.0 <2.6.0",
- "resolved": "https://registry.npmjs.org/contentful-sdk-core/-/contentful-sdk-core-2.5.0.tgz"
- },
- "cookie": {
- "version": "0.2.4",
- "from": "cookie@>=0.2.3 <0.3.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.2.4.tgz"
- },
- "cookie-parser": {
- "version": "1.3.5",
- "from": "cookie-parser@1.3.5",
- "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.3.5.tgz",
- "dependencies": {
- "cookie": {
- "version": "0.1.3",
- "from": "cookie@0.1.3",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz"
- }
- }
- },
- "cookie-signature": {
- "version": "1.0.6",
- "from": "cookie-signature@1.0.6",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
- },
- "core-js": {
- "version": "1.2.7",
- "from": "core-js@>=1.2.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz"
- },
- "core-util-is": {
- "version": "1.0.2",
- "from": "core-util-is@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
- },
- "crc": {
- "version": "3.4.1",
- "from": "crc@3.4.1",
- "resolved": "https://registry.npmjs.org/crc/-/crc-3.4.1.tgz"
- },
- "crc32-stream": {
- "version": "0.2.0",
- "from": "crc32-stream@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-0.2.0.tgz"
- },
- "cross-env": {
- "version": "3.1.3",
- "from": "cross-env@>=3.1.2 <4.0.0",
- "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-3.1.3.tgz"
- },
- "cross-spawn": {
- "version": "3.0.1",
- "from": "cross-spawn@>=3.0.1 <4.0.0",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
- "dependencies": {
- "lru-cache": {
- "version": "4.0.2",
- "from": "lru-cache@>=4.0.1 <5.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz"
- },
- "which": {
- "version": "1.2.12",
- "from": "which@>=1.2.9 <2.0.0",
- "resolved": "https://registry.npmjs.org/which/-/which-1.2.12.tgz"
- }
- }
- },
- "cryptiles": {
- "version": "2.0.5",
- "from": "cryptiles@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz"
- },
- "crypto-browserify": {
- "version": "1.0.9",
- "from": "crypto-browserify@1.0.9",
- "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz"
- },
- "csrf": {
- "version": "3.0.4",
- "from": "csrf@>=3.0.3 <3.1.0",
- "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.4.tgz"
- },
- "css": {
- "version": "1.0.8",
- "from": "css@>=1.0.8 <1.1.0",
- "resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz"
- },
- "css-parse": {
- "version": "1.0.4",
- "from": "css-parse@1.0.4",
- "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz"
- },
- "css-stringify": {
- "version": "1.0.5",
- "from": "css-stringify@1.0.5",
- "resolved": "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz"
- },
- "csurf": {
- "version": "1.9.0",
- "from": "csurf@>=1.8.3 <2.0.0",
- "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.9.0.tgz",
- "dependencies": {
- "cookie": {
- "version": "0.3.1",
- "from": "cookie@0.3.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz"
- }
- }
- },
- "dashdash": {
- "version": "1.14.1",
- "from": "dashdash@>=1.12.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
- "dependencies": {
- "assert-plus": {
- "version": "1.0.0",
- "from": "assert-plus@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz"
- }
- }
- },
- "dateformat": {
- "version": "1.0.4-1.2.3",
- "from": "dateformat@1.0.4-1.2.3",
- "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.4-1.2.3.tgz"
- },
- "debug": {
- "version": "1.0.4",
- "from": "debug@>=1.0.2 <1.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz"
- },
- "decamelize": {
- "version": "1.2.0",
- "from": "decamelize@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
- },
- "deep-eql": {
- "version": "0.1.3",
- "from": "deep-eql@^0.1.3",
- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz",
- "dependencies": {
- "type-detect": {
- "version": "0.1.1",
- "from": "type-detect@0.1.1",
- "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz"
- }
- }
- },
- "deep-extend": {
- "version": "0.4.1",
- "from": "deep-extend@>=0.4.0 <0.5.0",
- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz"
- },
- "deflate-crc32-stream": {
- "version": "0.1.2",
- "from": "deflate-crc32-stream@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/deflate-crc32-stream/-/deflate-crc32-stream-0.1.2.tgz"
- },
- "delayed-stream": {
- "version": "1.0.0",
- "from": "delayed-stream@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
- },
- "delegates": {
- "version": "1.0.0",
- "from": "delegates@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz"
- },
- "depd": {
- "version": "1.1.0",
- "from": "depd@>=1.1.0 <1.2.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz"
- },
- "destroy": {
- "version": "1.0.3",
- "from": "destroy@1.0.3",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz"
- },
- "dicer": {
- "version": "0.2.5",
- "from": "dicer@0.2.5",
- "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
- "dependencies": {
- "readable-stream": {
- "version": "1.1.14",
- "from": "readable-stream@>=1.1.0 <1.2.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz"
- }
- }
- },
- "diff": {
- "version": "1.0.7",
- "from": "diff@1.0.7",
- "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz"
- },
- "dottie": {
- "version": "1.1.1",
- "from": "dottie@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/dottie/-/dottie-1.1.1.tgz"
- },
- "double-ended-queue": {
- "version": "2.1.0-0",
- "from": "double-ended-queue@>=2.1.0-0 <3.0.0",
- "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz"
- },
- "each-series": {
- "version": "1.0.0",
- "from": "each-series@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/each-series/-/each-series-1.0.0.tgz"
- },
- "ecc-jsbn": {
- "version": "0.1.1",
- "from": "ecc-jsbn@>=0.1.1 <0.2.0",
- "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
- "optional": true
- },
- "ee-first": {
- "version": "1.1.1",
- "from": "ee-first@1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
- },
- "ejs": {
- "version": "0.8.8",
- "from": "ejs@>=0.8.3 <0.9.0",
- "resolved": "https://registry.npmjs.org/ejs/-/ejs-0.8.8.tgz"
- },
- "encoding": {
- "version": "0.1.12",
- "from": "encoding@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
- "dependencies": {
- "iconv-lite": {
- "version": "0.4.15",
- "from": "iconv-lite@>=0.4.13 <0.5.0",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz"
- }
- }
- },
- "end-of-stream": {
- "version": "0.1.5",
- "from": "end-of-stream@>=0.1.3 <0.2.0",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz"
- },
- "es6-promise": {
- "version": "3.2.1",
- "from": "es6-promise@3.2.1",
- "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz"
- },
- "escape-html": {
- "version": "1.0.2",
- "from": "escape-html@1.0.2",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz"
- },
- "escape-string-regexp": {
- "version": "1.0.5",
- "from": "escape-string-regexp@>=1.0.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
- },
- "esprima": {
- "version": "1.0.4",
- "from": "esprima@>=1.0.2 <1.1.0",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz"
- },
- "etag": {
- "version": "1.7.0",
- "from": "etag@>=1.7.0 <1.8.0",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz"
- },
- "eventemitter2": {
- "version": "0.4.14",
- "from": "eventemitter2@>=0.4.13 <0.5.0",
- "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz"
- },
- "eventemitter3": {
- "version": "1.2.0",
- "from": "eventemitter3@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz"
- },
- "exit": {
- "version": "0.1.2",
- "from": "exit@>=0.1.1 <0.2.0",
- "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz"
- },
- "express": {
- "version": "4.13.0",
- "from": "express@4.13.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.13.0.tgz",
- "dependencies": {
- "cookie": {
- "version": "0.1.3",
- "from": "cookie@0.1.3",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz"
- },
- "debug": {
- "version": "2.2.0",
- "from": "debug@>=2.2.0 <2.3.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
- },
- "depd": {
- "version": "1.0.1",
- "from": "depd@>=1.0.1 <1.1.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
- },
- "ms": {
- "version": "0.7.1",
- "from": "ms@0.7.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
- },
- "qs": {
- "version": "2.4.2",
- "from": "qs@2.4.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-2.4.2.tgz"
- }
- }
- },
- "express-session": {
- "version": "1.14.2",
- "from": "express-session@>=1.14.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.14.2.tgz",
- "dependencies": {
- "cookie": {
- "version": "0.3.1",
- "from": "cookie@0.3.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz"
- },
- "debug": {
- "version": "2.2.0",
- "from": "debug@>=2.2.0 <2.3.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
- },
- "ms": {
- "version": "0.7.1",
- "from": "ms@0.7.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
- }
- }
- },
- "extend": {
- "version": "3.0.0",
- "from": "extend@>=3.0.0 <3.1.0",
- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz"
- },
- "extendible": {
- "version": "0.1.1",
- "from": "extendible@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/extendible/-/extendible-0.1.1.tgz"
- },
- "extsprintf": {
- "version": "1.0.2",
- "from": "extsprintf@1.0.2",
- "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz"
- },
- "failure": {
- "version": "1.1.1",
- "from": "failure@>=1.1.0 <1.2.0",
- "resolved": "https://registry.npmjs.org/failure/-/failure-1.1.1.tgz"
- },
- "file-utils": {
- "version": "0.1.5",
- "from": "file-utils@>=0.1.5 <0.2.0",
- "resolved": "https://registry.npmjs.org/file-utils/-/file-utils-0.1.5.tgz",
- "dependencies": {
- "lodash": {
- "version": "2.1.0",
- "from": "lodash@>=2.1.0 <2.2.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.1.0.tgz"
- }
- }
- },
- "finalhandler": {
- "version": "0.4.0",
- "from": "finalhandler@0.4.0",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz",
- "dependencies": {
- "debug": {
- "version": "2.2.0",
- "from": "debug@>=2.2.0 <2.3.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
- },
- "ms": {
- "version": "0.7.1",
- "from": "ms@0.7.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
- }
- }
- },
- "findup-sync": {
- "version": "0.1.3",
- "from": "findup-sync@>=0.1.2 <0.2.0",
- "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz",
- "dependencies": {
- "lodash": {
- "version": "2.4.2",
- "from": "lodash@>=2.4.1 <2.5.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
- }
- }
- },
- "flexbuffer": {
- "version": "0.0.6",
- "from": "flexbuffer@0.0.6",
- "resolved": "https://registry.npmjs.org/flexbuffer/-/flexbuffer-0.0.6.tgz"
- },
- "follow-redirects": {
- "version": "0.0.7",
- "from": "follow-redirects@0.0.7",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz",
- "dependencies": {
- "debug": {
- "version": "2.4.5",
- "from": "debug@>=2.2.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.4.5.tgz"
- },
- "ms": {
- "version": "0.7.2",
- "from": "ms@0.7.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz"
- }
- }
- },
- "forever-agent": {
- "version": "0.6.1",
- "from": "forever-agent@>=0.6.1 <0.7.0",
- "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz"
- },
- "form-data": {
- "version": "2.1.2",
- "from": "form-data@>=2.1.1 <2.2.0",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.2.tgz"
- },
- "formatio": {
- "version": "1.1.1",
- "from": "formatio@1.1.1",
- "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz"
- },
- "forwarded": {
- "version": "0.1.0",
- "from": "forwarded@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz"
- },
- "fresh": {
- "version": "0.3.0",
- "from": "fresh@0.3.0",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.3.0.tgz"
- },
- "fs-extra": {
- "version": "0.11.1",
- "from": "fs-extra@>=0.11.1 <0.12.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.11.1.tgz",
- "dependencies": {
- "glob": {
- "version": "7.1.1",
- "from": "glob@>=7.0.5 <8.0.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz"
- },
- "minimatch": {
- "version": "3.0.3",
- "from": "minimatch@>=3.0.2 <4.0.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz"
- },
- "ncp": {
- "version": "0.6.0",
- "from": "ncp@>=0.6.0 <0.7.0",
- "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.6.0.tgz"
- },
- "rimraf": {
- "version": "2.5.4",
- "from": "rimraf@>=2.2.8 <3.0.0",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz"
- }
- }
- },
- "fs.realpath": {
- "version": "1.0.0",
- "from": "fs.realpath@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
- },
- "fstream": {
- "version": "1.0.10",
- "from": "fstream@>=1.0.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.10.tgz"
- },
- "fstream-ignore": {
- "version": "1.0.5",
- "from": "fstream-ignore@>=1.0.5 <1.1.0",
- "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz",
- "dependencies": {
- "minimatch": {
- "version": "3.0.3",
- "from": "minimatch@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz"
- }
- }
- },
- "gauge": {
- "version": "2.7.2",
- "from": "gauge@>=2.7.1 <2.8.0",
- "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.2.tgz"
- },
- "generate-function": {
- "version": "2.0.0",
- "from": "generate-function@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz"
- },
- "generate-object-property": {
- "version": "1.2.0",
- "from": "generate-object-property@>=1.1.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz"
- },
- "generic-pool": {
- "version": "2.4.2",
- "from": "generic-pool@2.4.2",
- "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.4.2.tgz"
- },
- "getobject": {
- "version": "0.1.0",
- "from": "getobject@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz"
- },
- "getpass": {
- "version": "0.1.6",
- "from": "getpass@>=0.1.1 <0.2.0",
- "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz",
- "dependencies": {
- "assert-plus": {
- "version": "1.0.0",
- "from": "assert-plus@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz"
- }
- }
- },
- "glob": {
- "version": "3.2.11",
- "from": "glob@>=3.2.6 <3.3.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
- "dependencies": {
- "minimatch": {
- "version": "0.3.0",
- "from": "minimatch@>=0.3.0 <0.4.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz"
- }
- }
- },
- "graceful-fs": {
- "version": "4.1.11",
- "from": "graceful-fs@>=4.1.2 <5.0.0",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz"
- },
- "graceful-readlink": {
- "version": "1.0.1",
- "from": "graceful-readlink@>=1.0.0",
- "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz"
- },
- "growl": {
- "version": "1.7.0",
- "from": "growl@>=1.7.0 <1.8.0",
- "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz"
- },
- "grunt": {
- "version": "0.4.5",
- "from": "grunt@>=0.4.5 <0.5.0",
- "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz",
- "dependencies": {
- "async": {
- "version": "0.1.22",
- "from": "async@>=0.1.22 <0.2.0",
- "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz"
- },
- "dateformat": {
- "version": "1.0.2-1.2.3",
- "from": "dateformat@1.0.2-1.2.3",
- "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz"
- },
- "glob": {
- "version": "3.1.21",
- "from": "glob@>=3.1.21 <3.2.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz"
- },
- "graceful-fs": {
- "version": "1.2.3",
- "from": "graceful-fs@>=1.2.0 <1.3.0",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz"
- },
- "inherits": {
- "version": "1.0.2",
- "from": "inherits@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz"
- },
- "lodash": {
- "version": "0.9.2",
- "from": "lodash@>=0.9.2 <0.10.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz"
- },
- "nopt": {
- "version": "1.0.10",
- "from": "nopt@>=1.0.10 <1.1.0",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz"
- },
- "rimraf": {
- "version": "2.2.8",
- "from": "rimraf@>=2.2.8 <2.3.0",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz"
- }
- }
- },
- "grunt-legacy-log": {
- "version": "0.1.3",
- "from": "grunt-legacy-log@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz",
- "dependencies": {
- "lodash": {
- "version": "2.4.2",
- "from": "lodash@>=2.4.1 <2.5.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
- },
- "underscore.string": {
- "version": "2.3.3",
- "from": "underscore.string@>=2.3.3 <2.4.0",
- "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz"
- }
- }
- },
- "grunt-legacy-log-utils": {
- "version": "0.1.1",
- "from": "grunt-legacy-log-utils@>=0.1.1 <0.2.0",
- "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz",
- "dependencies": {
- "lodash": {
- "version": "2.4.2",
- "from": "lodash@>=2.4.1 <2.5.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
- },
- "underscore.string": {
- "version": "2.3.3",
- "from": "underscore.string@>=2.3.3 <2.4.0",
- "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz"
- }
- }
- },
- "grunt-legacy-util": {
- "version": "0.2.0",
- "from": "grunt-legacy-util@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz",
- "dependencies": {
- "async": {
- "version": "0.1.22",
- "from": "async@>=0.1.22 <0.2.0",
- "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz"
- },
- "lodash": {
- "version": "0.9.2",
- "from": "lodash@>=0.9.2 <0.10.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz"
- }
- }
- },
- "hang": {
- "version": "1.0.0",
- "from": "hang@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/hang/-/hang-1.0.0.tgz"
- },
- "har-validator": {
- "version": "2.0.6",
- "from": "har-validator@>=2.0.6 <2.1.0",
- "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz"
- },
- "has-ansi": {
- "version": "2.0.0",
- "from": "has-ansi@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz"
- },
- "has-unicode": {
- "version": "2.0.1",
- "from": "has-unicode@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz"
- },
- "hawk": {
- "version": "3.1.3",
- "from": "hawk@>=3.1.3 <3.2.0",
- "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz"
- },
- "heapdump": {
- "version": "0.3.7",
- "from": "heapdump@>=0.3.7 <0.4.0",
- "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.7.tgz"
- },
- "hoek": {
- "version": "2.16.3",
- "from": "hoek@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz"
- },
- "hooker": {
- "version": "0.2.3",
- "from": "hooker@>=0.2.3 <0.3.0",
- "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz"
- },
- "hooks-fixed": {
- "version": "1.1.0",
- "from": "hooks-fixed@1.1.0",
- "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-1.1.0.tgz"
- },
- "http-errors": {
- "version": "1.5.1",
- "from": "http-errors@>=1.5.0 <1.6.0",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.1.tgz"
- },
- "http-proxy": {
- "version": "1.16.2",
- "from": "http-proxy@>=1.8.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.16.2.tgz"
- },
- "http-signature": {
- "version": "1.1.1",
- "from": "http-signature@>=1.1.0 <1.2.0",
- "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz"
- },
- "iconv-lite": {
- "version": "0.2.11",
- "from": "iconv-lite@>=0.2.11 <0.3.0",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz"
- },
- "ieee754": {
- "version": "1.1.8",
- "from": "ieee754@>=1.1.4 <2.0.0",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz"
- },
- "inflection": {
- "version": "1.10.0",
- "from": "inflection@>=1.6.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.10.0.tgz"
- },
- "inflight": {
- "version": "1.0.6",
- "from": "inflight@>=1.0.4 <2.0.0",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz"
- },
- "inherits": {
- "version": "2.0.3",
- "from": "inherits@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz"
- },
- "ini": {
- "version": "1.3.4",
- "from": "ini@>=1.3.0 <1.4.0",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz"
- },
- "ioredis": {
- "version": "2.4.3",
- "from": "ioredis@>=2.4.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-2.4.3.tgz",
- "dependencies": {
- "debug": {
- "version": "2.4.5",
- "from": "debug@>=2.2.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.4.5.tgz"
- },
- "ms": {
- "version": "0.7.2",
- "from": "ms@0.7.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz"
- },
- "redis-parser": {
- "version": "1.3.0",
- "from": "redis-parser@>=1.3.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-1.3.0.tgz"
- }
- }
- },
- "ipaddr.js": {
- "version": "1.0.5",
- "from": "ipaddr.js@1.0.5",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz"
- },
- "is-fullwidth-code-point": {
- "version": "1.0.0",
- "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz"
- },
- "is-my-json-valid": {
- "version": "2.15.0",
- "from": "is-my-json-valid@>=2.12.4 <3.0.0",
- "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz"
- },
- "is-promise": {
- "version": "1.0.1",
- "from": "is-promise@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz"
- },
- "is-property": {
- "version": "1.0.2",
- "from": "is-property@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz"
- },
- "is-typedarray": {
- "version": "1.0.0",
- "from": "is-typedarray@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz"
- },
- "isarray": {
- "version": "0.0.1",
- "from": "isarray@0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
- },
- "isbinaryfile": {
- "version": "0.1.9",
- "from": "isbinaryfile@>=0.1.9 <0.2.0",
- "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-0.1.9.tgz"
- },
- "isexe": {
- "version": "1.1.2",
- "from": "isexe@>=1.1.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-1.1.2.tgz"
- },
- "isstream": {
- "version": "0.1.2",
- "from": "isstream@>=0.1.2 <0.2.0",
- "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz"
- },
- "jade": {
- "version": "1.3.1",
- "from": "jade@>=1.3.1 <1.4.0",
- "resolved": "https://registry.npmjs.org/jade/-/jade-1.3.1.tgz",
- "dependencies": {
- "commander": {
- "version": "2.1.0",
- "from": "commander@2.1.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz"
- },
- "mkdirp": {
- "version": "0.3.5",
- "from": "mkdirp@>=0.3.5 <0.4.0",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
- }
- }
- },
- "jmespath": {
- "version": "0.15.0",
- "from": "jmespath@0.15.0",
- "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz"
- },
- "jodid25519": {
- "version": "1.0.2",
- "from": "jodid25519@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz",
- "optional": true
- },
- "js-yaml": {
- "version": "2.0.5",
- "from": "js-yaml@>=2.0.5 <2.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz"
- },
- "jsbn": {
- "version": "0.1.0",
- "from": "jsbn@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz",
- "optional": true
- },
- "json-schema": {
- "version": "0.2.3",
- "from": "json-schema@0.2.3",
- "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz"
- },
- "json-stringify-safe": {
- "version": "5.0.1",
- "from": "json-stringify-safe@>=5.0.1 <5.1.0",
- "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
- },
- "jsonfile": {
- "version": "2.4.0",
- "from": "jsonfile@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz"
- },
- "jsonpointer": {
- "version": "4.0.0",
- "from": "jsonpointer@>=4.0.0 <5.0.0",
- "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.0.tgz"
- },
- "jsprim": {
- "version": "1.3.1",
- "from": "jsprim@>=1.2.2 <2.0.0",
- "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.1.tgz"
- },
- "kareem": {
- "version": "1.0.1",
- "from": "kareem@1.0.1",
- "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.0.1.tgz"
- },
- "kerberos": {
- "version": "0.0.22",
- "from": "kerberos@>=0.0.0 <0.1.0",
- "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.22.tgz",
- "optional": true,
- "dependencies": {
- "nan": {
- "version": "2.4.0",
- "from": "nan@>=2.4.0 <2.5.0",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz",
- "optional": true
- }
- }
- },
- "lazystream": {
- "version": "0.1.0",
- "from": "lazystream@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-0.1.0.tgz"
- },
- "ldap-filter": {
- "version": "0.2.2",
- "from": "ldap-filter@0.2.2",
- "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
- "dependencies": {
- "assert-plus": {
- "version": "0.1.5",
- "from": "assert-plus@0.1.5",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz"
- }
- }
- },
- "ldapauth-fork": {
- "version": "2.5.4",
- "from": "ldapauth-fork@>=2.5.0 <2.6.0",
- "resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-2.5.4.tgz",
- "dependencies": {
- "assert-plus": {
- "version": "1.0.0",
- "from": "assert-plus@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz"
- },
- "bunyan": {
- "version": "1.8.5",
- "from": "bunyan@>=1.8.3 <2.0.0",
- "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.5.tgz",
- "dependencies": {
- "dtrace-provider": {
- "version": "0.8.0",
- "from": "dtrace-provider@>=0.8.0 <0.9.0",
- "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.0.tgz",
- "optional": true
- }
- }
- },
- "dtrace-provider": {
- "version": "0.7.1",
- "from": "dtrace-provider@>=0.7.0 <0.8.0",
- "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.7.1.tgz",
- "optional": true
- },
- "extsprintf": {
- "version": "1.2.0",
- "from": "extsprintf@1.2.0",
- "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz"
- },
- "glob": {
- "version": "6.0.4",
- "from": "glob@>=6.0.1 <7.0.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
- "optional": true
- },
- "ldapjs": {
- "version": "1.0.1",
- "from": "ldapjs@>=1.0.1 <1.1.0",
- "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.1.tgz"
- },
- "lru-cache": {
- "version": "3.2.0",
- "from": "lru-cache@3.2.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-3.2.0.tgz"
- },
- "minimatch": {
- "version": "3.0.3",
- "from": "minimatch@>=2.0.0 <3.0.0||>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz",
- "optional": true
- },
- "mv": {
- "version": "2.1.1",
- "from": "mv@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
- "optional": true
- },
- "once": {
- "version": "1.4.0",
- "from": "once@>=1.4.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
- },
- "rimraf": {
- "version": "2.4.5",
- "from": "rimraf@>=2.4.0 <2.5.0",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
- "optional": true
- },
- "vasync": {
- "version": "1.6.4",
- "from": "vasync@>=1.6.4 <2.0.0",
- "resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
- "dependencies": {
- "verror": {
- "version": "1.6.0",
- "from": "verror@1.6.0",
- "resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz"
- }
- }
- },
- "verror": {
- "version": "1.9.0",
- "from": "verror@>=1.8.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/verror/-/verror-1.9.0.tgz"
- }
- }
- },
- "ldapjs": {
- "version": "0.7.1",
- "from": "ldapjs@>=0.7.1 <0.8.0",
- "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-0.7.1.tgz",
- "dependencies": {
- "asn1": {
- "version": "0.2.1",
- "from": "asn1@0.2.1",
- "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.1.tgz"
- },
- "assert-plus": {
- "version": "0.1.5",
- "from": "assert-plus@0.1.5",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz"
- },
- "nopt": {
- "version": "2.1.1",
- "from": "nopt@2.1.1",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.1.tgz"
- }
- }
- },
- "libbase64": {
- "version": "0.1.0",
- "from": "libbase64@0.1.0",
- "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz"
- },
- "libmime": {
- "version": "2.0.0",
- "from": "libmime@2.0.0",
- "resolved": "https://registry.npmjs.org/libmime/-/libmime-2.0.0.tgz",
- "dependencies": {
- "iconv-lite": {
- "version": "0.4.13",
- "from": "iconv-lite@0.4.13",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz"
- }
- }
- },
- "libqp": {
- "version": "1.1.0",
- "from": "libqp@1.1.0",
- "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz"
- },
- "loads": {
- "version": "0.0.4",
- "from": "loads@>=0.0.0 <0.1.0",
- "resolved": "https://registry.npmjs.org/loads/-/loads-0.0.4.tgz"
- },
- "lodash": {
- "version": "4.17.2",
- "from": "lodash@>=4.13.1 <5.0.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.2.tgz"
- },
- "logger-sharelatex": {
- "version": "1.5.1",
- "from": "git+https://github.com/sharelatex/logger-sharelatex.git#master",
- "resolved": "git+https://github.com/sharelatex/logger-sharelatex.git#405cf1350ca5ae5f7bb1e7091e28d5aa3aaaa72c",
- "dependencies": {
- "bunyan": {
- "version": "1.5.1",
- "from": "bunyan@1.5.1",
- "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz"
- },
- "coffee-script": {
- "version": "1.4.0",
- "from": "coffee-script@1.4.0",
- "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz"
- },
- "dtrace-provider": {
- "version": "0.6.0",
- "from": "dtrace-provider@>=0.6.0 <0.7.0",
- "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz",
- "optional": true
- },
- "glob": {
- "version": "6.0.4",
- "from": "glob@>=6.0.1 <7.0.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
- "optional": true
- },
- "minimatch": {
- "version": "3.0.3",
- "from": "minimatch@>=2.0.0 <3.0.0||>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz",
- "optional": true
- },
- "mv": {
- "version": "2.1.1",
- "from": "mv@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
- "optional": true
- },
- "rimraf": {
- "version": "2.4.5",
- "from": "rimraf@>=2.4.0 <2.5.0",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
- "optional": true
- }
- }
- },
- "lolex": {
- "version": "1.3.2",
- "from": "lolex@1.3.2",
- "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz"
- },
- "lru-cache": {
- "version": "2.7.3",
- "from": "lru-cache@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
- },
- "lsmod": {
- "version": "0.0.3",
- "from": "lsmod@>=0.0.3 <0.1.0",
- "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-0.0.3.tgz"
- },
- "lynx": {
- "version": "0.1.1",
- "from": "lynx@0.1.1",
- "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz"
- },
- "mailcomposer": {
- "version": "3.3.2",
- "from": "mailcomposer@3.3.2",
- "resolved": "https://registry.npmjs.org/mailcomposer/-/mailcomposer-3.3.2.tgz"
- },
- "marked": {
- "version": "0.3.6",
- "from": "marked@>=0.3.5 <0.4.0",
- "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz"
- },
- "media-typer": {
- "version": "0.3.0",
- "from": "media-typer@0.3.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
- },
- "merge-descriptors": {
- "version": "1.0.0",
- "from": "merge-descriptors@1.0.0",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz"
- },
- "mersenne": {
- "version": "0.0.3",
- "from": "mersenne@>=0.0.3 <0.1.0",
- "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.3.tgz"
- },
- "method-override": {
- "version": "2.3.7",
- "from": "method-override@>=2.3.3 <3.0.0",
- "resolved": "https://registry.npmjs.org/method-override/-/method-override-2.3.7.tgz",
- "dependencies": {
- "debug": {
- "version": "2.3.3",
- "from": "debug@2.3.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz"
- },
- "ms": {
- "version": "0.7.2",
- "from": "ms@0.7.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz"
- },
- "vary": {
- "version": "1.1.0",
- "from": "vary@>=1.1.0 <1.2.0",
- "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.0.tgz"
- }
- }
- },
- "methods": {
- "version": "1.1.2",
- "from": "methods@>=1.1.1 <1.2.0",
- "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
- },
- "metrics-sharelatex": {
- "version": "1.6.0",
- "from": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.6.0",
- "resolved": "git+https://github.com/sharelatex/metrics-sharelatex.git#718f1144407ab2c867b869ebb38e07de2be1933b",
- "dependencies": {
- "coffee-script": {
- "version": "1.6.0",
- "from": "coffee-script@1.6.0",
- "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz"
- }
- }
- },
- "mime": {
- "version": "1.3.4",
- "from": "mime@1.3.4",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz"
- },
- "mime-db": {
- "version": "1.25.0",
- "from": "mime-db@>=1.25.0 <1.26.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz"
- },
- "mime-types": {
- "version": "2.1.13",
- "from": "mime-types@>=2.1.7 <2.2.0",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz"
- },
- "mimelib": {
- "version": "0.2.14",
- "from": "mimelib@0.2.14",
- "resolved": "https://registry.npmjs.org/mimelib/-/mimelib-0.2.14.tgz"
- },
- "minimatch": {
- "version": "0.2.14",
- "from": "minimatch@>=0.2.12 <0.3.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz"
- },
- "minimist": {
- "version": "0.0.8",
- "from": "minimist@0.0.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
- },
- "mkdirp": {
- "version": "0.5.1",
- "from": "mkdirp@>=0.5.0 <0.6.0",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz"
- },
- "mocha": {
- "version": "1.17.1",
- "from": "mocha@1.17.1",
- "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz",
- "dependencies": {
- "commander": {
- "version": "2.0.0",
- "from": "commander@2.0.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz"
- },
- "glob": {
- "version": "3.2.3",
- "from": "glob@3.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz"
- },
- "graceful-fs": {
- "version": "2.0.3",
- "from": "graceful-fs@>=2.0.0 <2.1.0",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz"
- },
- "jade": {
- "version": "0.26.3",
- "from": "jade@0.26.3",
- "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz",
- "dependencies": {
- "commander": {
- "version": "0.6.1",
- "from": "commander@0.6.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz"
- },
- "mkdirp": {
- "version": "0.3.0",
- "from": "mkdirp@0.3.0",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz"
- }
- }
- },
- "mkdirp": {
- "version": "0.3.5",
- "from": "mkdirp@0.3.5",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
- }
- }
- },
- "moment": {
- "version": "2.17.1",
- "from": "moment@>=2.10.6 <3.0.0",
- "resolved": "https://registry.npmjs.org/moment/-/moment-2.17.1.tgz"
- },
- "moment-timezone": {
- "version": "0.5.10",
- "from": "moment-timezone@>=0.5.4 <0.6.0",
- "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.10.tgz"
- },
- "mongodb": {
- "version": "2.2.16",
- "from": "mongodb@>=2.0.45 <3.0.0",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.16.tgz",
- "dependencies": {
- "isarray": {
- "version": "1.0.0",
- "from": "isarray@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
- },
- "readable-stream": {
- "version": "2.1.5",
- "from": "readable-stream@2.1.5",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz"
- }
- }
- },
- "mongodb-core": {
- "version": "2.1.2",
- "from": "mongodb-core@2.1.2",
- "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.2.tgz"
- },
- "mongojs": {
- "version": "2.4.0",
- "from": "mongojs@2.4.0",
- "resolved": "https://registry.npmjs.org/mongojs/-/mongojs-2.4.0.tgz",
- "dependencies": {
- "isarray": {
- "version": "1.0.0",
- "from": "isarray@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
- },
- "readable-stream": {
- "version": "2.2.2",
- "from": "readable-stream@>=2.0.2 <3.0.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.2.tgz"
- }
- }
- },
- "mongoose": {
- "version": "4.1.0",
- "from": "mongoose@4.1.0",
- "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.1.0.tgz",
- "dependencies": {
- "async": {
- "version": "0.9.0",
- "from": "async@0.9.0",
- "resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz"
- },
- "bson": {
- "version": "0.3.2",
- "from": "bson@>=0.3.0 <0.4.0",
- "resolved": "https://registry.npmjs.org/bson/-/bson-0.3.2.tgz"
- },
- "mongodb": {
- "version": "2.0.34",
- "from": "mongodb@2.0.34",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.0.34.tgz"
- },
- "mongodb-core": {
- "version": "1.2.0",
- "from": "mongodb-core@1.2.0",
- "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-1.2.0.tgz",
- "dependencies": {
- "bson": {
- "version": "0.4.23",
- "from": "bson@>=0.4.0 <0.5.0",
- "resolved": "https://registry.npmjs.org/bson/-/bson-0.4.23.tgz"
- }
- }
- },
- "ms": {
- "version": "0.1.0",
- "from": "ms@0.1.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.1.0.tgz"
- },
- "readable-stream": {
- "version": "1.0.31",
- "from": "readable-stream@1.0.31",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz"
- }
- }
- },
- "monocle": {
- "version": "1.1.51",
- "from": "monocle@1.1.51",
- "resolved": "https://registry.npmjs.org/monocle/-/monocle-1.1.51.tgz"
- },
- "mpath": {
- "version": "0.1.1",
- "from": "mpath@0.1.1",
- "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.1.1.tgz"
- },
- "mpromise": {
- "version": "0.5.4",
- "from": "mpromise@0.5.4",
- "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.4.tgz"
- },
- "mquery": {
- "version": "1.6.1",
- "from": "mquery@1.6.1",
- "resolved": "https://registry.npmjs.org/mquery/-/mquery-1.6.1.tgz",
- "dependencies": {
- "bluebird": {
- "version": "2.9.26",
- "from": "bluebird@2.9.26",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.26.tgz"
- },
- "debug": {
- "version": "2.2.0",
- "from": "debug@2.2.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
- },
- "ms": {
- "version": "0.7.1",
- "from": "ms@0.7.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
- }
- }
- },
- "ms": {
- "version": "0.6.2",
- "from": "ms@0.6.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz"
- },
- "multer": {
- "version": "0.1.8",
- "from": "multer@>=0.1.8 <0.2.0",
- "resolved": "https://registry.npmjs.org/multer/-/multer-0.1.8.tgz",
- "dependencies": {
- "mime-db": {
- "version": "1.12.0",
- "from": "mime-db@>=1.12.0 <1.13.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz"
- },
- "mime-types": {
- "version": "2.0.14",
- "from": "mime-types@>=2.0.9 <2.1.0",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz"
- },
- "mkdirp": {
- "version": "0.3.5",
- "from": "mkdirp@>=0.3.5 <0.4.0",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
- },
- "qs": {
- "version": "1.2.2",
- "from": "qs@>=1.2.2 <1.3.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-1.2.2.tgz"
- },
- "type-is": {
- "version": "1.5.7",
- "from": "type-is@>=1.5.2 <1.6.0",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz"
- }
- }
- },
- "muri": {
- "version": "1.0.0",
- "from": "muri@1.0.0",
- "resolved": "https://registry.npmjs.org/muri/-/muri-1.0.0.tgz"
- },
- "mv": {
- "version": "0.0.5",
- "from": "mv@0.0.5",
- "resolved": "https://registry.npmjs.org/mv/-/mv-0.0.5.tgz",
- "optional": true
- },
- "nan": {
- "version": "2.3.5",
- "from": "nan@2.3.5",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz"
- },
- "ncp": {
- "version": "2.0.0",
- "from": "ncp@>=2.0.0 <2.1.0",
- "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
- "optional": true
- },
- "negotiator": {
- "version": "0.5.3",
- "from": "negotiator@0.5.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz"
- },
- "node-forge": {
- "version": "0.2.24",
- "from": "node-forge@0.2.24",
- "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.2.24.tgz"
- },
- "node-pre-gyp": {
- "version": "0.6.30",
- "from": "node-pre-gyp@0.6.30",
- "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.30.tgz",
- "dependencies": {
- "glob": {
- "version": "7.1.1",
- "from": "glob@>=7.0.5 <8.0.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz"
- },
- "minimatch": {
- "version": "3.0.3",
- "from": "minimatch@>=3.0.2 <4.0.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz"
- },
- "rimraf": {
- "version": "2.5.4",
- "from": "rimraf@>=2.5.0 <2.6.0",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz"
- }
- }
- },
- "node-uuid": {
- "version": "1.4.1",
- "from": "node-uuid@1.4.1",
- "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.1.tgz"
- },
- "nodemailer": {
- "version": "2.1.0",
- "from": "nodemailer@2.1.0",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-2.1.0.tgz"
- },
- "nodemailer-direct-transport": {
- "version": "2.0.1",
- "from": "nodemailer-direct-transport@2.0.1",
- "resolved": "https://registry.npmjs.org/nodemailer-direct-transport/-/nodemailer-direct-transport-2.0.1.tgz"
- },
- "nodemailer-fetch": {
- "version": "1.2.1",
- "from": "nodemailer-fetch@1.2.1",
- "resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.2.1.tgz"
- },
- "nodemailer-sendgrid-transport": {
- "version": "0.2.0",
- "from": "nodemailer-sendgrid-transport@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/nodemailer-sendgrid-transport/-/nodemailer-sendgrid-transport-0.2.0.tgz"
- },
- "nodemailer-ses-transport": {
- "version": "1.5.0",
- "from": "nodemailer-ses-transport@>=1.3.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/nodemailer-ses-transport/-/nodemailer-ses-transport-1.5.0.tgz"
- },
- "nodemailer-shared": {
- "version": "1.0.3",
- "from": "nodemailer-shared@1.0.3",
- "resolved": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.0.3.tgz"
- },
- "nodemailer-smtp-pool": {
- "version": "2.1.0",
- "from": "nodemailer-smtp-pool@2.1.0",
- "resolved": "https://registry.npmjs.org/nodemailer-smtp-pool/-/nodemailer-smtp-pool-2.1.0.tgz"
- },
- "nodemailer-smtp-transport": {
- "version": "2.0.1",
- "from": "nodemailer-smtp-transport@2.0.1",
- "resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.0.1.tgz"
- },
- "nodemailer-wellknown": {
- "version": "0.1.7",
- "from": "nodemailer-wellknown@0.1.7",
- "resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.7.tgz"
- },
- "nopt": {
- "version": "3.0.6",
- "from": "nopt@>=3.0.1 <3.1.0",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz"
- },
- "npmlog": {
- "version": "4.0.2",
- "from": "npmlog@>=4.0.0 <5.0.0",
- "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.0.2.tgz"
- },
- "number-is-nan": {
- "version": "1.0.1",
- "from": "number-is-nan@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz"
- },
- "oauth-sign": {
- "version": "0.8.2",
- "from": "oauth-sign@>=0.8.1 <0.9.0",
- "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz"
- },
- "object-assign": {
- "version": "4.1.0",
- "from": "object-assign@>=4.1.0 <5.0.0",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz"
- },
- "on-finished": {
- "version": "2.3.0",
- "from": "on-finished@>=2.3.0 <2.4.0",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz"
- },
- "on-headers": {
- "version": "1.0.1",
- "from": "on-headers@>=1.0.1 <1.1.0",
- "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz"
- },
- "once": {
- "version": "1.3.3",
- "from": "once@>=1.3.0 <1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz"
- },
- "one-time": {
- "version": "0.0.4",
- "from": "one-time@>=0.0.0 <0.1.0",
- "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz"
- },
- "optimist": {
- "version": "0.6.1",
- "from": "optimist@0.6.1",
- "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz"
- },
- "os-tmpdir": {
- "version": "1.0.2",
- "from": "os-tmpdir@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz"
- },
- "parse-mongo-url": {
- "version": "1.1.1",
- "from": "parse-mongo-url@>=1.1.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/parse-mongo-url/-/parse-mongo-url-1.1.1.tgz"
- },
- "parseurl": {
- "version": "1.3.1",
- "from": "parseurl@>=1.3.0 <1.4.0",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz"
- },
- "passport": {
- "version": "0.3.2",
- "from": "passport@>=0.3.2 <0.4.0",
- "resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz"
- },
- "passport-ldapauth": {
- "version": "0.6.0",
- "from": "passport-ldapauth@>=0.6.0 <0.7.0",
- "resolved": "https://registry.npmjs.org/passport-ldapauth/-/passport-ldapauth-0.6.0.tgz"
- },
- "passport-local": {
- "version": "1.0.0",
- "from": "passport-local@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz"
- },
- "passport-saml": {
- "version": "0.15.0",
- "from": "passport-saml@>=0.15.0 <0.16.0",
- "resolved": "https://registry.npmjs.org/passport-saml/-/passport-saml-0.15.0.tgz",
- "dependencies": {
- "xml2js": {
- "version": "0.4.17",
- "from": "xml2js@>=0.4.0 <0.5.0",
- "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz",
- "dependencies": {
- "xmlbuilder": {
- "version": "4.2.1",
- "from": "xmlbuilder@>=4.1.0 <5.0.0",
- "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz"
- }
- }
- },
- "xmlbuilder": {
- "version": "2.5.2",
- "from": "xmlbuilder@>=2.5.0 <2.6.0",
- "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.5.2.tgz",
- "dependencies": {
- "lodash": {
- "version": "3.2.0",
- "from": "lodash@>=3.2.0 <3.3.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.2.0.tgz"
- }
- }
- }
- }
- },
- "passport-strategy": {
- "version": "1.0.0",
- "from": "passport-strategy@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz"
- },
- "path-is-absolute": {
- "version": "1.0.1",
- "from": "path-is-absolute@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
- },
- "path-to-regexp": {
- "version": "0.1.6",
- "from": "path-to-regexp@0.1.6",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.6.tgz"
- },
- "pause": {
- "version": "0.0.1",
- "from": "pause@0.0.1",
- "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz"
- },
- "pinkie": {
- "version": "2.0.4",
- "from": "pinkie@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz"
- },
- "pinkie-promise": {
- "version": "2.0.1",
- "from": "pinkie-promise@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz"
- },
- "pooling": {
- "version": "0.4.6",
- "from": "pooling@0.4.6",
- "resolved": "https://registry.npmjs.org/pooling/-/pooling-0.4.6.tgz",
- "dependencies": {
- "assert-plus": {
- "version": "0.1.5",
- "from": "assert-plus@0.1.5",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz"
- },
- "once": {
- "version": "1.3.0",
- "from": "once@1.3.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.3.0.tgz"
- }
- }
- },
- "precond": {
- "version": "0.2.3",
- "from": "precond@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz"
- },
- "process-nextick-args": {
- "version": "1.0.7",
- "from": "process-nextick-args@>=1.0.6 <1.1.0",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz"
- },
- "promise": {
- "version": "2.0.0",
- "from": "promise@>=2.0.0 <2.1.0",
- "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz"
- },
- "proxy-addr": {
- "version": "1.0.10",
- "from": "proxy-addr@>=1.0.8 <1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz"
- },
- "pseudomap": {
- "version": "1.0.2",
- "from": "pseudomap@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz"
- },
- "punycode": {
- "version": "1.4.1",
- "from": "punycode@>=1.4.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz"
- },
- "q": {
- "version": "1.1.2",
- "from": "q@>=1.1.0 <1.2.0",
- "resolved": "https://registry.npmjs.org/q/-/q-1.1.2.tgz"
- },
- "qs": {
- "version": "6.3.0",
- "from": "qs@>=6.3.0 <6.4.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.0.tgz"
- },
- "querystring": {
- "version": "0.2.0",
- "from": "querystring@0.2.0",
- "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz"
- },
- "random-bytes": {
- "version": "1.0.0",
- "from": "random-bytes@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz"
- },
- "range-parser": {
- "version": "1.0.3",
- "from": "range-parser@>=1.0.2 <1.1.0",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz"
- },
- "raven": {
- "version": "0.8.1",
- "from": "raven@>=0.8.0 <0.9.0",
- "resolved": "https://registry.npmjs.org/raven/-/raven-0.8.1.tgz",
- "dependencies": {
- "cookie": {
- "version": "0.1.0",
- "from": "cookie@0.1.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz"
- }
- }
- },
- "raw-body": {
- "version": "2.1.7",
- "from": "raw-body@>=2.1.7 <2.2.0",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.7.tgz",
- "dependencies": {
- "iconv-lite": {
- "version": "0.4.13",
- "from": "iconv-lite@0.4.13",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz"
- }
- }
- },
- "rc": {
- "version": "1.1.6",
- "from": "rc@>=1.1.0 <1.2.0",
- "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz",
- "dependencies": {
- "minimist": {
- "version": "1.2.0",
- "from": "minimist@>=1.2.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
- }
- }
- },
- "readable-stream": {
- "version": "1.0.34",
- "from": "readable-stream@>=1.0.24 <1.1.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
- },
- "readdirp": {
- "version": "0.2.5",
- "from": "readdirp@>=0.2.3 <0.3.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-0.2.5.tgz"
- },
- "redback": {
- "version": "0.4.0",
- "from": "redback@0.4.0",
- "resolved": "https://registry.npmjs.org/redback/-/redback-0.4.0.tgz"
- },
- "redis": {
- "version": "0.10.1",
- "from": "redis@0.10.1",
- "resolved": "https://registry.npmjs.org/redis/-/redis-0.10.1.tgz"
- },
- "redis-commands": {
- "version": "1.3.0",
- "from": "redis-commands@>=1.2.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.0.tgz"
- },
- "redis-parser": {
- "version": "2.3.0",
- "from": "redis-parser@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.3.0.tgz"
- },
- "redis-sentinel": {
- "version": "0.1.1",
- "from": "redis-sentinel@0.1.1",
- "resolved": "https://registry.npmjs.org/redis-sentinel/-/redis-sentinel-0.1.1.tgz",
- "dependencies": {
- "q": {
- "version": "0.9.2",
- "from": "q@0.9.2",
- "resolved": "https://registry.npmjs.org/q/-/q-0.9.2.tgz"
- },
- "redis": {
- "version": "0.11.0",
- "from": "redis@>=0.11.0 <0.12.0",
- "resolved": "https://registry.npmjs.org/redis/-/redis-0.11.0.tgz"
- }
- }
- },
- "redis-sharelatex": {
- "version": "0.0.9",
- "from": "redis-sharelatex@0.0.9",
- "resolved": "https://registry.npmjs.org/redis-sharelatex/-/redis-sharelatex-0.0.9.tgz",
- "dependencies": {
- "ansi-regex": {
- "version": "0.2.1",
- "from": "ansi-regex@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz"
- },
- "ansi-styles": {
- "version": "1.1.0",
- "from": "ansi-styles@>=1.1.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz"
- },
- "assertion-error": {
- "version": "1.0.0",
- "from": "assertion-error@1.0.0",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz"
- },
- "chai": {
- "version": "1.9.1",
- "from": "chai@1.9.1",
- "resolved": "https://registry.npmjs.org/chai/-/chai-1.9.1.tgz"
- },
- "chalk": {
- "version": "0.5.1",
- "from": "chalk@>=0.5.0 <0.6.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz"
- },
- "coffee-script": {
- "version": "1.8.0",
- "from": "coffee-script@1.8.0",
- "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.8.0.tgz"
- },
- "commander": {
- "version": "2.0.0",
- "from": "commander@2.0.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz"
- },
- "formatio": {
- "version": "1.0.2",
- "from": "formatio@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.0.2.tgz"
- },
- "glob": {
- "version": "3.2.3",
- "from": "glob@3.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz"
- },
- "graceful-fs": {
- "version": "2.0.3",
- "from": "graceful-fs@>=2.0.0 <2.1.0",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz"
- },
- "growl": {
- "version": "1.8.1",
- "from": "growl@>=1.8.0 <1.9.0",
- "resolved": "https://registry.npmjs.org/growl/-/growl-1.8.1.tgz"
- },
- "grunt-contrib-coffee": {
- "version": "0.11.1",
- "from": "grunt-contrib-coffee@0.11.1",
- "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.11.1.tgz",
- "dependencies": {
- "coffee-script": {
- "version": "1.7.1",
- "from": "coffee-script@>=1.7.0 <1.8.0",
- "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz"
- }
- }
- },
- "grunt-mocha-test": {
- "version": "0.12.0",
- "from": "grunt-mocha-test@0.12.0",
- "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.12.0.tgz"
- },
- "has-ansi": {
- "version": "0.1.0",
- "from": "has-ansi@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz"
- },
- "jade": {
- "version": "0.26.3",
- "from": "jade@0.26.3",
- "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz",
- "dependencies": {
- "commander": {
- "version": "0.6.1",
- "from": "commander@0.6.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz"
- },
- "mkdirp": {
- "version": "0.3.0",
- "from": "mkdirp@0.3.0",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz"
- }
- }
- },
- "lodash": {
- "version": "2.4.2",
- "from": "lodash@>=2.4.1 <2.5.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
- },
- "mkdirp": {
- "version": "0.3.5",
- "from": "mkdirp@>=0.3.5 <0.4.0",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
- },
- "mocha": {
- "version": "1.21.4",
- "from": "mocha@1.21.4",
- "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.21.4.tgz"
- },
- "redis": {
- "version": "0.12.1",
- "from": "redis@0.12.1",
- "resolved": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz"
- },
- "sandboxed-module": {
- "version": "1.0.1",
- "from": "sandboxed-module@1.0.1",
- "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-1.0.1.tgz"
- },
- "sinon": {
- "version": "1.10.3",
- "from": "sinon@1.10.3",
- "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.10.3.tgz"
- },
- "stack-trace": {
- "version": "0.0.9",
- "from": "stack-trace@0.0.9",
- "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz"
- },
- "strip-ansi": {
- "version": "0.3.0",
- "from": "strip-ansi@>=0.3.0 <0.4.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz"
- },
- "underscore": {
- "version": "1.7.0",
- "from": "underscore@1.7.0",
- "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz"
- }
- }
- },
- "regexp-clone": {
- "version": "0.0.1",
- "from": "regexp-clone@0.0.1",
- "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz"
- },
- "request": {
- "version": "2.79.0",
- "from": "request@>=2.69.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz"
- },
- "requests": {
- "version": "0.1.7",
- "from": "requests@>=0.1.7 <0.2.0",
- "resolved": "https://registry.npmjs.org/requests/-/requests-0.1.7.tgz",
- "dependencies": {
- "eventemitter3": {
- "version": "1.1.1",
- "from": "eventemitter3@>=1.1.0 <1.2.0",
- "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.1.1.tgz"
- }
- }
- },
- "require_optional": {
- "version": "1.0.0",
- "from": "require_optional@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.0.tgz"
- },
- "require-like": {
- "version": "0.1.2",
- "from": "require-like@0.1.2",
- "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz"
- },
- "requires-port": {
- "version": "1.0.0",
- "from": "requires-port@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz"
- },
- "resolve-from": {
- "version": "2.0.0",
- "from": "resolve-from@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz"
- },
- "retry-as-promised": {
- "version": "2.2.0",
- "from": "retry-as-promised@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-2.2.0.tgz",
- "dependencies": {
- "debug": {
- "version": "2.4.5",
- "from": "debug@>=2.2.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.4.5.tgz"
- },
- "ms": {
- "version": "0.7.2",
- "from": "ms@0.7.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz"
- }
- }
- },
- "rimraf": {
- "version": "2.2.6",
- "from": "rimraf@2.2.6",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz"
- },
- "rndm": {
- "version": "1.2.0",
- "from": "rndm@1.2.0",
- "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz"
- },
- "safe-json-stringify": {
- "version": "1.0.3",
- "from": "safe-json-stringify@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.3.tgz",
- "optional": true
- },
- "samsam": {
- "version": "1.1.2",
- "from": "samsam@1.1.2",
- "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz"
- },
- "sanitizer": {
- "version": "0.1.1",
- "from": "sanitizer@0.1.1",
- "resolved": "https://registry.npmjs.org/sanitizer/-/sanitizer-0.1.1.tgz"
- },
- "sax": {
- "version": "1.1.5",
- "from": "sax@1.1.5",
- "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.5.tgz"
- },
- "semver": {
- "version": "5.3.0",
- "from": "semver@>=5.3.0 <5.4.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz"
- },
- "send": {
- "version": "0.13.0",
- "from": "send@0.13.0",
- "resolved": "https://registry.npmjs.org/send/-/send-0.13.0.tgz",
- "dependencies": {
- "debug": {
- "version": "2.2.0",
- "from": "debug@>=2.2.0 <2.3.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
- },
- "depd": {
- "version": "1.0.1",
- "from": "depd@>=1.0.1 <1.1.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
- },
- "http-errors": {
- "version": "1.3.1",
- "from": "http-errors@>=1.3.1 <1.4.0",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz"
- },
- "ms": {
- "version": "0.7.1",
- "from": "ms@0.7.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
- },
- "statuses": {
- "version": "1.2.1",
- "from": "statuses@>=1.2.1 <1.3.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
- }
- }
- },
- "sendgrid": {
- "version": "1.9.2",
- "from": "sendgrid@>=1.8.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/sendgrid/-/sendgrid-1.9.2.tgz",
- "dependencies": {
- "lodash": {
- "version": "3.10.1",
- "from": "lodash@>=3.0.1 <4.0.0||>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz"
- }
- }
- },
- "sequelize": {
- "version": "3.28.0",
- "from": "sequelize@>=3.2.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-3.28.0.tgz",
- "dependencies": {
- "lodash": {
- "version": "4.12.0",
- "from": "lodash@4.12.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.12.0.tgz"
- },
- "node-uuid": {
- "version": "1.4.7",
- "from": "node-uuid@>=1.4.4 <1.5.0",
- "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz"
- }
- }
- },
- "serve-static": {
- "version": "1.10.3",
- "from": "serve-static@>=1.10.0 <1.11.0",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.10.3.tgz",
- "dependencies": {
- "debug": {
- "version": "2.2.0",
- "from": "debug@>=2.2.0 <2.3.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
- },
- "destroy": {
- "version": "1.0.4",
- "from": "destroy@>=1.0.4 <1.1.0",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz"
- },
- "escape-html": {
- "version": "1.0.3",
- "from": "escape-html@>=1.0.3 <1.1.0",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz"
- },
- "http-errors": {
- "version": "1.3.1",
- "from": "http-errors@>=1.3.1 <1.4.0",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz"
- },
- "ms": {
- "version": "0.7.1",
- "from": "ms@0.7.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
- },
- "send": {
- "version": "0.13.2",
- "from": "send@0.13.2",
- "resolved": "https://registry.npmjs.org/send/-/send-0.13.2.tgz"
- },
- "statuses": {
- "version": "1.2.1",
- "from": "statuses@>=1.2.1 <1.3.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
- }
- }
- },
- "set-blocking": {
- "version": "2.0.0",
- "from": "set-blocking@>=2.0.0 <2.1.0",
- "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"
- },
- "setprototypeof": {
- "version": "1.0.2",
- "from": "setprototypeof@1.0.2",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz"
- },
- "settings-sharelatex": {
- "version": "1.0.0",
- "from": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
- "resolved": "git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559",
- "dependencies": {
- "coffee-script": {
- "version": "1.6.0",
- "from": "coffee-script@1.6.0",
- "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz"
- }
- }
- },
- "shimmer": {
- "version": "1.1.0",
- "from": "shimmer@1.1.0",
- "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.1.0.tgz"
- },
- "sigmund": {
- "version": "1.0.1",
- "from": "sigmund@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
- },
- "signal-exit": {
- "version": "3.0.2",
- "from": "signal-exit@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz"
- },
- "sinon": {
- "version": "1.17.6",
- "from": "sinon@latest",
- "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.6.tgz"
- },
- "sixpack-client": {
- "version": "1.0.0",
- "from": "sixpack-client@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/sixpack-client/-/sixpack-client-1.0.0.tgz"
- },
- "sliced": {
- "version": "0.0.5",
- "from": "sliced@0.0.5",
- "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz"
- },
- "smtp-connection": {
- "version": "2.0.1",
- "from": "smtp-connection@2.0.1",
- "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.0.1.tgz"
- },
- "smtpapi": {
- "version": "1.2.0",
- "from": "smtpapi@>=1.2.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/smtpapi/-/smtpapi-1.2.0.tgz"
- },
- "sntp": {
- "version": "1.0.9",
- "from": "sntp@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz"
- },
- "source-map": {
- "version": "0.1.34",
- "from": "source-map@0.1.34",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz"
- },
- "sshpk": {
- "version": "1.10.1",
- "from": "sshpk@>=1.7.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.10.1.tgz",
- "dependencies": {
- "assert-plus": {
- "version": "1.0.0",
- "from": "assert-plus@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz"
- }
- }
- },
- "stack-trace": {
- "version": "0.0.7",
- "from": "stack-trace@0.0.7",
- "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.7.tgz"
- },
- "statsd-parser": {
- "version": "0.0.4",
- "from": "statsd-parser@>=0.0.4 <0.1.0",
- "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz"
- },
- "statuses": {
- "version": "1.3.1",
- "from": "statuses@>=1.3.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz"
- },
- "stream-consume": {
- "version": "0.1.0",
- "from": "stream-consume@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.0.tgz"
- },
- "streamsearch": {
- "version": "0.1.2",
- "from": "streamsearch@0.1.2",
- "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz"
- },
- "string_decoder": {
- "version": "0.10.31",
- "from": "string_decoder@>=0.10.0 <0.11.0",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
- },
- "string-width": {
- "version": "1.0.2",
- "from": "string-width@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz"
- },
- "stringstream": {
- "version": "0.0.5",
- "from": "stringstream@>=0.0.4 <0.1.0",
- "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz"
- },
- "strip-ansi": {
- "version": "3.0.1",
- "from": "strip-ansi@>=3.0.1 <4.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz"
- },
- "strip-json-comments": {
- "version": "1.0.4",
- "from": "strip-json-comments@>=1.0.4 <1.1.0",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz"
- },
- "supports-color": {
- "version": "0.2.0",
- "from": "supports-color@>=0.2.0 <0.3.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz"
- },
- "tar": {
- "version": "2.2.1",
- "from": "tar@>=2.2.0 <2.3.0",
- "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz"
- },
- "tar-pack": {
- "version": "3.1.4",
- "from": "tar-pack@>=3.1.0 <3.2.0",
- "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.1.4.tgz",
- "dependencies": {
- "debug": {
- "version": "2.2.0",
- "from": "debug@>=2.2.0 <2.3.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
- },
- "glob": {
- "version": "7.1.1",
- "from": "glob@>=7.0.5 <8.0.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz"
- },
- "isarray": {
- "version": "1.0.0",
- "from": "isarray@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
- },
- "minimatch": {
- "version": "3.0.3",
- "from": "minimatch@>=3.0.2 <4.0.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz"
- },
- "ms": {
- "version": "0.7.1",
- "from": "ms@0.7.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
- },
- "readable-stream": {
- "version": "2.1.5",
- "from": "readable-stream@>=2.1.4 <2.2.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz"
- },
- "rimraf": {
- "version": "2.5.4",
- "from": "rimraf@>=2.5.1 <2.6.0",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz"
- }
- }
- },
- "tar-stream": {
- "version": "0.3.3",
- "from": "tar-stream@>=0.3.0 <0.4.0",
- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-0.3.3.tgz"
- },
- "temp": {
- "version": "0.8.3",
- "from": "temp@>=0.8.3 <0.9.0",
- "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz"
- },
- "terraformer": {
- "version": "1.0.7",
- "from": "terraformer@>=1.0.5 <1.1.0",
- "resolved": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.7.tgz"
- },
- "terraformer-wkt-parser": {
- "version": "1.1.2",
- "from": "terraformer-wkt-parser@>=1.1.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.1.2.tgz"
- },
- "thunky": {
- "version": "0.1.0",
- "from": "thunky@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz"
- },
- "timekeeper": {
- "version": "1.0.0",
- "from": "timekeeper@latest",
- "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-1.0.0.tgz"
- },
- "to-mongodb-core": {
- "version": "2.0.0",
- "from": "to-mongodb-core@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/to-mongodb-core/-/to-mongodb-core-2.0.0.tgz"
- },
- "toposort-class": {
- "version": "1.0.1",
- "from": "toposort-class@>=1.0.1 <2.0.0",
- "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz"
- },
- "tough-cookie": {
- "version": "2.3.2",
- "from": "tough-cookie@>=2.3.0 <2.4.0",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz"
- },
- "transformers": {
- "version": "2.1.0",
- "from": "transformers@2.1.0",
- "resolved": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz",
- "dependencies": {
- "optimist": {
- "version": "0.3.7",
- "from": "optimist@>=0.3.5 <0.4.0",
- "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz"
- },
- "uglify-js": {
- "version": "2.2.5",
- "from": "uglify-js@>=2.2.5 <2.3.0",
- "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz"
- }
- }
- },
- "tsscmp": {
- "version": "1.0.5",
- "from": "tsscmp@1.0.5",
- "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz"
- },
- "tunnel-agent": {
- "version": "0.4.3",
- "from": "tunnel-agent@>=0.4.1 <0.5.0",
- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz"
- },
- "tweetnacl": {
- "version": "0.14.5",
- "from": "tweetnacl@>=0.14.0 <0.15.0",
- "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
- "optional": true
- },
- "type-detect": {
- "version": "1.0.0",
- "from": "type-detect@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz"
- },
- "type-is": {
- "version": "1.6.14",
- "from": "type-is@>=1.6.13 <1.7.0",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.14.tgz"
- },
- "uglify-js": {
- "version": "2.4.24",
- "from": "uglify-js@>=2.4.0 <2.5.0",
- "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz",
- "dependencies": {
- "async": {
- "version": "0.2.10",
- "from": "async@>=0.2.6 <0.3.0",
- "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
- }
- }
- },
- "uglify-to-browserify": {
- "version": "1.0.2",
- "from": "uglify-to-browserify@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz"
- },
- "uid-number": {
- "version": "0.0.6",
- "from": "uid-number@>=0.0.6 <0.1.0",
- "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz"
- },
- "uid-safe": {
- "version": "2.1.3",
- "from": "uid-safe@2.1.3",
- "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.3.tgz"
- },
- "underscore": {
- "version": "1.6.0",
- "from": "underscore@1.6.0",
- "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz"
- },
- "underscore.string": {
- "version": "2.2.1",
- "from": "underscore.string@>=2.2.1 <2.3.0",
- "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz"
- },
- "unpipe": {
- "version": "1.0.0",
- "from": "unpipe@1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
- },
- "url": {
- "version": "0.10.3",
- "from": "url@0.10.3",
- "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
- "dependencies": {
- "punycode": {
- "version": "1.3.2",
- "from": "punycode@1.3.2",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz"
- }
- }
- },
- "util": {
- "version": "0.10.3",
- "from": "util@>=0.10.3 <1",
- "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
- "dependencies": {
- "inherits": {
- "version": "2.0.1",
- "from": "inherits@2.0.1",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
- }
- }
- },
- "util-deprecate": {
- "version": "1.0.2",
- "from": "util-deprecate@>=1.0.1 <1.1.0",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
- },
- "utils-merge": {
- "version": "1.0.0",
- "from": "utils-merge@1.0.0",
- "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz"
- },
- "uuid": {
- "version": "3.0.1",
- "from": "uuid@>=3.0.0 <4.0.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz"
- },
- "v8-profiler": {
- "version": "5.6.5",
- "from": "v8-profiler@>=5.2.3 <6.0.0",
- "resolved": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.6.5.tgz"
- },
- "validator": {
- "version": "5.7.0",
- "from": "validator@>=5.2.0 <6.0.0",
- "resolved": "https://registry.npmjs.org/validator/-/validator-5.7.0.tgz"
- },
- "vary": {
- "version": "1.0.1",
- "from": "vary@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz"
- },
- "vasync": {
- "version": "1.4.0",
- "from": "vasync@1.4.0",
- "resolved": "https://registry.npmjs.org/vasync/-/vasync-1.4.0.tgz",
- "dependencies": {
- "extsprintf": {
- "version": "1.0.0",
- "from": "extsprintf@1.0.0",
- "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz"
- },
- "json-schema": {
- "version": "0.2.2",
- "from": "json-schema@0.2.2",
- "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz"
- },
- "jsprim": {
- "version": "0.3.0",
- "from": "jsprim@0.3.0",
- "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-0.3.0.tgz",
- "dependencies": {
- "verror": {
- "version": "1.3.3",
- "from": "verror@1.3.3",
- "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.3.tgz"
- }
- }
- },
- "verror": {
- "version": "1.1.0",
- "from": "verror@1.1.0",
- "resolved": "https://registry.npmjs.org/verror/-/verror-1.1.0.tgz"
- }
- }
- },
- "verror": {
- "version": "1.3.6",
- "from": "verror@1.3.6",
- "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz"
- },
- "which": {
- "version": "1.0.9",
- "from": "which@>=1.0.5 <1.1.0",
- "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz"
- },
- "wide-align": {
- "version": "1.1.0",
- "from": "wide-align@>=1.1.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.0.tgz"
- },
- "window-size": {
- "version": "0.1.0",
- "from": "window-size@0.1.0",
- "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz"
- },
- "with": {
- "version": "3.0.1",
- "from": "with@>=3.0.0 <3.1.0",
- "resolved": "https://registry.npmjs.org/with/-/with-3.0.1.tgz"
- },
- "wkx": {
- "version": "0.2.0",
- "from": "wkx@0.2.0",
- "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.2.0.tgz"
- },
- "wordwrap": {
- "version": "0.0.2",
- "from": "wordwrap@0.0.2",
- "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz"
- },
- "wrappy": {
- "version": "1.0.2",
- "from": "wrappy@>=1.0.0 <2.0.0",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
- },
- "xhr-response": {
- "version": "1.0.1",
- "from": "xhr-response@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/xhr-response/-/xhr-response-1.0.1.tgz"
- },
- "xhr-send": {
- "version": "1.0.0",
- "from": "xhr-send@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/xhr-send/-/xhr-send-1.0.0.tgz"
- },
- "xhr-status": {
- "version": "1.0.0",
- "from": "xhr-status@>=1.0.0 <1.1.0",
- "resolved": "https://registry.npmjs.org/xhr-status/-/xhr-status-1.0.0.tgz"
- },
- "xml-crypto": {
- "version": "0.8.5",
- "from": "xml-crypto@>=0.8.0 <0.9.0",
- "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-0.8.5.tgz",
- "dependencies": {
- "xmldom": {
- "version": "0.1.19",
- "from": "xmldom@0.1.19",
- "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.19.tgz"
- }
- }
- },
- "xml-encryption": {
- "version": "0.7.4",
- "from": "xml-encryption@>=0.7.0 <0.8.0",
- "resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-0.7.4.tgz",
- "dependencies": {
- "async": {
- "version": "0.2.10",
- "from": "async@>=0.2.7 <0.3.0",
- "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
- }
- }
- },
- "xml2js": {
- "version": "0.2.0",
- "from": "xml2js@0.2.0",
- "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.0.tgz"
- },
- "xmlbuilder": {
- "version": "2.6.2",
- "from": "xmlbuilder@2.6.2",
- "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.6.2.tgz",
- "dependencies": {
- "lodash": {
- "version": "3.5.0",
- "from": "lodash@>=3.5.0 <3.6.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.5.0.tgz"
- }
- }
- },
- "xmldom": {
- "version": "0.1.27",
- "from": "xmldom@>=0.1.0 <0.2.0",
- "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz"
- },
- "xpath": {
- "version": "0.0.5",
- "from": "xpath@0.0.5",
- "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.5.tgz"
- },
- "xpath.js": {
- "version": "1.0.7",
- "from": "xpath.js@>=0.0.3",
- "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.0.7.tgz"
- },
- "xtend": {
- "version": "4.0.1",
- "from": "xtend@>=4.0.0 <5.0.0",
- "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
- },
- "yallist": {
- "version": "2.0.0",
- "from": "yallist@>=2.0.0 <3.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.0.0.tgz"
- },
- "yargs": {
- "version": "3.5.4",
- "from": "yargs@>=3.5.4 <3.6.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz"
- },
- "zip-stream": {
- "version": "0.3.7",
- "from": "zip-stream@>=0.3.0 <0.4.0",
- "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-0.3.7.tgz",
- "dependencies": {
- "lodash": {
- "version": "2.4.2",
- "from": "lodash@>=2.4.1 <2.5.0",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
- }
- }
- }
- }
+ "version": "0.1.4"
}
diff --git a/services/web/package.json b/services/web/package.json
index 7fff540f1e..de95b0a15a 100644
--- a/services/web/package.json
+++ b/services/web/package.json
@@ -42,7 +42,6 @@
"mongojs": "2.4.0",
"mongoose": "4.1.0",
"multer": "^0.1.8",
- "node-uuid": "1.4.1",
"nodemailer": "2.1.0",
"nodemailer-sendgrid-transport": "^0.2.0",
"nodemailer-ses-transport": "^1.3.0",
@@ -50,7 +49,6 @@
"passport": "^0.3.2",
"passport-ldapauth": "^0.6.0",
"passport-local": "^1.0.0",
- "redback": "0.4.0",
"redis": "0.10.1",
"redis-sharelatex": "0.0.9",
"request": "^2.69.0",
@@ -64,14 +62,19 @@
"underscore": "1.6.0",
"v8-profiler": "^5.2.3",
"xml2js": "0.2.0",
- "passport-saml": "^0.15.0"
+ "passport-saml": "^0.15.0",
+ "pug": "^2.0.0-beta6",
+ "uuid": "^3.0.1",
+ "rolling-rate-limiter": "git+https://github.com/ShaneKilkelly/rolling-rate-limiter.git#master"
},
"devDependencies": {
+ "autoprefixer": "^6.6.1",
"bunyan": "0.22.1",
- "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master",
"chai": "",
"chai-spies": "",
"grunt": "0.4.1",
+ "clean-css": "^3.4.18",
+ "es6-promise": "^4.0.5",
"grunt-available-tasks": "0.4.1",
"grunt-bunyan": "0.5.0",
"grunt-contrib-clean": "0.5.0",
@@ -80,7 +83,6 @@
"grunt-contrib-requirejs": "0.4.1",
"grunt-contrib-watch": "^1.0.0",
"grunt-env": "0.4.4",
- "clean-css": "^3.4.18",
"grunt-exec": "^0.4.7",
"grunt-execute": "^0.2.2",
"grunt-file-append": "0.0.6",
@@ -88,9 +90,11 @@
"grunt-mocha-test": "0.9.0",
"grunt-newer": "^1.2.0",
"grunt-parallel": "^0.5.1",
+ "grunt-postcss": "^0.8.0",
"grunt-sed": "^0.1.1",
"sandboxed-module": "0.2.0",
"sinon": "",
- "timekeeper": ""
+ "timekeeper": "",
+ "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master"
}
}
diff --git a/services/web/public/coffee/directives/asyncForm.coffee b/services/web/public/coffee/directives/asyncForm.coffee
index 2c6345d878..d9ca11231b 100644
--- a/services/web/public/coffee/directives/asyncForm.coffee
+++ b/services/web/public/coffee/directives/asyncForm.coffee
@@ -33,6 +33,11 @@ define [
response.success = true
response.error = false
+ onSuccessHandler = scope[attrs.onSuccess]
+ if onSuccessHandler
+ onSuccessHandler(data, status, headers, config)
+ return
+
if data.redir?
ga('send', 'event', formName, 'success')
window.location = data.redir
@@ -50,6 +55,12 @@ define [
scope[attrs.name].inflight = false
response.success = false
response.error = true
+
+ onErrorHandler = scope[attrs.onError]
+ if onErrorHandler
+ onErrorHandler(data, status, headers, config)
+ return
+
if status == 403 # Forbidden
response.message =
text: "Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies."
diff --git a/services/web/public/coffee/directives/expandableTextArea.coffee b/services/web/public/coffee/directives/expandableTextArea.coffee
new file mode 100644
index 0000000000..d0bfa9cb99
--- /dev/null
+++ b/services/web/public/coffee/directives/expandableTextArea.coffee
@@ -0,0 +1,17 @@
+define [
+ "base"
+], (App) ->
+ App.directive "expandableTextArea", () ->
+ restrict: "A"
+ link: (scope, el) ->
+ resetHeight = () ->
+ curHeight = el.outerHeight()
+ fitHeight = el.prop("scrollHeight")
+
+ if fitHeight > curHeight and el.val() != ""
+ scope.$emit "expandable-text-area:resize"
+ el.css("height", fitHeight)
+
+ scope.$watch (() -> el.val()), resetHeight
+
+
\ No newline at end of file
diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee
index 370f144ee1..08531f993a 100644
--- a/services/web/public/coffee/ide.coffee
+++ b/services/web/public/coffee/ide.coffee
@@ -28,6 +28,7 @@ define [
"directives/onEnter"
"directives/stopPropagation"
"directives/rightClick"
+ "directives/expandableTextArea"
"services/queued-http"
"filters/formatDate"
"main/event"
@@ -57,9 +58,6 @@ define [
else
this.$originalApply(fn);
- if window.location.search.match /tcon=true/ # track changes on
- $scope.trackChangesFeatureFlag = true
-
$scope.state = {
loading: true
load_progress: 40
@@ -70,7 +68,7 @@ define [
view: "editor"
chatOpen: false
pdfLayout: 'sideBySide'
- reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}") and $scope.trackChangesFeatureFlag
+ reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}")
showCodeCheckerOnboarding: !window.userSettings.syntaxValidation?
}
$scope.user = window.user
@@ -86,6 +84,7 @@ define [
ide.toggleReviewPanel = $scope.toggleReviewPanel = () ->
$scope.ui.reviewPanelOpen = !$scope.ui.reviewPanelOpen
+ event_tracking.sendMB "rp-toggle-panel", { value : $scope.ui.reviewPanelOpen }
$scope.$watch "ui.reviewPanelOpen", (value) ->
if value?
diff --git a/services/web/public/coffee/ide/chat/services/chatMessages.coffee b/services/web/public/coffee/ide/chat/services/chatMessages.coffee
index 1205a51267..6c33f95ea7 100644
--- a/services/web/public/coffee/ide/chat/services/chatMessages.coffee
+++ b/services/web/public/coffee/ide/chat/services/chatMessages.coffee
@@ -1,5 +1,6 @@
define [
"base"
+ "libs/md5"
], (App) ->
App.factory "chatMessages", ($http, ide) ->
MESSAGES_URL = "/project/#{ide.project_id}/messages"
@@ -72,7 +73,7 @@ define [
firstMessage.contents.unshift message.content
else
chat.state.messages.unshift({
- user: message.user
+ user: formatUser(message.user)
timestamp: message.timestamp
contents: [message.content]
})
@@ -93,9 +94,14 @@ define [
lastMessage.contents.push message.content
else
chat.state.messages.push({
- user: message.user
+ user: formatUser(message.user)
timestamp: message.timestamp
contents: [message.content]
})
+
+ formatUser = (user) ->
+ hash = CryptoJS.MD5(user.email.toLowerCase())
+ user.gravatar_url = "//www.gravatar.com/avatar/#{hash}"
+ return user
return chat
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/connection/ConnectionManager.coffee b/services/web/public/coffee/ide/connection/ConnectionManager.coffee
index afbc656a02..a8696fc999 100644
--- a/services/web/public/coffee/ide/connection/ConnectionManager.coffee
+++ b/services/web/public/coffee/ide/connection/ConnectionManager.coffee
@@ -1,3 +1,4 @@
+
define [], () ->
ONEHOUR = 1000 * 60 * 60
class ConnectionManager
diff --git a/services/web/public/coffee/ide/directives/layout.coffee b/services/web/public/coffee/ide/directives/layout.coffee
index 7fc459a539..f20a37b342 100644
--- a/services/web/public/coffee/ide/directives/layout.coffee
+++ b/services/web/public/coffee/ide/directives/layout.coffee
@@ -117,5 +117,10 @@ define [
element.layout().hide("east")
else
element.layout().show("east")
+
+ post: (scope, element, attrs) ->
+ name = attrs.layout
+ state = element.layout().readState()
+ scope.$broadcast "layout:#{name}:linked", state
}
]
diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee
index 17b1d9e28f..1de01b5467 100644
--- a/services/web/public/coffee/ide/editor/Document.coffee
+++ b/services/web/public/coffee/ide/editor/Document.coffee
@@ -1,7 +1,8 @@
define [
"utils/EventEmitter"
"ide/editor/ShareJsDoc"
-], (EventEmitter, ShareJsDoc) ->
+ "ide/review-panel/RangesTracker"
+], (EventEmitter, ShareJsDoc, RangesTracker) ->
class Document extends EventEmitter
@getDocument: (ide, doc_id) ->
@openDocs ||= {}
@@ -40,6 +41,8 @@ define [
editorDoc = @ace?.getSession().getDocument()
editorDoc?.off "change", @_checkConsistency
@ide.$scope.$emit 'document:closed', @doc
+
+ submitOp: (args...) -> @doc?.submitOp(args...)
_checkConsistency: () ->
# We've been seeing a lot of errors when I think there shouldn't be
@@ -77,6 +80,15 @@ define [
hasBufferedOps: () ->
@doc?.hasBufferedOps()
+
+ setTrackingChanges: (track_changes) ->
+ @doc.track_changes = track_changes
+
+ getTrackingChanges: () ->
+ !!@doc.track_changes
+
+ setTrackChangesIdSeeds: (id_seeds) ->
+ @doc.track_changes_id_seeds = id_seeds
_bindToSocketEvents: () ->
@_onUpdateAppliedHandler = (update) => @_onUpdateApplied(update)
@@ -239,16 +251,18 @@ define [
_joinDoc: (callback = (error) ->) ->
if @doc?
- @ide.socket.emit 'joinDoc', @doc_id, @doc.getVersion(), (error, docLines, version, updates) =>
+ @ide.socket.emit 'joinDoc', @doc_id, @doc.getVersion(), (error, docLines, version, updates, ranges) =>
return callback(error) if error?
@joined = true
@doc.catchUp( updates )
+ @_catchUpRanges( ranges?.changes, ranges?.comments )
callback()
else
- @ide.socket.emit 'joinDoc', @doc_id, (error, docLines, version) =>
+ @ide.socket.emit 'joinDoc', @doc_id, (error, docLines, version, updates, ranges) =>
return callback(error) if error?
@joined = true
@doc = new ShareJsDoc @doc_id, docLines, version, @ide.socket
+ @ranges = new RangesTracker(ranges?.changes, ranges?.comments)
@_bindToShareJsDocEvents()
callback()
@@ -307,6 +321,10 @@ define [
inflightOp: inflightOp,
pendingOp: pendingOp
v: version
+ @doc.on "change", (ops, oldSnapshot, msg) =>
+ @_applyOpsToRanges(ops, oldSnapshot, msg)
+ @doc.on "flipped_pending_to_inflight", () =>
+ @trigger "flipped_pending_to_inflight"
_onError: (error, meta = {}) ->
meta.doc_id = @doc_id
@@ -319,3 +337,34 @@ define [
# the disconnect event, which means we try to leaveDoc when the connection comes back.
# This could intefere with the new connection of a new instance of this document.
@_cleanUp()
+
+ _applyOpsToRanges: (ops = [], oldSnapshot, msg) ->
+ track_changes_as = null
+ remote_op = msg?
+ if msg?.meta?.tc?
+ old_id_seed = @ranges.getIdSeed()
+ @ranges.setIdSeed(msg.meta.tc)
+ if remote_op and msg.meta?.tc
+ track_changes_as = msg.meta.user_id
+ else if !remote_op and @track_changes_as?
+ track_changes_as = @track_changes_as
+ @ranges.track_changes = track_changes_as?
+ for op in ops
+ @ranges.applyOp op, { user_id: track_changes_as }
+ if old_id_seed?
+ @ranges.setIdSeed(old_id_seed)
+
+ _catchUpRanges: (changes = [], comments = []) ->
+ # We've just been given the current server's ranges, but need to apply any local ops we have.
+ # Reset to the server state then apply our local ops again.
+ @ranges.emit "clear"
+ @ranges.changes = changes
+ @ranges.comments = comments
+ @ranges.track_changes = @doc.track_changes
+ for op in @doc.getInflightOp() or []
+ @ranges.setIdSeed(@doc.track_changes_id_seeds.inflight)
+ @ranges.applyOp(op, { user_id: @track_changes_as })
+ for op in @doc.getPendingOp() or []
+ @ranges.setIdSeed(@doc.track_changes_id_seeds.pending)
+ @ranges.applyOp(op, { user_id: @track_changes_as })
+ @ranges.emit "redraw"
diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee
index eb063c9c6a..e01a12cb8b 100644
--- a/services/web/public/coffee/ide/editor/EditorManager.coffee
+++ b/services/web/public/coffee/ide/editor/EditorManager.coffee
@@ -10,6 +10,8 @@ define [
open_doc_id: null
open_doc_name: null
opening: true
+ trackChanges: false
+ wantTrackChanges: false
}
@$scope.$on "entity:selected", (event, entity) =>
@@ -31,6 +33,14 @@ define [
@$scope.$on "flush-changes", () =>
Document.flushAll()
+
+ @$scope.$watch "editor.wantTrackChanges", (value) =>
+ return if !value?
+ @_syncTrackChangesState(@$scope.editor.sharejs_doc)
+
+ @$scope.$watch "project.features.trackChanges", (trackChangesFeature) =>
+ return if !trackChangesFeature?
+ @$scope.editor.wantTrackChanges = window.trackChangesEnabled and trackChangesFeature
autoOpenDoc: () ->
open_doc_id =
@@ -83,6 +93,8 @@ define [
"Sorry, something went wrong opening this document. Please try again."
)
return
+
+ @_syncTrackChangesState(sharejs_doc)
@$scope.$broadcast "doc:opened"
@@ -144,3 +156,26 @@ define [
stopIgnoringExternalUpdates: () ->
@_ignoreExternalUpdates = false
+
+ _syncTimeout: null
+ _syncTrackChangesState: (doc) ->
+ return if !doc?
+
+ if @_syncTimeout?
+ clearTimeout @_syncTimeout
+ @_syncTimeout = null
+
+ want = @$scope.editor.wantTrackChanges
+ have = doc.getTrackingChanges()
+ if want == have
+ @$scope.editor.trackChanges = want
+ return
+
+ do tryToggle = () =>
+ saved = !doc.getInflightOp()? and !doc.getPendingOp()?
+ if saved
+ doc.setTrackingChanges(want)
+ @$scope.$apply () =>
+ @$scope.editor.trackChanges = want
+ else
+ @_syncTimeout = setTimeout tryToggle, 100
diff --git a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee
index 5d8b4ef11a..f580c56f77 100644
--- a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee
+++ b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee
@@ -9,21 +9,9 @@ define [
# Dencode any binary bits of data
# See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
@type = "text"
- docLines = for line in docLines
- if line.text?
- @type = "json"
- line.text = decodeURIComponent(escape(line.text))
- else
- @type = "text"
- line = decodeURIComponent(escape(line))
- line
-
- if @type == "text"
- snapshot = docLines.join("\n")
- else if @type == "json"
- snapshot = { lines: docLines }
- else
- throw new Error("Unknown type: #{@type}")
+ docLines = (decodeURIComponent(escape(line)) for line in docLines)
+ snapshot = docLines.join("\n")
+ @track_changes = false
@connection = {
send: (update) =>
@@ -34,6 +22,9 @@ define [
if window.dropUpdates? and Math.random() < window.dropUpdates
sl_console.log "Simulating a lost update", update
return
+ if @track_changes
+ update.meta ?= {}
+ update.meta.tc = @track_changes_id_seeds.inflight
@socket.emit "applyOtUpdate", @doc_id, update, (error) =>
return @_handleError(error) if error?
state: "ok"
@@ -43,8 +34,8 @@ define [
@_doc = new ShareJs.Doc @connection, @doc_id,
type: @type
@_doc.setFlushDelay(SINGLE_USER_FLUSH_DELAY)
- @_doc.on "change", () =>
- @trigger "change"
+ @_doc.on "change", (args...) =>
+ @trigger "change", args...
@_doc.on "acknowledge", () =>
@lastAcked = new Date() # note time of last ack from server for an op we sent
@trigger "acknowledge"
@@ -53,6 +44,8 @@ define [
# ops as quickly as possible for low latency.
@_doc.setFlushDelay(0)
@trigger "remoteop", args...
+ @_doc.on "flipped_pending_to_inflight", () =>
+ @trigger "flipped_pending_to_inflight"
@_doc.on "error", (e) =>
@_handleError(e)
@@ -70,6 +63,7 @@ define [
@_doc._onMessage message
catch error
# Version mismatches are thrown as errors
+ console.log error
@_handleError(error)
if message?.meta?.type == "external"
@@ -125,7 +119,7 @@ define [
attachToAce: (ace) -> @_doc.attach_ace(ace, false, window.maxDocLength)
detachFromAce: () -> @_doc.detach_ace?()
-
+
INFLIGHT_OP_TIMEOUT: 5000 # Retry sending ops after 5 seconds without an ack
WAIT_FOR_CONNECTION_TIMEOUT: 500 # If we're waiting for the project to join, try again in 0.5 seconds
_startInflightOpTimeout: (update) ->
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
index b17f3a1268..9205cb7b87 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
@@ -13,7 +13,7 @@ define [
EditSession = ace.require('ace/edit_session').EditSession
ModeList = ace.require('ace/ext/modelist')
- # set the path for ace workers if using a CDN (from editor.jade)
+ # set the path for ace workers if using a CDN (from editor.pug)
if window.aceWorkerPath != ""
syntaxValidationEnabled = true
ace.config.set('workerPath', "#{window.aceWorkerPath}")
@@ -54,10 +54,10 @@ define [
syntaxValidation: "="
reviewPanel: "="
eventsBridge: "="
- trackNewChanges: "="
+ trackChanges: "="
trackChangesEnabled: "="
- changesTracker: "="
docId: "="
+ rendererData: "="
}
link: (scope, element, attrs) ->
# Don't freak out if we're already in an apply callback
@@ -167,10 +167,22 @@ define [
if arg == "/"
ace.require("ace/ext/searchbox").Search(editor, true)
+ getCursorScreenPosition = () ->
+ session = editor.getSession()
+ cursorPosition = session.selection.getCursor()
+ sessionPos = session.documentToScreenPosition(cursorPosition.row, cursorPosition.column)
+ screenPos = editor.renderer.textToScreenCoordinates(sessionPos.row, sessionPos.column)
+ return sessionPos.row * editor.renderer.lineHeight - session.getScrollTop()
+
if attrs.resizeOn?
for event in attrs.resizeOn.split(",")
scope.$on event, () ->
+ previousScreenPosition = getCursorScreenPosition()
editor.resize()
+ # Put cursor back to same vertical position on screen
+ newScreenPosition = getCursorScreenPosition()
+ session = editor.getSession()
+ session.setScrollTop(session.getScrollTop() + newScreenPosition - previousScreenPosition)
scope.$watch "theme", (value) ->
editor.setTheme("ace/theme/#{value}")
@@ -279,7 +291,7 @@ define [
session.setUseWrapMode(true)
# use syntax validation only when explicitly set
- if scope.syntaxValidation? and syntaxValidationEnabled
+ if scope.syntaxValidation? and syntaxValidationEnabled and !scope.fileName.match(/\.bib$/)
session.setOption("useWorker", scope.syntaxValidation);
# now attach session to editor
@@ -318,6 +330,14 @@ define [
doc = session.getDocument()
doc.off "change", onChange
+
+ editor.renderer.on "changeCharacterSize", () ->
+ scope.$apply () ->
+ scope.rendererData.lineHeight = editor.renderer.lineHeight
+
+ scope.$watch "rendererData", (rendererData) ->
+ if rendererData?
+ rendererData.lineHeight = editor.renderer.lineHeight
template: """
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee
index b8a3d43819..119aa471e1 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee
@@ -37,6 +37,9 @@ define [
@gotoOffset(offset)
, 10 # Hack: Must happen after @gotoStoredPosition
+ @$scope.$on "#{@$scope.name}:clearSelection", (e) =>
+ @editor.selection.clearSelection()
+
storeScrollTopPosition: (session) ->
if @doc_id?
docPosition = @localStorage("doc.position.#{@doc_id}") || {}
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/HighlightedWordManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/HighlightedWordManager.coffee
index 470909a9ed..5014559562 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/HighlightedWordManager.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/HighlightedWordManager.coffee
@@ -12,6 +12,9 @@ define [
class HighlightedWordManager
constructor: (@editor) ->
+ @reset()
+
+ reset: () ->
@highlights = rows: []
addHighlight: (highlight) ->
@@ -21,7 +24,7 @@ define [
highlight.row, highlight.column,
highlight.row, highlight.column + highlight.word.length
)
- highlight.markerId = @editor.getSession().addMarker range, "spelling-highlight", null, true
+ highlight.markerId = @editor.getSession().addMarker range, "spelling-highlight", 'text', false
@highlights.rows[highlight.row] ||= []
@highlights.rows[highlight.row].push highlight
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee
index e84ce1d785..759b1d2b70 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee
@@ -22,6 +22,10 @@ define [
@closeContextMenu()
@editor.on "changeSession", (e) =>
+ @highlightedWordManager.reset()
+ if @inProgressRequest?
+ @inProgressRequest.abort()
+
if @$scope.spellCheckEnabled and @$scope.spellCheckLanguage and @$scope.spellCheckLanguage != ""
@runSpellCheckSoon(200)
@@ -183,7 +187,8 @@ define [
if not words.length
displayResult highlights
else
- @apiRequest "/check", {language: language, words: words}, (error, result) =>
+ @inProgressRequest = @apiRequest "/check", {language: language, words: words}, (error, result) =>
+ delete @inProgressRequest
if error? or !result? or !result.misspellings?
return null
mispelled = []
@@ -240,4 +245,4 @@ define [
callback null, data
error: (xhr, status, error) ->
callback error
- $.ajax options
+ return $.ajax options
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee
index af9815b2cb..ed15da2958 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee
@@ -10,18 +10,18 @@ define [
constructor: (@$scope, @editor, @element) ->
window.trackChangesManager ?= @
- @$scope.$watch "changesTracker", (changesTracker) =>
- return if !changesTracker?
- @disconnectFromChangesTracker()
- @changesTracker = changesTracker
- @connectToChangesTracker()
-
- @$scope.$watch "trackNewChanges", (track_new_changes) =>
- return if !track_new_changes?
- @changesTracker?.track_changes = track_new_changes
+ @$scope.$watch "trackChanges", (track_changes) =>
+ return if !track_changes?
+ @setTrackChanges(track_changes)
- @$scope.$on "comment:add", (e, comment) =>
- @addCommentToSelection(comment)
+ @$scope.$watch "sharejsDoc", (doc) =>
+ return if !doc?
+ @disconnectFromRangesTracker()
+ @rangesTracker = doc.ranges
+ @connectToRangesTracker()
+
+ @$scope.$on "comment:add", (e, thread_id) =>
+ @addCommentToSelection(thread_id)
@$scope.$on "comment:select_line", (e) =>
@selectLineIfNoSelection()
@@ -35,11 +35,11 @@ define [
@$scope.$on "comment:remove", (e, comment_id) =>
@removeCommentId(comment_id)
- @$scope.$on "comment:resolve", (e, comment_id, user_id) =>
- @resolveCommentId(comment_id, user_id)
+ @$scope.$on "comment:resolve_threads", (e, thread_ids) =>
+ @resolveCommentByThreadIds(thread_ids)
- @$scope.$on "comment:unresolve", (e, comment_id) =>
- @unresolveCommentId(comment_id)
+ @$scope.$on "comment:unresolve_thread", (e, thread_id) =>
+ @unresolveCommentByThreadId(thread_id)
@$scope.$on "review-panel:recalculate-screen-positions", () =>
@recalculateReviewEntriesScreenPositions()
@@ -58,48 +58,16 @@ define [
onResize = () =>
@recalculateReviewEntriesScreenPositions()
- onChange = (e) =>
- if !@editor.initing
- # This change is trigger by a sharejs 'change' event, which is before the
- # sharejs 'remoteop' event. So wait until the next event loop when the 'remoteop'
- # will have fired, before we decide if it was a remote op.
- setTimeout () =>
- if @nextUpdateMetaData?
- user_id = @nextUpdateMetaData.user_id
- # The remote op may have contained multiple atomic ops, each of which is an Ace
- # 'change' event (i.e. bulk commenting out of lines is a single remote op
- # but gives us one event for each % inserted). These all come in a single event loop
- # though, so wait until the next one before clearing the metadata.
- setTimeout () =>
- @nextUpdateMetaData = null
- else
- user_id = window.user.id
-
- was_tracking = @changesTracker.track_changes
- if @dont_track_next_update
- @changesTracker.track_changes = false
- @dont_track_next_update = false
- @applyChange(e, { user_id })
- @changesTracker.track_changes = was_tracking
-
- # TODO: Just for debugging, remove before going live.
- setTimeout () =>
- @checkMapping()
- , 100
-
onChangeSession = (e) =>
- e.oldSession?.getDocument().off "change", onChange
- e.session.getDocument().on "change", onChange
+ @clearAnnotations()
@redrawAnnotations()
bindToAce = () =>
- @editor.getSession().getDocument().on "change", onChange
@editor.on "changeSelection", onChangeSelection
@editor.on "changeSession", onChangeSession
@editor.renderer.on "resize", onResize
unbindFromAce = () =>
- @editor.getSession().getDocument().off "change", onChange
@editor.off "changeSelection", onChangeSelection
@editor.off "changeSession", onChangeSession
@editor.renderer.off "resize", onResize
@@ -111,94 +79,117 @@ define [
else
unbindFromAce()
- disconnectFromChangesTracker: () ->
+ disconnectFromRangesTracker: () ->
@changeIdToMarkerIdMap = {}
- if @changesTracker?
- @changesTracker.off "insert:added"
- @changesTracker.off "insert:removed"
- @changesTracker.off "delete:added"
- @changesTracker.off "delete:removed"
- @changesTracker.off "changes:moved"
- @changesTracker.off "comment:added"
- @changesTracker.off "comment:moved"
- @changesTracker.off "comment:removed"
- @changesTracker.off "comment:resolved"
- @changesTracker.off "comment:unresolved"
-
- connectToChangesTracker: () ->
- @changesTracker.track_changes = @$scope.trackNewChanges
-
- @changesTracker.on "insert:added", (change) =>
- sl_console.log "[insert:added]", change
- @_onInsertAdded(change)
- @changesTracker.on "insert:removed", (change) =>
- sl_console.log "[insert:removed]", change
- @_onInsertRemoved(change)
- @changesTracker.on "delete:added", (change) =>
- sl_console.log "[delete:added]", change
- @_onDeleteAdded(change)
- @changesTracker.on "delete:removed", (change) =>
- sl_console.log "[delete:removed]", change
- @_onDeleteRemoved(change)
- @changesTracker.on "changes:moved", (changes) =>
- sl_console.log "[changes:moved]", changes
- @_onChangesMoved(changes)
+ if @rangesTracker?
+ @rangesTracker.off "insert:added"
+ @rangesTracker.off "insert:removed"
+ @rangesTracker.off "delete:added"
+ @rangesTracker.off "delete:removed"
+ @rangesTracker.off "changes:moved"
+ @rangesTracker.off "comment:added"
+ @rangesTracker.off "comment:moved"
+ @rangesTracker.off "comment:removed"
- @changesTracker.on "comment:added", (comment) =>
- sl_console.log "[comment:added]", comment
- @_onCommentAdded(comment)
- @changesTracker.on "comment:moved", (comment) =>
- sl_console.log "[comment:moved]", comment
- @_onCommentMoved(comment)
- @changesTracker.on "comment:removed", (comment) =>
- sl_console.log "[comment:removed]", comment
- @_onCommentRemoved(comment)
- @changesTracker.on "comment:resolved", (comment) =>
- sl_console.log "[comment:resolved]", comment
- @_onCommentRemoved(comment)
- @changesTracker.on "comment:unresolved", (comment) =>
- sl_console.log "[comment:unresolved]", comment
- @_onCommentAdded(comment)
+ setTrackChanges: (value) ->
+ if value
+ @$scope.sharejsDoc?.track_changes_as = window.user.id or "anonymous"
+ else
+ @$scope.sharejsDoc?.track_changes_as = null
+
+ connectToRangesTracker: () ->
+ @setTrackChanges(@$scope.trackChanges)
+ # Add a timeout because on remote ops, we get these notifications before
+ # ace has updated
+ @rangesTracker.on "insert:added", (change) =>
+ sl_console.log "[insert:added]", change
+ setTimeout () =>
+ @_onInsertAdded(change)
+ @broadcastChange()
+ @rangesTracker.on "insert:removed", (change) =>
+ sl_console.log "[insert:removed]", change
+ setTimeout () =>
+ @_onInsertRemoved(change)
+ @broadcastChange()
+ @rangesTracker.on "delete:added", (change) =>
+ sl_console.log "[delete:added]", change
+ setTimeout () =>
+ @_onDeleteAdded(change)
+ @broadcastChange()
+ @rangesTracker.on "delete:removed", (change) =>
+ sl_console.log "[delete:removed]", change
+ setTimeout () =>
+ @_onDeleteRemoved(change)
+ @broadcastChange()
+ @rangesTracker.on "changes:moved", (changes) =>
+ sl_console.log "[changes:moved]", changes
+ setTimeout () =>
+ @_onChangesMoved(changes)
+ @broadcastChange()
+
+ @rangesTracker.on "comment:added", (comment) =>
+ sl_console.log "[comment:added]", comment
+ setTimeout () =>
+ @_onCommentAdded(comment)
+ @broadcastChange()
+ @rangesTracker.on "comment:moved", (comment) =>
+ sl_console.log "[comment:moved]", comment
+ setTimeout () =>
+ @_onCommentMoved(comment)
+ @broadcastChange()
+ @rangesTracker.on "comment:removed", (comment) =>
+ sl_console.log "[comment:removed]", comment
+ setTimeout () =>
+ @_onCommentRemoved(comment)
+ @broadcastChange()
+
+ @rangesTracker.on "clear", () =>
+ @clearAnnotations()
+ @rangesTracker.on "redraw", () =>
+ @redrawAnnotations()
+
+ clearAnnotations: () ->
+ session = @editor.getSession()
+ for change_id, markers of @changeIdToMarkerIdMap
+ for marker_name, marker_id of markers
+ session.removeMarker marker_id
+ @changeIdToMarkerIdMap = {}
+
redrawAnnotations: () ->
- for change in @changesTracker.changes
+ for change in @rangesTracker.changes
if change.op.i?
@_onInsertAdded(change)
else if change.op.d?
@_onDeleteAdded(change)
- for comment in @changesTracker.comments
+ for comment in @rangesTracker.comments
@_onCommentAdded(comment)
+
+ @broadcastChange()
- addComment: (offset, length, content) ->
- @changesTracker.addComment offset, length, {
- thread: [{
- content: content
- user_id: window.user_id
- ts: new Date()
- }]
- }
+ addComment: (offset, content, thread_id) ->
+ op = { c: content, p: offset, t: thread_id }
+ # @rangesTracker.applyOp op # Will apply via sharejs
+ @$scope.sharejsDoc.submitOp op
- addCommentToSelection: (content) ->
+ addCommentToSelection: (thread_id) ->
range = @editor.getSelectionRange()
+ content = @editor.getSelectedText()
offset = @_aceRangeToShareJs(range.start)
- end = @_aceRangeToShareJs(range.end)
- length = end - offset
- @addComment(offset, length, content)
+ @addComment(offset, content, thread_id)
selectLineIfNoSelection: () ->
if @editor.selection.isEmpty()
@editor.selection.selectLine()
acceptChangeId: (change_id) ->
- @changesTracker.removeChangeId(change_id)
+ @rangesTracker.removeChangeId(change_id)
rejectChangeId: (change_id) ->
- change = @changesTracker.getChange(change_id)
+ change = @rangesTracker.getChange(change_id)
return if !change?
- @changesTracker.removeChangeId(change_id)
- @dont_track_next_update = true
session = @editor.getSession()
if change.op.d?
content = change.op.d
@@ -215,17 +206,25 @@ define [
throw new Error("unknown change: #{JSON.stringify(change)}")
removeCommentId: (comment_id) ->
- @changesTracker.removeCommentId(comment_id)
+ @rangesTracker.removeCommentId(comment_id)
- resolveCommentId: (comment_id, user_id) ->
- @changesTracker.resolveCommentId(comment_id, {
- user_id, ts: new Date()
- })
+ resolveCommentByThreadIds: (thread_ids) ->
+ resolve_ids = {}
+ for id in thread_ids
+ resolve_ids[id] = true
+ for comment in @rangesTracker?.comments or []
+ if resolve_ids[comment.op.t]
+ @_onCommentRemoved(comment)
+ @broadcastChange()
- unresolveCommentId: (comment_id) ->
- @changesTracker.unresolveCommentId(comment_id)
+ unresolveCommentByThreadId: (thread_id) ->
+ for comment in @rangesTracker?.comments or []
+ if comment.op.t == thread_id
+ @_onCommentAdded(comment)
+ @broadcastChange()
checkMapping: () ->
+ # TODO: reintroduce this check
session = @editor.getSession()
# Make a copy of session.getMarkers() so we can modify it
@@ -234,7 +233,7 @@ define [
markers[marker_id] = marker
expected_markers = []
- for change in @changesTracker.changes
+ for change in @rangesTracker.changes
if @changeIdToMarkerIdMap[change.id]?
op = change.op
{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id]
@@ -246,11 +245,11 @@ define [
expected_markers.push { marker_id: background_marker_id, start, end }
expected_markers.push { marker_id: callout_marker_id, start, end: start }
- for comment in @changesTracker.comments
+ for comment in @rangesTracker.comments
if @changeIdToMarkerIdMap[comment.id]?
{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[comment.id]
- start = @_shareJsOffsetToAcePosition(comment.offset)
- end = @_shareJsOffsetToAcePosition(comment.offset + comment.length)
+ start = @_shareJsOffsetToAcePosition(comment.op.p)
+ end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length)
expected_markers.push { marker_id: background_marker_id, start, end }
expected_markers.push { marker_id: callout_marker_id, start, end: start }
@@ -267,16 +266,13 @@ define [
if marker.clazz.match("track-changes")
console.error "Orphaned ace marker", marker
- applyChange: (delta, metadata) ->
- op = @_aceChangeToShareJs(delta)
- @changesTracker.applyOp(op, metadata)
-
updateFocus: () ->
selection = @editor.getSelectionRange()
- cursor_offset = @_aceRangeToShareJs(selection.start)
+ selection_start = @_aceRangeToShareJs(selection.start)
+ selection_end = @_aceRangeToShareJs(selection.end)
entries = @_getCurrentDocEntries()
- selection = !(selection.start.column == selection.end.column and selection.start.row == selection.end.row)
- @$scope.$emit "editor:focus:changed", cursor_offset, selection
+ is_selection = (selection_start != selection_end)
+ @$scope.$emit "editor:focus:changed", selection_start, selection_end, is_selection
broadcastChange: () ->
@$scope.$emit "editor:track-changes:changed", @$scope.docId
@@ -330,7 +326,6 @@ define [
background_marker_id = session.addMarker background_range, "track-changes-marker track-changes-added-marker", "text"
callout_marker_id = @_createCalloutMarker(start, "track-changes-added-marker-callout")
@changeIdToMarkerIdMap[change.id] = { background_marker_id, callout_marker_id }
- @broadcastChange()
_onDeleteAdded: (change) ->
position = @_shareJsOffsetToAcePosition(change.op.p)
@@ -345,7 +340,6 @@ define [
callout_marker_id = @_createCalloutMarker(position, "track-changes-deleted-marker-callout")
@changeIdToMarkerIdMap[change.id] = { background_marker_id, callout_marker_id }
- @broadcastChange()
_onInsertRemoved: (change) ->
{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id]
@@ -353,7 +347,6 @@ define [
session = @editor.getSession()
session.removeMarker background_marker_id
session.removeMarker callout_marker_id
- @broadcastChange()
_onDeleteRemoved: (change) ->
{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id]
@@ -361,20 +354,21 @@ define [
session = @editor.getSession()
session.removeMarker background_marker_id
session.removeMarker callout_marker_id
- @broadcastChange()
_onCommentAdded: (comment) ->
+ if @rangesTracker.resolvedThreadIds[comment.op.t]
+ # Comment is resolved so shouldn't be displayed.
+ return
if !@changeIdToMarkerIdMap[comment.id]?
# Only create new markers if they don't already exist
- start = @_shareJsOffsetToAcePosition(comment.offset)
- end = @_shareJsOffsetToAcePosition(comment.offset + comment.length)
+ start = @_shareJsOffsetToAcePosition(comment.op.p)
+ end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length)
session = @editor.getSession()
doc = session.getDocument()
background_range = new Range(start.row, start.column, end.row, end.column)
background_marker_id = session.addMarker background_range, "track-changes-marker track-changes-comment-marker", "text"
callout_marker_id = @_createCalloutMarker(start, "track-changes-comment-marker-callout")
@changeIdToMarkerIdMap[comment.id] = { background_marker_id, callout_marker_id }
- @broadcastChange()
_onCommentRemoved: (comment) ->
if @changeIdToMarkerIdMap[comment.id]?
@@ -384,7 +378,6 @@ define [
session = @editor.getSession()
session.removeMarker background_marker_id
session.removeMarker callout_marker_id
- @broadcastChange()
_aceRangeToShareJs: (range) ->
lines = @editor.getSession().getDocument().getLines 0, range.row
@@ -409,25 +402,23 @@ define [
end = start
@_updateMarker(change.id, start, end)
@editor.renderer.updateBackMarkers()
- @broadcastChange()
_onCommentMoved: (comment) ->
- start = @_shareJsOffsetToAcePosition(comment.offset)
- end = @_shareJsOffsetToAcePosition(comment.offset + comment.length)
+ start = @_shareJsOffsetToAcePosition(comment.op.p)
+ end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length)
@_updateMarker(comment.id, start, end)
@editor.renderer.updateBackMarkers()
- @broadcastChange()
_updateMarker: (change_id, start, end) ->
return if !@changeIdToMarkerIdMap[change_id]?
session = @editor.getSession()
markers = session.getMarkers()
{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change_id]
- if background_marker_id?
+ if background_marker_id? and markers[background_marker_id]?
background_marker = markers[background_marker_id]
background_marker.range.start = start
background_marker.range.end = end
- if callout_marker_id?
+ if callout_marker_id? and markers[callout_marker_id]?
callout_marker = markers[callout_marker_id]
callout_marker.range.start = start
callout_marker.range.end = start
diff --git a/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee b/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee
index d25baf89d5..aca3560b49 100644
--- a/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee
+++ b/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee
@@ -71,7 +71,7 @@ class Doc
# Its important that these event handlers are called with oldSnapshot.
# The reason is that the OT type APIs might need to access the snapshots to
# determine information about the received op.
- @emit 'change', docOp, oldSnapshot
+ @emit 'change', docOp, oldSnapshot, msg
@emit 'remoteop', docOp, oldSnapshot, msg if isRemote
_connectionStateChanged: (state, data) ->
@@ -266,6 +266,8 @@ class Doc
@pendingOp = null
@pendingCallbacks = []
+ @emit "flipped_pending_to_inflight"
+
#console.log "SENDING OP TO SERVER", @inflightOp, @version
@connection.send {doc:@name, op:@inflightOp, v:@version}
@@ -274,6 +276,7 @@ class Doc
submitOp: (op, callback) ->
op = @type.normalize(op) if @type.normalize?
+ oldSnapshot = @snapshot
# If this throws an exception, no changes should have been made to the doc
@snapshot = @type.apply @snapshot, op
@@ -284,7 +287,7 @@ class Doc
@pendingCallbacks.push callback if callback
- @emit 'change', op
+ @emit 'change', op, oldSnapshot
@delayedFlush()
diff --git a/services/web/public/coffee/ide/editor/sharejs/vendor/types/text-api.coffee b/services/web/public/coffee/ide/editor/sharejs/vendor/types/text-api.coffee
index 96243ceffb..274b6019c5 100644
--- a/services/web/public/coffee/ide/editor/sharejs/vendor/types/text-api.coffee
+++ b/services/web/public/coffee/ide/editor/sharejs/vendor/types/text-api.coffee
@@ -28,5 +28,5 @@ text.api =
for component in op
if component.i != undefined
@emit 'insert', component.p, component.i
- else
+ else if component.d != undefined
@emit 'delete', component.p, component.d
diff --git a/services/web/public/coffee/ide/editor/sharejs/vendor/types/text.coffee b/services/web/public/coffee/ide/editor/sharejs/vendor/types/text.coffee
index c64b4dfa68..2a3b79997d 100644
--- a/services/web/public/coffee/ide/editor/sharejs/vendor/types/text.coffee
+++ b/services/web/public/coffee/ide/editor/sharejs/vendor/types/text.coffee
@@ -31,7 +31,8 @@ checkValidComponent = (c) ->
i_type = typeof c.i
d_type = typeof c.d
- throw new Error 'component needs an i or d field' unless (i_type == 'string') ^ (d_type == 'string')
+ c_type = typeof c.c
+ throw new Error 'component needs an i, d or c field' unless (i_type == 'string') ^ (d_type == 'string') ^ (c_type == 'string')
throw new Error 'position cannot be negative' unless c.p >= 0
@@ -44,11 +45,15 @@ text.apply = (snapshot, op) ->
for component in op
if component.i?
snapshot = strInject snapshot, component.p, component.i
- else
+ else if component.d?
deleted = snapshot[component.p...(component.p + component.d.length)]
throw new Error "Delete component '#{component.d}' does not match deleted text '#{deleted}'" unless component.d == deleted
snapshot = snapshot[...component.p] + snapshot[(component.p + component.d.length)..]
-
+ else if component.c?
+ comment = snapshot[component.p...(component.p + component.c.length)]
+ throw new Error "Comment component '#{component.c}' does not match commented text '#{comment}'" unless component.c == comment
+ else
+ throw new Error "Unknown op type"
snapshot
@@ -112,7 +117,7 @@ transformPosition = (pos, c, insertAfter) ->
pos + c.i.length
else
pos
- else
+ else if c.d?
# I think this could also be written as: Math.min(c.p, Math.min(c.p - otherC.p, otherC.d.length))
# but I think its harder to read that way, and it compiles using ternary operators anyway
# so its no slower written like this.
@@ -122,6 +127,10 @@ transformPosition = (pos, c, insertAfter) ->
c.p
else
pos - c.d.length
+ else if c.c?
+ pos
+ else
+ throw new Error("unknown op type")
# Helper method to transform a cursor position as a result of an op.
#
@@ -143,7 +152,7 @@ text._tc = transformComponent = (dest, c, otherC, side) ->
if c.i?
append dest, {i:c.i, p:transformPosition(c.p, otherC, side == 'right')}
- else # Delete
+ else if c.d? # Delete
if otherC.i? # delete vs insert
s = c.d
if c.p < otherC.p
@@ -152,7 +161,7 @@ text._tc = transformComponent = (dest, c, otherC, side) ->
if s != ''
append dest, {d:s, p:c.p + otherC.i.length}
- else # Delete vs delete
+ else if otherC.d? # Delete vs delete
if c.p >= otherC.p + otherC.d.length
append dest, {d:c.d, p:c.p - otherC.d.length}
else if c.p + c.d.length <= otherC.p
@@ -177,6 +186,51 @@ text._tc = transformComponent = (dest, c, otherC, side) ->
# This could be rewritten similarly to insert v delete, above.
newC.p = transformPosition newC.p, otherC
append dest, newC
+
+ else if otherC.c?
+ append dest, c
+
+ else
+ throw new Error("unknown op type")
+
+ else if c.c? # Comment
+ if otherC.i?
+ if c.p < otherC.p < c.p + c.c.length
+ offset = otherC.p - c.p
+ new_c = (c.c[0..(offset-1)] + otherC.i + c.c[offset...])
+ append dest, {c:new_c, p:c.p, t: c.t}
+ else
+ append dest, {c:c.c, p:transformPosition(c.p, otherC, true), t: c.t}
+
+ else if otherC.d?
+ if c.p >= otherC.p + otherC.d.length
+ append dest, {c:c.c, p:c.p - otherC.d.length, t: c.t}
+ else if c.p + c.c.length <= otherC.p
+ append dest, c
+ else # Delete overlaps comment
+ # They overlap somewhere.
+ newC = {c:'', p:c.p, t: c.t}
+ if c.p < otherC.p
+ newC.c = c.c[...(otherC.p - c.p)]
+ if c.p + c.c.length > otherC.p + otherC.d.length
+ newC.c += c.c[(otherC.p + otherC.d.length - c.p)..]
+
+ # This is entirely optional - just for a check that the deleted
+ # text in the two ops matches
+ intersectStart = Math.max c.p, otherC.p
+ intersectEnd = Math.min c.p + c.c.length, otherC.p + otherC.d.length
+ cIntersect = c.c[intersectStart - c.p...intersectEnd - c.p]
+ otherIntersect = otherC.d[intersectStart - otherC.p...intersectEnd - otherC.p]
+ throw new Error 'Delete ops delete different text in the same region of the document' unless cIntersect == otherIntersect
+
+ newC.p = transformPosition newC.p, otherC
+ append dest, newC
+
+ else if otherC.c?
+ append dest, c
+
+ else
+ throw new Error("unknown op type")
dest
diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee
index 8c49d54c23..c4ad4b30a4 100644
--- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee
+++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee
@@ -275,6 +275,16 @@ define [
doc: entity
path: path
}
+ # Keep list ordered by folders, then name
+ @$scope.docs.sort (a,b) ->
+ aDepth = (a.path.match(/\//g) || []).length
+ bDepth = (b.path.match(/\//g) || []).length
+ if aDepth - bDepth != 0
+ return -(aDepth - bDepth) # Deeper path == folder first
+ else if a.path < b.path
+ return -1
+ else
+ return 1
getEntityPath: (entity) ->
@_getEntityPathInFolder @$scope.rootFolder, entity
diff --git a/services/web/public/coffee/ide/permissions/PermissionsManager.coffee b/services/web/public/coffee/ide/permissions/PermissionsManager.coffee
index 096f15babe..88dea13084 100644
--- a/services/web/public/coffee/ide/permissions/PermissionsManager.coffee
+++ b/services/web/public/coffee/ide/permissions/PermissionsManager.coffee
@@ -5,15 +5,22 @@ define [], () ->
read: false
write: false
admin: false
+ comment: false
@$scope.$watch "permissionsLevel", (permissionsLevel) =>
if permissionsLevel?
if permissionsLevel == "readOnly"
@$scope.permissions.read = true
+ @$scope.permissions.comment = true
else if permissionsLevel == "readAndWrite"
@$scope.permissions.read = true
@$scope.permissions.write = true
+ @$scope.permissions.comment = true
else if permissionsLevel == "owner"
@$scope.permissions.read = true
@$scope.permissions.write = true
@$scope.permissions.admin = true
+ @$scope.permissions.comment = true
+
+ if @$scope.anonymous
+ @$scope.permissions.comment = false
diff --git a/services/web/public/coffee/ide/review-panel/ChangesTracker.coffee b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee
similarity index 85%
rename from services/web/public/coffee/ide/review-panel/ChangesTracker.coffee
rename to services/web/public/coffee/ide/review-panel/RangesTracker.coffee
index 0b668c90dd..e31b84f051 100644
--- a/services/web/public/coffee/ide/review-panel/ChangesTracker.coffee
+++ b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee
@@ -1,7 +1,5 @@
-define [
- "utils/EventEmitter"
-], (EventEmitter) ->
- class ChangesTracker extends EventEmitter
+load = (EventEmitter) ->
+ class RangesTracker extends EventEmitter
# The purpose of this class is to track a set of inserts and deletes to a document, like
# track changes in Word. We store these as a set of ShareJs style ranges:
# {i: "foo", p: 42} # Insert 'foo' at offset 42
@@ -36,30 +34,34 @@ define [
# * Deletes by another user will consume deletes by the first user
# * Inserts by another user will not combine with inserts by the first user. If they are in the
# middle of a previous insert by the first user, the original insert will be split into two.
- constructor: () ->
- # Change objects have the following structure:
- # {
- # id: ... # Uniquely generated by us
- # op: { # ShareJs style op tracking the offset (p) and content inserted (i) or deleted (d)
- # i: "..."
- # p: 42
- # }
- # }
- #
- # Ids are used to uniquely identify a change, e.g. for updating it in the database, or keeping in
- # sync with Ace ranges.
- @changes = []
- @comments = []
- @id = 0
+ constructor: (@changes = [], @comments = []) ->
+ @setIdSeed(RangesTracker.generateIdSeed())
+
+ getIdSeed: () ->
+ return @id_seed
+
+ setIdSeed: (seed) ->
+ @id_seed = seed
+ @id_increment = 0
- addComment: (offset, length, metadata) ->
- # TODO: Don't allow overlapping comments?
- @comments.push comment = {
- id: @_newId()
- offset, length, metadata
- }
- @emit "comment:added", comment
- return comment
+ @generateIdSeed: () ->
+ # Generate a the first 18 characters of Mongo ObjectId, leaving 6 for the increment part
+ # Reference: https://github.com/dreampulse/ObjectId.js/blob/master/src/main/javascript/Objectid.js
+ pid = Math.floor(Math.random() * (32767)).toString(16)
+ machine = Math.floor(Math.random() * (16777216)).toString(16)
+ timestamp = Math.floor(new Date().valueOf() / 1000).toString(16)
+ return '00000000'.substr(0, 8 - timestamp.length) + timestamp +
+ '000000'.substr(0, 6 - machine.length) + machine +
+ '0000'.substr(0, 4 - pid.length) + pid
+
+ @generateId: () ->
+ @generateIdSeed() + "000001"
+
+ newId: () ->
+ @id_increment++
+ increment = @id_increment.toString(16)
+ id = @id_seed + '000000'.substr(0, 6 - increment.length) + increment;
+ return id
getComment: (comment_id) ->
comment = null
@@ -69,19 +71,6 @@ define [
break
return comment
- resolveCommentId: (comment_id, resolved_data) ->
- comment = @getComment(comment_id)
- return if !comment?
- comment.metadata.resolved = true
- comment.metadata.resolved_data = resolved_data
- @emit "comment:resolved", comment
-
- unresolveCommentId: (comment_id) ->
- comment = @getComment(comment_id)
- return if !comment?
- comment.metadata.resolved = false
- @emit "comment:unresolved", comment
-
removeCommentId: (comment_id) ->
comment = @getComment(comment_id)
return if !comment?
@@ -101,7 +90,7 @@ define [
return if !change?
@_removeChange(change)
- applyOp: (op, metadata) ->
+ applyOp: (op, metadata = {}) ->
metadata.ts ?= new Date()
# Apply an op that has been applied to the document to our changes to keep them up to date
if op.i?
@@ -110,14 +99,31 @@ define [
else if op.d?
@applyDeleteToChanges(op, metadata)
@applyDeleteToComments(op)
+ else if op.c?
+ @addComment(op, metadata)
+ else
+ throw new Error("unknown op type")
+
+ addComment: (op, metadata) ->
+ @comments.push comment = {
+ id: op.t or @newId()
+ op: # Copy because we'll modify in place
+ c: op.c
+ p: op.p
+ t: op.t
+ metadata
+ }
+ @emit "comment:added", comment
+ return comment
applyInsertToComments: (op) ->
for comment in @comments
- if op.p <= comment.offset
- comment.offset += op.i.length
+ if op.p <= comment.op.p
+ comment.op.p += op.i.length
@emit "comment:moved", comment
- else if op.p < comment.offset + comment.length
- comment.length += op.i.length
+ else if op.p < comment.op.p + comment.op.c.length
+ offset = op.p - comment.op.p
+ comment.op.c = comment.op.c[0..(offset-1)] + op.i + comment.op.c[offset...]
@emit "comment:moved", comment
applyDeleteToComments: (op) ->
@@ -125,20 +131,35 @@ define [
op_length = op.d.length
op_end = op.p + op_length
for comment in @comments
- comment_end = comment.offset + comment.length
- if op_end <= comment.offset
+ comment_start = comment.op.p
+ comment_end = comment.op.p + comment.op.c.length
+ comment_length = comment_end - comment_start
+ if op_end <= comment_start
# delete is fully before comment
- comment.offset -= op_length
+ comment.op.p -= op_length
@emit "comment:moved", comment
else if op_start >= comment_end
# delete is fully after comment, nothing to do
else
# delete and comment overlap
- delete_length_before = Math.max(0, comment.offset - op_start)
- delete_length_after = Math.max(0, op_end - comment_end)
- delete_length_overlapping = op_length - delete_length_before - delete_length_after
- comment.offset = Math.min(comment.offset, op_start)
- comment.length -= delete_length_overlapping
+ if op_start <= comment_start
+ remaining_before = ""
+ else
+ remaining_before = comment.op.c.slice(0, op_start - comment_start)
+ if op_end >= comment_end
+ remaining_after = ""
+ else
+ remaining_after = comment.op.c.slice(op_end - comment_start)
+
+ # Check deleted content matches delete op
+ deleted_comment = comment.op.c.slice(remaining_before.length, comment_length - remaining_after.length)
+ offset = Math.max(0, comment_start - op_start)
+ deleted_op_content = op.d.slice(offset).slice(0, deleted_comment.length)
+ if deleted_comment != deleted_op_content
+ throw new Error("deleted content does not match comment content")
+
+ comment.op.p = Math.min(comment_start, op_start)
+ comment.op.c = remaining_before + remaining_after
@emit "comment:moved", comment
applyInsertToChanges: (op, metadata) ->
@@ -374,12 +395,9 @@ define [
if moved_changes.length > 0
@emit "changes:moved", moved_changes
- _newId: () ->
- (@id++).toString()
-
_addOp: (op, metadata) ->
change = {
- id: @_newId()
+ id: @newId()
op: op
metadata: metadata
}
@@ -453,3 +471,9 @@ define [
else # Only update to the current change if we haven't removed it.
previous_change = change
return { moved_changes, remove_changes }
+
+if define?
+ define ["utils/EventEmitter"], load
+else
+ EventEmitter = require("events").EventEmitter
+ module.exports = load(EventEmitter)
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee b/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee
index 6a23d15016..1565d6db73 100644
--- a/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee
+++ b/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee
@@ -1,9 +1,13 @@
define [
"ide/review-panel/controllers/ReviewPanelController"
+ "ide/review-panel/controllers/TrackChangesUpgradeModalController"
"ide/review-panel/directives/reviewPanelSorted"
"ide/review-panel/directives/reviewPanelToggle"
"ide/review-panel/directives/changeEntry"
"ide/review-panel/directives/commentEntry"
"ide/review-panel/directives/addCommentEntry"
+ "ide/review-panel/directives/resolvedCommentEntry"
+ "ide/review-panel/directives/resolvedCommentsDropdown"
+ "ide/review-panel/filters/notEmpty"
"ide/review-panel/filters/orderOverviewEntries"
], () ->
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee
index 9623e2af9a..5cdf7c672c 100644
--- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee
+++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee
@@ -2,9 +2,9 @@ define [
"base",
"utils/EventEmitter"
"ide/colors/ColorManager"
- "ide/review-panel/ChangesTracker"
-], (App, EventEmitter, ColorManager, ChangesTracker) ->
- App.controller "ReviewPanelController", ($scope, $element, ide, $timeout) ->
+ "ide/review-panel/RangesTracker"
+], (App, EventEmitter, ColorManager, RangesTracker) ->
+ App.controller "ReviewPanelController", ($scope, $element, ide, $timeout, $http, $modal, event_tracking) ->
$reviewPanelEl = $element.find "#review-panel"
$scope.SubViews =
@@ -13,131 +13,113 @@ define [
$scope.reviewPanel =
entries: {}
- trackNewChanges: false
+ resolvedComments: {}
hasEntries: false
subView: $scope.SubViews.CUR_FILE
openSubView: $scope.SubViews.CUR_FILE
+ overview:
+ loading: false
+ dropdown:
+ loading: false
+ commentThreads: {}
+ resolvedThreadIds: {}
+ layoutToLeft: false
+ rendererData: {}
+ loadingThreads: false
+
+ $scope.$on "layout:pdf:linked", (event, state) ->
+ $scope.reviewPanel.layoutToLeft = (state.east?.size < 220 || state.east?.initClosed)
+ $scope.$broadcast "review-panel:layout"
+
+ $scope.$on "layout:pdf:resize", (event, state) ->
+ $scope.reviewPanel.layoutToLeft = (state.east?.size < 220 || state.east?.initClosed)
+ $scope.$broadcast "review-panel:layout", false
+
+ $scope.$on "expandable-text-area:resize", (event) ->
+ $timeout () ->
+ $scope.$broadcast "review-panel:layout"
+
+ $scope.$watch "ui.pdfLayout", (layout) ->
+ $scope.reviewPanel.layoutToLeft = (layout == "flat")
+
+ $scope.$watch "project.features.trackChangesVisible", (visible) ->
+ return if !visible?
+ if !visible
+ $scope.ui.reviewPanelOpen = false
$scope.commentState =
adding: false
content: ""
- $scope.reviewPanelEventsBridge = new EventEmitter()
+ $scope.users = {}
- changesTrackers = {}
+ $scope.reviewPanelEventsBridge = new EventEmitter()
+
+ ide.socket.on "new-comment", (thread_id, comment) ->
+ thread = getThread(thread_id)
+ delete thread.submitting
+ thread.messages.push(formatComment(comment))
+ $scope.$apply()
+ $timeout () ->
+ $scope.$broadcast "review-panel:layout"
+
+ ide.socket.on "accept-change", (doc_id, change_id) ->
+ if doc_id != $scope.editor.open_doc_id
+ getChangeTracker(doc_id).removeChangeId(change_id)
+ else
+ $scope.$broadcast "change:accept", change_id
+ updateEntries(doc_id)
+ $scope.$apply () ->
+
+ ide.socket.on "resolve-thread", (thread_id, user) ->
+ _onCommentResolved(thread_id, user)
+
+ ide.socket.on "reopen-thread", (thread_id) ->
+ _onCommentReopened(thread_id)
+
+ ide.socket.on "delete-thread", (thread_id) ->
+ _onThreadDeleted(thread_id)
+ $scope.$apply () ->
+
+ ide.socket.on "edit-message", (thread_id, message_id, content) ->
+ _onCommentEdited(thread_id, message_id, content)
+ $scope.$apply () ->
+
+ ide.socket.on "delete-message", (thread_id, message_id) ->
+ _onCommentDeleted(thread_id, message_id)
+ $scope.$apply () ->
+
+ rangesTrackers = {}
getDocEntries = (doc_id) ->
$scope.reviewPanel.entries[doc_id] ?= {}
return $scope.reviewPanel.entries[doc_id]
+ getDocResolvedComments = (doc_id) ->
+ $scope.reviewPanel.resolvedComments[doc_id] ?= {}
+ return $scope.reviewPanel.resolvedComments[doc_id]
+
+ getThread = (thread_id) ->
+ $scope.reviewPanel.commentThreads[thread_id] ?= { messages: [] }
+ return $scope.reviewPanel.commentThreads[thread_id]
+
getChangeTracker = (doc_id) ->
- changesTrackers[doc_id] ?= new ChangesTracker()
- return changesTrackers[doc_id]
-
- # TODO Just for prototyping purposes; remove afterwards.
- mockedUserId = 'mock_user_id_1'
- mockedUserId2 = 'mock_user_id_2'
-
- if window.location.search.match /mocktc=true/
- mock_changes = {
- "main.tex":
- changes: [{
- op: { i: "Habitat loss and conflicts with humans are the greatest causes of concern.", p: 925 - 38 }
- metadata: { user_id: mockedUserId, ts: new Date(Date.now() - 30 * 60 * 1000) }
- }, {
- op: { d: "The lion is now a vulnerable species. ", p: 778 }
- metadata: { user_id: mockedUserId, ts: new Date(Date.now() - 31 * 60 * 1000) }
- }]
- comments: [{
- offset: 1375 - 38
- length: 79
- metadata:
- thread: [{
- content: "Do we have a source for this?"
- user_id: mockedUserId
- ts: new Date(Date.now() - 45 * 60 * 1000)
- }]
- }]
- "chapter_1.tex":
- changes: [{
- "op":{"p":740,"d":", to take down large animals"},
- "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 15 * 60 * 1000)}
- }, {
- "op":{"i":", to keep hold of the prey","p":920},
- "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 130 * 60 * 1000)}
- }, {
- "op":{"i":" being","p":1057},
- "metadata":{"user_id":mockedUserId2, ts: new Date(Date.now() - 72 * 60 * 1000)}
- }]
- comments:[{
- "offset":111,"length":5,
- "metadata":{
- "thread": [
- {"content":"Have we used 'pride' too much here?","user_id":mockedUserId, ts: new Date(Date.now() - 12 * 60 * 1000)},
- {"content":"No, I think this is OK","user_id":mockedUserId2, ts: new Date(Date.now() - 9 * 60 * 1000)}
- ]
- }
- },{
- "offset":452,"length":21,
- "metadata":{
- "thread":[
- {"content":"TODO: Don't use as many parentheses!","user_id":mockedUserId2, ts: new Date(Date.now() - 99 * 60 * 1000)}
- ]
- }
- }]
- "chapter_2.tex":
- changes: [{
- "op":{"p":458,"d":"other"},
- "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 133 * 60 * 1000)}
- },{
- "op":{"i":"usually 2-3, ","p":928},
- "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 27 * 60 * 1000)}
- },{
- "op":{"i":"If the parents are a male lion and a female tiger, it is called a liger. A tigon comes from a male tiger and a female lion.","p":1126},
- "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 152 * 60 * 1000)}
- }]
- comments: [{
- "offset":299,"length":10,
- "metadata":{
- "thread":[{
- "content":"Should we use a different word here if 'den' needs clarifying?","user_id":mockedUserId,"ts": new Date(Date.now() - 430 * 60 * 1000)
- }]
- }
- },{
- "offset":843,"length":66,
- "metadata":{
- "thread":[{
- "content":"This sentence is a little ambiguous","user_id":mockedUserId,"ts": new Date(Date.now() - 430 * 60 * 1000)
- }]
- }
- }]
- }
- ide.$scope.$on "file-tree:initialized", () ->
- ide.fileTreeManager.forEachEntity (entity) ->
- if mock_changes[entity.name]?
- changesTracker = getChangeTracker(entity.id)
- for change in mock_changes[entity.name].changes
- changesTracker._addOp change.op, change.metadata
- for comment in mock_changes[entity.name].comments
- changesTracker.addComment comment.offset, comment.length, comment.metadata
- for doc_id, changesTracker of changesTrackers
- updateEntries(doc_id)
+ if !rangesTrackers[doc_id]?
+ rangesTrackers[doc_id] = new RangesTracker()
+ rangesTrackers[doc_id].resolvedThreadIds = $scope.reviewPanel.resolvedThreadIds
+ return rangesTrackers[doc_id]
scrollbar = {}
$scope.reviewPanelEventsBridge.on "aceScrollbarVisibilityChanged", (isVisible, scrollbarWidth) ->
scrollbar = {isVisible, scrollbarWidth}
updateScrollbar()
-
+
updateScrollbar = () ->
if scrollbar.isVisible and $scope.reviewPanel.subView == $scope.SubViews.CUR_FILE
$reviewPanelEl.css "right", "#{ scrollbar.scrollbarWidth }px"
else
$reviewPanelEl.css "right", "0"
-
- $scope.$watch "reviewPanel.subView", (subView) ->
- return if !subView?
- updateScrollbar()
-
+
$scope.$watch "ui.reviewPanelOpen", (open) ->
return if !open?
if !open
@@ -147,33 +129,88 @@ define [
else
# Reset back to what we had when previously open
$scope.reviewPanel.subView = $scope.reviewPanel.openSubView
+
+ $scope.$watch "reviewPanel.subView", (view) ->
+ return if !view?
+ updateScrollbar()
+ if view == $scope.SubViews.OVERVIEW
+ refreshOverviewPanel()
- $scope.$watch "editor.open_doc_id", (open_doc_id) ->
- return if !open_doc_id?
- changesTrackers[open_doc_id] ?= new ChangesTracker()
- $scope.reviewPanel.changesTracker = changesTrackers[open_doc_id]
+ $scope.$watch "editor.sharejs_doc", (doc, old_doc) ->
+ return if !doc?
+ # The open doc range tracker is kept up to date in real-time so
+ # replace any outdated info with this
+ rangesTrackers[doc.doc_id] = doc.ranges
+ rangesTrackers[doc.doc_id].resolvedThreadIds = $scope.reviewPanel.resolvedThreadIds
+ $scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id]
+ if old_doc?
+ old_doc.off "flipped_pending_to_inflight"
+ doc.on "flipped_pending_to_inflight", () ->
+ regenerateTrackChangesId(doc)
+ regenerateTrackChangesId(doc)
$scope.$watch (() ->
entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {}
Object.keys(entries).length
), (nEntries) ->
- $scope.reviewPanel.hasEntries = nEntries > 0 and $scope.trackChangesFeatureFlag
+ $scope.reviewPanel.hasEntries = nEntries > 0 and $scope.project.features.trackChangesVisible
$scope.$watch "ui.reviewPanelOpen", (reviewPanelOpen) ->
return if !reviewPanelOpen?
$timeout () ->
$scope.$broadcast "review-panel:toggle"
- $scope.$broadcast "review-panel:layout"
+ $scope.$broadcast "review-panel:layout", false
+
+ regenerateTrackChangesId = (doc) ->
+ old_id = getChangeTracker(doc.doc_id).getIdSeed()
+ new_id = RangesTracker.generateIdSeed()
+ getChangeTracker(doc.doc_id).setIdSeed(new_id)
+ doc.setTrackChangesIdSeeds({pending: new_id, inflight: old_id})
+ refreshRanges = () ->
+ $http.get "/project/#{$scope.project_id}/ranges"
+ .success (docs) ->
+ for doc in docs
+ if doc.id != $scope.editor.open_doc_id # this is kept up to date in real-time, don't overwrite
+ rangesTracker = getChangeTracker(doc.id)
+ rangesTracker.comments = doc.ranges?.comments or []
+ rangesTracker.changes = doc.ranges?.changes or []
+ updateEntries(doc.id)
+
+ refreshOverviewPanel = () ->
+ $scope.reviewPanel.overview.loading = true
+ refreshRanges()
+ .then () ->
+ $scope.reviewPanel.overview.loading = false
+ .catch () ->
+ $scope.reviewPanel.overview.loading = false
+
+ $scope.refreshResolvedCommentsDropdown = () ->
+ $scope.reviewPanel.dropdown.loading = true
+ q = refreshRanges()
+ q.then () ->
+ $scope.reviewPanel.dropdown.loading = false
+ q.catch () ->
+ $scope.reviewPanel.dropdown.loading = false
+ return q
+
updateEntries = (doc_id) ->
- changesTracker = getChangeTracker(doc_id)
+ rangesTracker = getChangeTracker(doc_id)
entries = getDocEntries(doc_id)
+ resolvedComments = getDocResolvedComments(doc_id)
+ changed = false
+
# Assume we'll delete everything until we see it, then we'll remove it from this object
delete_changes = {}
- delete_changes[change_id] = true for change_id, change of entries
+ for change_id, change of entries
+ if change_id != "add-comment"
+ delete_changes[change_id] = true
+ for change_id, change of resolvedComments
+ delete_changes[change_id] = true
- for change in changesTracker.changes
+ for change in rangesTracker.changes
+ changed = true
delete delete_changes[change.id]
entries[change.id] ?= {}
@@ -189,22 +226,37 @@ define [
for key, value of new_entry
entries[change.id][key] = value
- for comment in changesTracker.comments
+ if !$scope.users[change.metadata.user_id]?
+ refreshChangeUsers(change.metadata.user_id)
+
+ if rangesTracker.comments.length > 0
+ ensureThreadsAreLoaded()
+
+ for comment in rangesTracker.comments
+ changed = true
delete delete_changes[comment.id]
- entries[comment.id] ?= {}
+ if $scope.reviewPanel.resolvedThreadIds[comment.op.t]
+ new_comment = resolvedComments[comment.id] ?= {}
+ delete entries[comment.id]
+ else
+ new_comment = entries[comment.id] ?= {}
+ delete resolvedComments[comment.id]
new_entry = {
type: "comment"
- thread: comment.metadata.thread
- resolved: comment.metadata.resolved
- resolved_data: comment.metadata.resolved_data
- offset: comment.offset
- length: comment.length
+ thread_id: comment.op.t
+ content: comment.op.c
+ offset: comment.op.p
}
for key, value of new_entry
- entries[comment.id][key] = value
+ new_comment[key] = value
for change_id, _ of delete_changes
+ changed = true
delete entries[change_id]
+ delete resolvedComments[change_id]
+
+ if changed
+ $scope.$broadcast "entries:changed"
$scope.$on "editor:track-changes:changed", () ->
doc_id = $scope.editor.open_doc_id
@@ -212,53 +264,65 @@ define [
$scope.$broadcast "review-panel:recalculate-screen-positions"
$scope.$broadcast "review-panel:layout"
- $scope.$on "editor:focus:changed", (e, cursor_offset, selection) ->
+ $scope.$on "editor:focus:changed", (e, selection_offset_start, selection_offset_end, selection) ->
doc_id = $scope.editor.open_doc_id
entries = getDocEntries(doc_id)
- if !selection
- delete entries["add-comment"]
- else
- entries["add-comment"] = {
- type: "add-comment"
- offset: cursor_offset
- }
+ delete entries["add-comment"]
+ if selection
+ # Only show add comment if we're not already overlapping one
+ overlapping_comment = false
+ for id, entry of entries
+ if entry.type == "comment" and not $scope.reviewPanel.resolvedThreadIds[entry.thread_id]
+ unless entry.offset >= selection_offset_end or entry.offset + entry.content.length <= selection_offset_start
+ overlapping_comment = true
+ if !overlapping_comment
+ entries["add-comment"] = {
+ type: "add-comment"
+ offset: selection_offset_start
+ }
for id, entry of entries
- if entry.type == "comment" and not entry.resolved
- entry.focused = (entry.offset <= cursor_offset <= entry.offset + entry.length)
+ if entry.type == "comment" and not $scope.reviewPanel.resolvedThreadIds[entry.thread_id]
+ entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
else if entry.type == "insert"
- entry.focused = (entry.offset <= cursor_offset <= entry.offset + entry.content.length)
+ entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
else if entry.type == "delete"
- entry.focused = (entry.offset == cursor_offset)
+ entry.focused = (entry.offset == selection_offset_start)
else if entry.type == "add-comment" and selection
entry.focused = true
$scope.$broadcast "review-panel:recalculate-screen-positions"
$scope.$broadcast "review-panel:layout"
-
+
$scope.acceptChange = (entry_id) ->
+ $http.post "/project/#{$scope.project_id}/doc/#{$scope.editor.open_doc_id}/changes/#{entry_id}/accept", {_csrf: window.csrfToken}
$scope.$broadcast "change:accept", entry_id
+ event_tracking.sendMB "rp-change-accepted", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
$scope.rejectChange = (entry_id) ->
$scope.$broadcast "change:reject", entry_id
+ event_tracking.sendMB "rp-change-rejected", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
$scope.startNewComment = () ->
- # $scope.commentState.adding = true
$scope.$broadcast "comment:select_line"
$timeout () ->
$scope.$broadcast "review-panel:layout"
$scope.submitNewComment = (content) ->
- # $scope.commentState.adding = false
- $scope.$broadcast "comment:add", content
- # $scope.commentState.content = ""
+ thread_id = RangesTracker.generateId()
+ thread = getThread(thread_id)
+ thread.submitting = true
+ $scope.$broadcast "comment:add", thread_id
+ $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken})
+ .error (error) ->
+ ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment")
+ $scope.$broadcast "editor:clearSelection"
$timeout () ->
$scope.$broadcast "review-panel:layout"
-
+ event_tracking.sendMB "rp-new-comment", { size: content.length }
+
$scope.cancelNewComment = (entry) ->
- # $scope.commentState.adding = false
- # $scope.commentState.content = ""
$timeout () ->
$scope.$broadcast "review-panel:layout"
@@ -267,117 +331,216 @@ define [
$timeout () ->
$scope.$broadcast "review-panel:layout"
- # $scope.handleCommentReplyKeyPress = (ev, entry) ->
- # if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey
- # ev.preventDefault()
- # ev.target.blur()
- # $scope.submitReply(entry)
+ $scope.submitReply = (entry, entry_id) ->
+ thread_id = entry.thread_id
+ content = entry.replyContent
+ $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken})
+ .error (error) ->
+ ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment")
+
+ trackingMetadata =
+ view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini'
+ size: entry.replyContent.length
+ thread: thread_id
- $scope.submitReply = (entry, entry_id) ->
- $scope.unresolveComment(entry_id)
- entry.thread.push {
- content: entry.replyContent
- ts: new Date()
- user_id: window.user_id
- }
+ thread = getThread(thread_id)
+ thread.submitting = true
entry.replyContent = ""
entry.replying = false
$timeout () ->
$scope.$broadcast "review-panel:layout"
- # TODO Just for prototyping purposes; remove afterwards
- window.setTimeout((() ->
- $scope.$applyAsync(() -> submitMockedReply(entry))
- ), 1000 * 2)
+ event_tracking.sendMB "rp-comment-reply", trackingMetadata
- # TODO Just for prototyping purposes; remove afterwards.
- submitMockedReply = (entry) ->
- entry.thread.push {
- content: 'Sounds good!'
- ts: new Date()
- user_id: mockedUserId
- }
- entry.replyContent = ""
- entry.replying = false
- $timeout () ->
- $scope.$broadcast "review-panel:layout"
-
$scope.cancelReply = (entry) ->
entry.replying = false
entry.replyContent = ""
$scope.$broadcast "review-panel:layout"
$scope.resolveComment = (entry, entry_id) ->
- entry.showWhenResolved = false
entry.focused = false
- $scope.$broadcast "comment:resolve", entry_id, window.user_id
-
- $scope.unresolveComment = (entry_id) ->
- $scope.$broadcast "comment:unresolve", entry_id
-
- $scope.deleteComment = (entry_id) ->
- $scope.$broadcast "comment:remove", entry_id
+ $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/resolve", {_csrf: window.csrfToken}
+ _onCommentResolved(entry.thread_id, ide.$scope.user)
+ event_tracking.sendMB "rp-comment-resolve", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
- $scope.showThread = (entry) ->
- entry.showWhenResolved = true
+ $scope.unresolveComment = (thread_id) ->
+ _onCommentReopened(thread_id)
+ $http.post "/project/#{$scope.project_id}/thread/#{thread_id}/reopen", {_csrf: window.csrfToken}
+ event_tracking.sendMB "rp-comment-reopen"
+
+ _onCommentResolved = (thread_id, user) ->
+ thread = getThread(thread_id)
+ return if !thread?
+ thread.resolved = true
+ thread.resolved_by_user = formatUser(user)
+ thread.resolved_at = new Date()
+ $scope.reviewPanel.resolvedThreadIds[thread_id] = true
+ $scope.$broadcast "comment:resolve_threads", [thread_id]
+
+ _onCommentReopened = (thread_id) ->
+ thread = getThread(thread_id)
+ return if !thread?
+ delete thread.resolved
+ delete thread.resolved_by_user
+ delete thread.resolved_at
+ delete $scope.reviewPanel.resolvedThreadIds[thread_id]
+ $scope.$broadcast "comment:unresolve_thread", thread_id
+
+ _onThreadDeleted = (thread_id) ->
+ delete $scope.reviewPanel.resolvedThreadIds[thread_id]
+ delete $scope.reviewPanel.commentThreads[thread_id]
+ $scope.$broadcast "comment:remove", thread_id
+
+ _onCommentEdited = (thread_id, comment_id, content) ->
+ thread = getThread(thread_id)
+ return if !thread?
+ for message in thread.messages
+ if message.id == comment_id
+ message.content = content
+ updateEntries()
+
+ _onCommentDeleted = (thread_id, comment_id) ->
+ thread = getThread(thread_id)
+ return if !thread?
+ thread.messages = thread.messages.filter (m) -> m.id != comment_id
+ updateEntries()
+
+ $scope.deleteThread = (entry_id, doc_id, thread_id) ->
+ _onThreadDeleted(thread_id)
+ $http({
+ method: "DELETE"
+ url: "/project/#{$scope.project_id}/doc/#{doc_id}/thread/#{thread_id}",
+ headers: {
+ 'X-CSRF-Token': window.csrfToken
+ }
+ })
+ event_tracking.sendMB "rp-comment-delete"
+
+ $scope.saveEdit = (thread_id, comment) ->
+ $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages/#{comment.id}/edit", {
+ content: comment.content
+ _csrf: window.csrfToken
+ })
$timeout () ->
$scope.$broadcast "review-panel:layout"
- $scope.hideThread = (entry) ->
- entry.showWhenResolved = false
+ $scope.deleteComment = (thread_id, comment) ->
+ _onCommentDeleted(thread_id, comment.id)
+ $http({
+ method: "DELETE"
+ url: "/project/#{$scope.project_id}/thread/#{thread_id}/messages/#{comment.id}",
+ headers: {
+ 'X-CSRF-Token': window.csrfToken
+ }
+ })
$timeout () ->
$scope.$broadcast "review-panel:layout"
$scope.setSubView = (subView) ->
$scope.reviewPanel.subView = subView
+ event_tracking.sendMB "rp-subview-change", { subView }
$scope.gotoEntry = (doc_id, entry) ->
ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset })
-
- DOC_ID_NAMES = {}
- $scope.getFileName = (doc_id) ->
- # This is called a lot and is relatively expensive, so cache the result
- if !DOC_ID_NAMES[doc_id]?
- entity = ide.fileTreeManager.findEntityById(doc_id)
- return if !entity?
- DOC_ID_NAMES[doc_id] = ide.fileTreeManager.getEntityPath(entity)
- return DOC_ID_NAMES[doc_id]
-
- # TODO: Eventually we need to get this from the server, and update it
- # when we get an id we don't know. This'll do for client side testing
- refreshUsers = () ->
- $scope.users = {}
- # TODO Just for prototyping purposes; remove afterwards.
- $scope.users[mockedUserId] = {
- email: "paulo@sharelatex.com"
- name: "Paulo Reis"
- isSelf: false
- hue: 70
- avatar_text: "PR"
- }
- $scope.users[mockedUserId2] = {
- email: "james@sharelatex.com"
- name: "James Allen"
- isSelf: false
- hue: 320
- avatar_text: "JA"
- }
-
- for member in $scope.project.members.concat($scope.project.owner)
- if member._id == window.user_id
- name = "You"
- isSelf = true
- else
- name = "#{member.first_name} #{member.last_name}"
- isSelf = false
-
- $scope.users[member._id] = {
- email: member.email
- name: name
- isSelf: isSelf
- hue: ColorManager.getHueForUserId(member._id)
- avatar_text: [member.first_name, member.last_name].filter((n) -> n?).map((n) -> n[0]).join ""
- }
- $scope.$watch "project.members", (members) ->
- return if !members?
- refreshUsers()
+ $scope.toggleTrackChanges = (value) ->
+ if $scope.project.features.trackChanges
+ $scope.editor.wantTrackChanges = value
+ $http.post "/project/#{$scope.project_id}/track_changes", {_csrf: window.csrfToken, on: value}
+ event_tracking.sendMB "rp-trackchanges-toggle", { value }
+ else
+ $scope.openTrackChangesUpgradeModal()
+
+ ide.socket.on "toggle-track-changes", (value) ->
+ $scope.$apply () ->
+ $scope.editor.wantTrackChanges = value
+
+ _refreshingRangeUsers = false
+ _refreshedForUserIds = {}
+ refreshChangeUsers = (refresh_for_user_id) ->
+ if refresh_for_user_id?
+ if _refreshedForUserIds[refresh_for_user_id]?
+ # We've already tried to refresh to get this user id, so stop it looping
+ return
+ _refreshedForUserIds[refresh_for_user_id] = true
+
+ # Only do one refresh at once
+ if _refreshingRangeUsers
+ return
+ _refreshingRangeUsers = true
+
+ $http.get "/project/#{$scope.project_id}/changes/users"
+ .success (users) ->
+ _refreshingRangeUsers = false
+ $scope.users = {}
+ # Always include ourself, since if we submit an op, we might need to display info
+ # about it locally before it has been flushed through the server
+ if ide.$scope.user?.id?
+ $scope.users[ide.$scope.user.id] = formatUser(ide.$scope.user)
+ for user in users
+ if user.id?
+ $scope.users[user.id] = formatUser(user)
+ .error () ->
+ _refreshingRangeUsers = false
+
+ _threadsLoaded = false
+ ensureThreadsAreLoaded = () ->
+ if _threadsLoaded
+ # We get any updates in real time so only need to load them once.
+ return
+ _threadsLoaded = true
+ $scope.reviewPanel.loadingThreads = true
+ $http.get "/project/#{$scope.project_id}/threads"
+ .success (threads) ->
+ $scope.reviewPanel.loadingThreads = false
+ for thread_id, _ of $scope.reviewPanel.resolvedThreadIds
+ delete $scope.reviewPanel.resolvedThreadIds[thread_id]
+ for thread_id, thread of threads
+ for comment in thread.messages
+ formatComment(comment)
+ if thread.resolved_by_user?
+ thread.resolved_by_user = formatUser(thread.resolved_by_user)
+ $scope.reviewPanel.resolvedThreadIds[thread_id] = true
+ $scope.$broadcast "comment:resolve_threads", [thread_id]
+ $scope.reviewPanel.commentThreads = threads
+ $timeout () ->
+ $scope.$broadcast "review-panel:layout"
+
+ formatComment = (comment) ->
+ comment.user = formatUser(comment.user)
+ comment.timestamp = new Date(comment.timestamp)
+ return comment
+
+ formatUser = (user) ->
+ id = user?._id or user?.id
+
+ if !id?
+ return {
+ email: null
+ name: "Anonymous"
+ isSelf: false
+ hue: ColorManager.ANONYMOUS_HUE
+ avatar_text: "A"
+ }
+ if id == window.user_id
+ name = "You"
+ isSelf = true
+ else
+ name = [user.first_name, user.last_name].filter((n) -> n? and n != "").join(" ")
+ if name == ""
+ name = user.email?.split("@")[0] or "Unknown"
+ isSelf = false
+ return {
+ id: id
+ email: user.email
+ name: name
+ isSelf: isSelf
+ hue: ColorManager.getHueForUserId(id)
+ avatar_text: [user.first_name, user.last_name].filter((n) -> n?).map((n) -> n[0]).join ""
+ }
+
+ $scope.openTrackChangesUpgradeModal = () ->
+ $modal.open {
+ templateUrl: "trackChangesUpgradeModalTemplate"
+ controller: "TrackChangesUpgradeModalController"
+ scope: $scope.$new()
+ }
diff --git a/services/web/public/coffee/ide/review-panel/controllers/TrackChangesUpgradeModalController.coffee b/services/web/public/coffee/ide/review-panel/controllers/TrackChangesUpgradeModalController.coffee
new file mode 100644
index 0000000000..ae8c049f69
--- /dev/null
+++ b/services/web/public/coffee/ide/review-panel/controllers/TrackChangesUpgradeModalController.coffee
@@ -0,0 +1,11 @@
+define [
+ "base"
+], (App) ->
+ App.controller "TrackChangesUpgradeModalController", ($scope, $modalInstance) ->
+ $scope.cancel = () ->
+ $modalInstance.dismiss()
+
+ $scope.startFreeTrial = (source) ->
+ ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
+ window.open("/user/subscription/new?planCode=student_free_trial_7_days")
+ $scope.startedFreeTrial = true
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee
index fd3edd09ca..124794e7b8 100644
--- a/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee
+++ b/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee
@@ -17,6 +17,8 @@ define [
scope.startNewComment = () ->
scope.state.isAdding = true
scope.onStartNew()
+ setTimeout () ->
+ scope.$broadcast "comment:new:open"
scope.cancelNewComment = () ->
scope.state.isAdding = false
@@ -25,11 +27,11 @@ define [
scope.handleCommentKeyPress = (ev) ->
if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey
ev.preventDefault()
- ev.target.blur()
- scope.submitNewComment()
+ if scope.state.content.length > 0
+ ev.target.blur()
+ scope.submitNewComment()
scope.submitNewComment = () ->
- console.log scope.state.content
scope.onSubmit { content: scope.state.content }
scope.state.isAdding = false
scope.state.content = ""
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee
index d436a34b2c..9dc1ef2a37 100644
--- a/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee
+++ b/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee
@@ -1,13 +1,30 @@
define [
"base"
], (App) ->
- App.directive "changeEntry", () ->
+ App.directive "changeEntry", ($timeout) ->
restrict: "E"
templateUrl: "changeEntryTemplate"
scope:
entry: "="
user: "="
+ permissions: "="
onAccept: "&"
onReject: "&"
onIndicatorClick: "&"
-
\ No newline at end of file
+ onBodyClick: "&"
+ link: (scope, element, attrs) ->
+ scope.contentLimit = 40
+ scope.isCollapsed = true
+ scope.needsCollapsing = false
+
+ element.on "click", (e) ->
+ if $(e.target).is('.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i')
+ scope.onBodyClick()
+
+ scope.toggleCollapse = () ->
+ scope.isCollapsed = !scope.isCollapsed
+ $timeout () ->
+ scope.$emit "review-panel:layout"
+
+ scope.$watch "entry.content.length", (contentLength) ->
+ scope.needsCollapsing = contentLength > scope.contentLimit
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee
index 6938062e2b..b2f09c96af 100644
--- a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee
+++ b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee
@@ -1,23 +1,65 @@
define [
"base"
], (App) ->
- App.directive "commentEntry", () ->
+ App.directive "commentEntry", ($timeout) ->
restrict: "E"
templateUrl: "commentEntryTemplate"
scope:
entry: "="
- users: "="
+ threads: "="
+ permissions: "="
onResolve: "&"
onReply: "&"
onIndicatorClick: "&"
+ onSaveEdit: "&"
onDelete: "&"
- onUnresolve: "&"
- onShowThread: "&"
- onHideThread: "&"
+ onBodyClick: "&"
link: (scope, element, attrs) ->
+ scope.state =
+ animating: false
+
+ element.on "click", (e) ->
+ if $(e.target).is('.rp-entry, .rp-comment-loaded, .rp-comment-content, .rp-comment-reply, .rp-entry-metadata')
+ scope.onBodyClick()
+
scope.handleCommentReplyKeyPress = (ev) ->
if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey
ev.preventDefault()
- ev.target.blur()
- scope.onReply()
-
\ No newline at end of file
+ if scope.entry.replyContent.length > 0
+ ev.target.blur()
+ scope.onReply()
+
+ scope.animateAndCallOnResolve = () ->
+ scope.state.animating = true
+ element.find(".rp-entry").css("top", 0)
+ $timeout((() -> scope.onResolve()), 350)
+ return true
+
+ scope.startEditing = (comment) ->
+ comment.editing = true
+ setTimeout () ->
+ scope.$emit "review-panel:layout"
+
+ scope.saveEdit = (comment) ->
+ comment.editing = false
+ scope.onSaveEdit({comment:comment})
+
+ scope.confirmDelete = (comment) ->
+ comment.deleting = true
+ setTimeout () ->
+ scope.$emit "review-panel:layout"
+
+ scope.cancelDelete = (comment) ->
+ comment.deleting = false
+ setTimeout () ->
+ scope.$emit "review-panel:layout"
+
+ scope.doDelete = (comment) ->
+ comment.deleting = false
+ scope.onDelete({comment: comment})
+
+ scope.saveEditOnEnter = (ev, comment) ->
+ if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey
+ ev.preventDefault()
+ scope.saveEdit(comment)
+
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee
new file mode 100644
index 0000000000..8a1d42990b
--- /dev/null
+++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee
@@ -0,0 +1,21 @@
+define [
+ "base"
+], (App) ->
+ App.directive "resolvedCommentEntry", () ->
+ restrict: "E"
+ templateUrl: "resolvedCommentEntryTemplate"
+ scope:
+ thread: "="
+ permissions: "="
+ onUnresolve: "&"
+ onDelete: "&"
+ link: (scope, element, attrs) ->
+ scope.contentLimit = 40
+ scope.needsCollapsing = false
+ scope.isCollapsed = true
+
+ scope.toggleCollapse = () ->
+ scope.isCollapsed = !scope.isCollapsed
+
+ scope.$watch "thread.content.length", (contentLength) ->
+ scope.needsCollapsing = contentLength > scope.contentLimit
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee
new file mode 100644
index 0000000000..d500d24db8
--- /dev/null
+++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee
@@ -0,0 +1,59 @@
+define [
+ "base"
+], (App) ->
+ App.directive "resolvedCommentsDropdown", (_) ->
+ restrict: "E"
+ templateUrl: "resolvedCommentsDropdownTemplate"
+ scope:
+ entries : "="
+ threads : "="
+ resolvedIds : "="
+ docs : "="
+ permissions: "="
+ onOpen : "&"
+ onUnresolve : "&"
+ onDelete : "&"
+ isLoading : "="
+
+ link: (scope, element, attrs) ->
+ scope.state =
+ isOpen: false
+
+ scope.toggleOpenState = () ->
+ scope.state.isOpen = !scope.state.isOpen
+ if (scope.state.isOpen)
+ scope.onOpen()
+ .then () -> filterResolvedComments()
+
+ scope.resolvedComments = []
+
+ scope.handleUnresolve = (threadId) ->
+ scope.onUnresolve({ threadId })
+ scope.resolvedComments = scope.resolvedComments.filter (c) -> c.threadId != threadId
+
+ scope.handleDelete = (entryId, docId, threadId) ->
+ scope.onDelete({ entryId, docId, threadId })
+ scope.resolvedComments = scope.resolvedComments.filter (c) -> c.threadId != threadId
+
+ getDocNameById = (docId) ->
+ doc = _.find(scope.docs, (doc) -> doc.doc.id == docId)
+ if doc?
+ return doc.path
+ else
+ return null
+
+ filterResolvedComments = () ->
+ scope.resolvedComments = []
+
+ for docId, docEntries of scope.entries
+ for entryId, entry of docEntries
+ if entry.type == "comment" and scope.threads[entry.thread_id]?.resolved?
+ resolvedComment = angular.copy scope.threads[entry.thread_id]
+
+ resolvedComment.content = entry.content
+ resolvedComment.threadId = entry.thread_id
+ resolvedComment.entryId = entryId
+ resolvedComment.docId = docId
+ resolvedComment.docName = getDocNameById(docId)
+
+ scope.resolvedComments.push(resolvedComment)
diff --git a/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee b/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee
index 82435faf44..4028406713 100644
--- a/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee
+++ b/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee
@@ -6,7 +6,11 @@ define [
link: (scope, element, attrs) ->
previous_focused_entry_index = 0
- layout = () ->
+ layout = (animate = true) ->
+ if animate
+ element.removeClass("no-animate")
+ else
+ element.addClass("no-animate")
sl_console.log "LAYOUT"
if scope.ui.reviewPanelOpen
PADDING = 8
@@ -32,6 +36,8 @@ define [
return if entries.length == 0
+ line_height = scope.reviewPanel.rendererData.lineHeight
+
focused_entry_index = Math.min(previous_focused_entry_index, entries.length - 1)
for entry, i in entries
if entry.scope.entry.focused
@@ -43,15 +49,34 @@ define [
previous_focused_entry_index = focused_entry_index
sl_console.log "focused_entry_index", focused_entry_index
-
- line_height = 15
-
- # Put the focused entry exactly where it wants to be
- focused_entry_top = Math.max(TOOLBAR_HEIGHT, focused_entry.scope.entry.screenPos.y)
+
+ # As we go backwards, we run the risk of pushing things off the top of the editor.
+ # If we go through the entries before and assume they are as pushed together as they
+ # could be, we can work out the 'ceiling' that each one can't go through. I.e. the first
+ # on can't go beyond the toolbar height, the next one can't go beyond the bottom of the first
+ # one at this minimum height, etc.
+ heights = (entry.$layout_el.height() for entry in entries_before)
+ previousMinTop = TOOLBAR_HEIGHT
+ min_tops = []
+ for height in heights
+ min_tops.push previousMinTop
+ previousMinTop += PADDING + height
+ min_tops.reverse()
+
+ positionLayoutEl = ($callout_el, original_top, top) ->
+ if original_top <= top
+ $callout_el.removeClass("rp-entry-callout-inverted")
+ $callout_el.css(top: original_top + line_height - 1, height: top - original_top)
+ else
+ $callout_el.addClass("rp-entry-callout-inverted")
+ $callout_el.css(top: top + line_height, height: original_top - top)
+
+ # Put the focused entry as close to where it wants to be as possible
+ focused_entry_top = Math.max(previousMinTop, focused_entry.scope.entry.screenPos.y)
focused_entry.$box_el.css(top: focused_entry_top)
focused_entry.$indicator_el.css(top: focused_entry_top)
- focused_entry.$callout_el.css(top: focused_entry_top + line_height, height: 0)
-
+ positionLayoutEl(focused_entry.$callout_el, focused_entry.scope.entry.screenPos.y, focused_entry_top)
+
previousBottom = focused_entry_top + focused_entry.$layout_el.height()
for entry in entries_after
original_top = entry.scope.entry.screenPos.y
@@ -60,31 +85,32 @@ define [
previousBottom = top + height
entry.$box_el.css(top: top)
entry.$indicator_el.css(top: top)
- entry.$callout_el.removeClass("rp-entry-callout-inverted")
- entry.$callout_el.css(top: original_top + line_height, height: top - original_top)
+ positionLayoutEl(entry.$callout_el, original_top, top)
sl_console.log "ENTRY", {entry: entry.scope.entry, top}
-
+
previousTop = focused_entry_top
entries_before.reverse() # Work through backwards, starting with the one just above
- for entry in entries_before
+ for entry, i in entries_before
original_top = entry.scope.entry.screenPos.y
height = entry.$layout_el.height()
original_bottom = original_top + height
bottom = Math.min(original_bottom, previousTop - PADDING)
- top = bottom - height
+ top = Math.max(bottom - height, min_tops[i])
previousTop = top
entry.$box_el.css(top: top)
entry.$indicator_el.css(top: top)
- entry.$callout_el.addClass("rp-entry-callout-inverted")
- entry.$callout_el.css(top: top + line_height + 1, height: original_top - top)
+ positionLayoutEl(entry.$callout_el, original_top, top)
sl_console.log "ENTRY", {entry: entry.scope.entry, top}
scope.$applyAsync () ->
layout()
- scope.$on "review-panel:layout", () ->
+ scope.$on "review-panel:layout", (e, animate = true) ->
scope.$applyAsync () ->
- layout()
+ layout(animate)
+
+ scope.$watch "reviewPanel.rendererData.lineHeight", () ->
+ layout()
## Scroll lock with Ace
scroller = element
diff --git a/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee b/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee
index e3844d1b12..2b5180dce6 100644
--- a/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee
+++ b/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee
@@ -4,10 +4,25 @@ define [
App.directive "reviewPanelToggle", () ->
restrict: "E"
scope:
- innerModel: '=ngModel'
+ onToggle: '='
+ ngModel: '='
+ disabled: '=?'
+ onDisabledClick: '=?'
+ link: (scope) ->
+ if !scope.disabled?
+ scope.disabled = false
+ scope.onChange = (args...) ->
+ scope.onToggle(scope.localModel)
+ scope.handleClick = () ->
+ if scope.disabled
+ scope.onDisabledClick()
+ scope.localModel = scope.ngModel
+ scope.$watch "ngModel", (value) ->
+ scope.localModel = value
+
template: """
-
-
+
+
"""
diff --git a/services/web/public/coffee/ide/review-panel/filters/notEmpty.coffee b/services/web/public/coffee/ide/review-panel/filters/notEmpty.coffee
new file mode 100644
index 0000000000..52100c7ff1
--- /dev/null
+++ b/services/web/public/coffee/ide/review-panel/filters/notEmpty.coffee
@@ -0,0 +1,5 @@
+define [
+ "base"
+], (App) ->
+ app.filter 'notEmpty', () ->
+ (object) -> !angular.equals({}, object)
diff --git a/services/web/public/coffee/main/project-list/left-hand-menu-promo-controller.coffee b/services/web/public/coffee/main/project-list/left-hand-menu-promo-controller.coffee
index 5a96bde5c1..9fdb3e9ca2 100644
--- a/services/web/public/coffee/main/project-list/left-hand-menu-promo-controller.coffee
+++ b/services/web/public/coffee/main/project-list/left-hand-menu-promo-controller.coffee
@@ -6,4 +6,4 @@ define [
$scope.hasProjects = window.data.projects.length > 0
$scope.userHasNoSubscription = window.userHasNoSubscription
- $scope.randomView = _.shuffle(["default", "dropbox", "github"])[0]
+
diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee
index 431c24ada7..161598d32d 100644
--- a/services/web/public/coffee/main/project-list/project-list.coffee
+++ b/services/web/public/coffee/main/project-list/project-list.coffee
@@ -14,10 +14,9 @@ define [
$scope.searchText =
value : ""
- if $scope.projects.length == 0
- $timeout () ->
- recalculateProjectListHeight()
- , 10
+ $timeout () ->
+ recalculateProjectListHeight()
+ , 10
recalculateProjectListHeight = () ->
topOffset = $(".project-list-card")?.offset()?.top
diff --git a/services/web/public/img/about/chris.jpg b/services/web/public/img/about/chris.jpg
new file mode 100644
index 0000000000..d317be783a
Binary files /dev/null and b/services/web/public/img/about/chris.jpg differ
diff --git a/services/web/public/img/about/geri.jpg b/services/web/public/img/about/geri.jpg
deleted file mode 100644
index 0de2f9a20e..0000000000
Binary files a/services/web/public/img/about/geri.jpg and /dev/null differ
diff --git a/services/web/public/img/about/joe_green.jpg b/services/web/public/img/about/joe_green.jpg
new file mode 100644
index 0000000000..0b730673d0
Binary files /dev/null and b/services/web/public/img/about/joe_green.jpg differ
diff --git a/services/web/public/img/teasers/track-changes/teaser-track-changes.gif b/services/web/public/img/teasers/track-changes/teaser-track-changes.gif
new file mode 100644
index 0000000000..00e02a29b0
Binary files /dev/null and b/services/web/public/img/teasers/track-changes/teaser-track-changes.gif differ
diff --git a/services/web/public/img/teasers/track-changes/teaser-track-changes.mp4 b/services/web/public/img/teasers/track-changes/teaser-track-changes.mp4
new file mode 100644
index 0000000000..1f75a461ad
Binary files /dev/null and b/services/web/public/img/teasers/track-changes/teaser-track-changes.mp4 differ
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 f183d7c263..8e7bbe4802 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,6 +242,10 @@ 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;
+ var params = Tokens[k+1];
+ if(params && params[1] === "Text") {
+ var paramNum = text.substring(params[2], params[3]);
+ if (paramNum.match(/^\[\d+\](\[[^\]]*\])*\s*$/)) {
+ 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(/^\[\d+\](\[[^\]]*\])*\s*$/)) {
+ 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;
@@ -1697,7 +1780,6 @@ var readUrl = function(TokeniseResult, k) {
return null;
};
-
var InterpretTokens = function (TokeniseResult, ErrorReporter) {
var Tokens = TokeniseResult.tokens;
var linePosition = TokeniseResult.linePosition;
@@ -1706,11 +1788,30 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
var TokenErrorFromTo = ErrorReporter.TokenErrorFromTo;
var TokenError = ErrorReporter.TokenError;
- var Environments = [];
+ var Environments = new EnvHandler(ErrorReporter);
+
+ var nextGroupMathMode = null; // if the next group should have
+ 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];
@@ -1759,15 +1860,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;};
@@ -1791,128 +1908,435 @@ 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" || 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) {
+ TokenError(token, type + seq + " must be inside math mode", {mathMode:true});
+ };
+ } else if (typeof seq === "string" && seq.match(/^(chapter|section|subsection|subsubsection)$/)) {
+ currentMathMode = Environments.getMathMode() ; // returns null / $(inline) / $$(display)
+ 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 === "$") {
+ 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 === "{") {
- Environments.push({command:"{", token:token});
- } else if (type === "}") {
- 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", {mathMode:true});
+ };
+ }
};
+
+ if (seenUserDefinedBeginEquation && seenUserDefinedEndEquation) {
+ ErrorReporter.filterMath = true;
+ };
+
return Environments;
};
-
-var CheckEnvironments = function (Environments, ErrorReporter) {
+var EnvHandler = function (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 = [];
- 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;
+
+ 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 (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 invalidEnvs = [];
+
+ this._end = function (thisEnv) {
+ do {
var lastEnv = state.pop();
+ var retry = false;
+ var i;
- 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) {
+ if (closedBy(lastEnv, thisEnv)) {
+ if (thisEnv.command === "end" && thisEnv.name === "document" && !documentClosed) {
documentClosed = thisEnv;
};
- continue;
+ return;
} else if (!lastEnv) {
- 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 + "}");
- }
+ if (documentClosed) {
+ ErrorFromTo(documentClosed, thisEnv, "\\end{" + documentClosed.name + "} is followed by unexpected content",{errorAtStart: true, type: "info"});
+ } else {
+ ErrorTo(thisEnv, "unexpected " + getName(thisEnv));
}
- } 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);
+ } 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);
+ }
}
+ 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;
}
- }
- while (state.length > 0) {
- thisEnv = state.pop();
- if (thisEnv.command === "{") {
- ErrorFrom(thisEnv, "unclosed group {", {type:"warning"});
- } else if (thisEnv.command === "begin") {
- ErrorFrom(thisEnv, "unclosed environment \\begin{" + thisEnv.name + "}");
+ };
+
+ 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 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;
+ 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, mathMode: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;
+
+ 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", {mathMode: true});
+ };
+ newMathMode = currentMathMode;
+ } else if (thisEnv.command === "begin") {
+ var name = thisEnv.name;
+ if (name) {
+ 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, mathMode: true});
+ resetMathMode();
+ };
+ newMathMode = null;
+ } 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, mathMode: true});
+ resetMathMode();
+ };
+ newMathMode = thisEnv;
+ } else {
+ newMathMode = undefined; // undefined means we don't know if we are in math mode or not
+ }
+ }
+ };
+ 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;
+ }
}
}
}
- }
+ };
+ 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;
@@ -1922,18 +2346,41 @@ 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) {
+ this.TokenError = function (token, message, options) {
+ if(!options) { options = { suppressIfEditing:true } ; };
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,
@@ -1945,10 +2392,12 @@ var ErrorReporter = function (TokeniseResult) {
text:message,
startPos: start,
endPos: end,
- suppressIfEditing:true});
+ suppressIfEditing:options.suppressIfEditing,
+ mathMode: options.mathMode});
};
- this.TokenErrorFromTo = function (fromToken, toToken, message) {
+ this.TokenErrorFromTo = function (fromToken, toToken, message, options) {
+ if(!options) { options = {suppressIfEditing:true } ; };
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;};
@@ -1965,7 +2414,8 @@ var ErrorReporter = function (TokeniseResult) {
text:message,
startPos: fromStart,
endPos: toEnd,
- suppressIfEditing:true});
+ suppressIfEditing:options.suppressIfEditing,
+ mathMode: options.mathMode});
};
@@ -1986,7 +2436,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) {
@@ -2002,7 +2453,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);
};
@@ -2019,7 +2471,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});
};
};
@@ -2027,7 +2480,7 @@ var Parse = function (text) {
var TokeniseResult = Tokenise(text);
var Reporter = new ErrorReporter(TokeniseResult);
var Environments = InterpretTokens(TokeniseResult, Reporter);
- CheckEnvironments(Environments, Reporter);
+ Environments.close();
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/file-tree.less b/services/web/public/stylesheets/app/editor/file-tree.less
index 5a4d7feed1..7847822bf9 100644
--- a/services/web/public/stylesheets/app/editor/file-tree.less
+++ b/services/web/public/stylesheets/app/editor/file-tree.less
@@ -35,6 +35,10 @@ aside#file-tree {
line-height: 2.6;
position: relative;
+ .entity {
+ user-select: none;
+ }
+
.entity-name {
color: @gray-darker;
cursor: pointer;
diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less
index d14e843591..0d4c0032e8 100644
--- a/services/web/public/stylesheets/app/editor/review-panel.less
+++ b/services/web/public/stylesheets/app/editor/review-panel.less
@@ -1,42 +1,61 @@
-@rp-base-font-size : 12px;
-@rp-small-font-size : 10px;
-@rp-icon-large-size : 22px;
+@rp-base-font-size : 12px;
+@rp-small-font-size : 10px;
+@rp-icon-large-size : 18px;
-@rp-bg-blue : #dadfed;
-@rp-bg-dim-blue : #fafafa;
-@rp-highlight-blue : #8a96b5;
+@rp-bg-blue : #dadfed;
+@rp-bg-dim-blue : #fafafa;
+@rp-highlight-blue : #8a96b5;
-@rp-border-grey : #d9d9d9;
+@rp-border-grey : #d9d9d9;
-@rp-green : #2c8e30;
-@rp-dim-green : #cae3cb;
-@rp-red : #c5060b;
-@rp-dim-red : #f3cdce;
-@rp-yellow : #f3b111;
-@rp-dim-yellow : #ffe9b2;
-@rp-grey : #aaaaaa;
+@rp-green : #2c8e30;
+@rp-dim-green : #cae3cb;
+@rp-green-on-dark : rgba(37, 107, 41, 0.5);
+@rp-red : #c5060b;
+@rp-dim-red : #f3cdce;
+@rp-yellow : #f3b111;
+@rp-yellow-on-dark : rgba(194, 93, 11, 0.5);
+@rp-dim-yellow : #ffe9b2;
+@rp-grey : #aaaaaa;
-@rp-type-blue : #6b7797;
-@rp-type-darkgrey : #3f3f3f;
+@rp-type-blue : #6b7797;
+@rp-type-darkgrey : #3f3f3f;
+
+@rp-entry-ribbon-width : 4px;
+@rp-entry-arrow-width : 6px;
+@rp-semibold-weight : 600;
+@review-panel-width : 230px;
+@review-off-width : 22px;
+
+@rp-toolbar-height : 32px;
-@rp-entry-ribbon-width : 4px;
-@rp-entry-arrow-width : 6px;
-@rp-semibold-weight : 600;
-@review-panel-width : 230px;
-@review-off-width : 22px;
-@rp-toolbar-height: 32px;
.rp-button() {
+ display: block; // IE doesn't do flex with inline items.
background-color: @rp-highlight-blue;
color: #FFF;
text-align: center;
+ line-height: 1.3;
+ user-select: none;
+ border: 0;
+
&:hover,
&:focus {
+ outline: 0;
background-color: darken(@rp-highlight-blue, 5%);
text-decoration: none;
color: #FFF;
}
+
+ &[disabled] {
+ opacity: 0.5;
+
+ &:hover,
+ &:focus {
+ background-color: @rp-highlight-blue;
+ }
+ }
}
.triangle(@_, @width, @height, @color) {
@@ -82,6 +101,7 @@
.rp-size-mini & {
display: block;
width: @review-off-width;
+ z-index: 6;
}
position: absolute;
@@ -92,6 +112,7 @@
border-left: solid 1px @rp-border-grey;
font-size: @rp-base-font-size;
color: @rp-type-blue;
+ z-index: 6;
}
.review-panel-toolbar {
@@ -102,23 +123,27 @@
justify-content: space-between;
padding: 0 5px;
}
- .rp-state-current-file & {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- }
+
+ position: relative;
height: @rp-toolbar-height;
border-bottom: 1px solid @rp-border-grey;
background-color: @rp-bg-dim-blue;
text-align: center;
- z-index: 2;
+ z-index: 3;
flex-basis: 32px;
flex-shrink: 0;
}
.review-panel-toolbar-label {
cursor: pointer;
- margin-right: 5px;
+ text-align: right;
+ flex-grow: 1;
+ }
+ .review-panel-toolbar-label-disabled {
+ cursor: auto;
+ margin-right: 5px;
+ }
+ .review-panel-toolbar-spinner {
+ margin-left: 5px;
}
.rp-entry-list {
@@ -135,7 +160,6 @@
bottom: 0;
}
-
.rp-state-overview & {
flex-grow: 2;
overflow-y: auto;
@@ -150,7 +174,6 @@
display: none;
.rp-size-mini & {
display: block;
- z-index: 12;
}
position: absolute;
left: 2px;
@@ -161,6 +184,9 @@
color: #FFF;
cursor: pointer;
transition: top 0.3s, left 0.1s, right 0.1s;
+ .no-animate & {
+ transition: none;
+ }
&-focused {
left: 0px;
@@ -186,21 +212,39 @@
display: none;
left: @review-off-width + @rp-entry-arrow-width;
box-shadow: 0 0 10px 5px rgba(0, 0, 0, .2);
- z-index: 11;
+ z-index: 1;
&::before {
- .triangle(left, @rp-entry-arrow-width, @rp-entry-arrow-width * 1.5, inherit);
- top: (@review-off-width / 2) - @rp-entry-arrow-width;
- left: -(@rp-entry-ribbon-width + @rp-entry-arrow-width);
- content: '';
- }
- &::after {
content: '';
position: absolute;
top: -(@review-off-width + @rp-entry-arrow-width);
right: -(@review-off-width + @rp-entry-arrow-width);
bottom: -(@review-off-width + @rp-entry-arrow-width);
+ left: -(2 * @rp-entry-arrow-width + 2);
+ z-index: -1;
+ }
+ &::after {
+ .triangle(left, @rp-entry-arrow-width, @rp-entry-arrow-width * 1.5, inherit);
+ top: (@review-off-width / 2) - @rp-entry-arrow-width;
+ left: -(@rp-entry-ribbon-width + @rp-entry-arrow-width);
+ content: '';
+ }
+ }
+ .rp-state-current-file-mini.rp-layout-left & {
+ left: auto;
+ right: @review-off-width + @rp-entry-arrow-width;
+ border-left-width: 0;
+ border-right-width: @rp-entry-ribbon-width;
+ border-right-style: solid;
+
+ &::before {
left: -(@review-off-width + @rp-entry-arrow-width);
+ right: -(2 * @rp-entry-arrow-width + 2);
+ }
+ &::after {
+ .triangle(right, @rp-entry-arrow-width, @rp-entry-arrow-width * 1.5, inherit);
+ right: -(@rp-entry-ribbon-width + @rp-entry-arrow-width);
+ left: auto;
}
}
.rp-state-current-file-expanded & {
@@ -224,15 +268,21 @@
}
.rp-state-overview & {
border-radius: 0;
- padding: 2px 5px;
border-bottom: solid 1px @rp-border-grey;
cursor: pointer;
}
+ .resolved-comments-dropdown & {
+ position: static;
+ margin-bottom: 5px;
+ }
border-left: solid @rp-entry-ribbon-width transparent;
border-radius: 3px;
background-color: #FFF;
transition: top 0.3s, left 0.1s, right 0.1s;
+ .no-animate & {
+ transition: none;
+ }
&-insert {
border-color: @rp-green;
@@ -246,6 +296,16 @@
border-color: @rp-yellow;
}
+ &-comment-resolving {
+ top: 4px;
+ left: 6px;
+ opacity: 0;
+ z-index: 3;
+ transform: scale(.1);
+ transform-origin: 0 0;
+ transition: top .35s ease-out, left .35s ease-out, transform .35s ease-out, opacity .35s ease-out .2s;
+ }
+
&-comment-resolved {
border-color: @rp-grey;
background-color: #efefef;
@@ -264,69 +324,59 @@
}
}
}
-
- .rp-entry-header {
+ .rp-entry-body {
display: flex;
align-items: center;
- padding: 5px;
-
- .rp-state-overview & {
- padding: 0px;
- }
+ padding: 4px 5px;
}
.rp-entry-action-icon {
font-size: @rp-icon-large-size;
- padding: 0 5px;
+ padding: 0 3px;
line-height: 0;
.rp-state-overview & {
- font-size: @rp-base-font-size;
- padding: 0px;
- margin-right: 5px;
+ display: none;
}
}
- .rp-entry-metadata {
- flex-grow: 1;
- padding: 0 5px;
- line-height: 1.2;
+ .rp-entry-details {
+ line-height: 1.4;
+ margin-left: 5px;
+ // We need to set any low-enough flex base size (0px), making it growable (1) and non-shrinkable (0).
+ // This is needed to ensure that IE makes the element fill the available space.
+ flex: 1 0 1px;
+ overflow-x: auto;
.rp-state-overview & {
- display: flex;
- line-height: inherit;
- padding: 0;
+ margin-left: 0;
}
}
- .rp-entry-metadata-line {
- margin: 0;
- .rp-state-overview &:last-of-type {
- flex-grow: 1;
- text-align: right;
+ .rp-entry-metadata {
+ font-size: @rp-small-font-size;
+ }
+ .rp-entry-user {
+ font-weight: @rp-semibold-weight;
+ font-style: normal;
+ }
+ .rp-comment-actions {
+ a { color: @rp-type-blue; }
+ }
+
+ .rp-content-highlight {
+ color: @rp-type-darkgrey;
+ font-weight: @rp-semibold-weight;
+ text-decoration: none;
+
+ .rp-entry-delete & {
+ text-decoration: line-through;
}
}
- .rp-entry-body {
- padding: 5px;
-
- .rp-state-overview & {
- padding: 0;
- }
- }
- .rp-content-highlight {
- color: @rp-type-darkgrey;
- font-weight: @rp-semibold-weight;
- text-decoration: none;
-
- .rp-entry-delete & {
- text-decoration: line-through;
- }
- }
-
.rp-entry-actions {
display: flex;
- .rp-state-overview & {
+ .rp-state-overview .rp-entry-list & {
display: none;
}
}
@@ -340,71 +390,54 @@
border-bottom-right-radius: 3px;
border-right-width: 0;
}
+
+ .rp-layout-left & {
+ &:first-child {
+ border-bottom-left-radius: 3px;
+ }
+ &:last-child {
+ border-bottom-right-radius: 0;
+ }
+ }
}
.rp-comment {
- display: flex;
- align-items: flex-start;
- padding: 5px;
+ margin: 2px 5px;
+ padding-bottom: 3px;
+ line-height: 1.4;
+ border-bottom: solid 1px @rp-border-grey;
- .rp-state-overview & {
- padding: 3px 0;
- line-height: 1.2;
+ &:last-child {
+ margin-bottom: 2px;
+ border-bottom-width: 0;
+ }
+
+ .rp-state-overview .rp-entry-list & {
+ margin: 4px 5px;
+
+ &:first-child {
+ margin-top: 0;
+ padding-top: 4px;
+ }
}
}
- .rp-comment-body {
- position: relative;
- background-color: currentColor;
- flex-grow: 1;
- padding: 2px 5px;
- margin-left: @rp-entry-arrow-width;
- border-radius: 3px;
-
- .rp-comment-self & {
- margin-left: 0;
- margin-right: @rp-entry-arrow-width;
- }
-
- &::after {
- .triangle(left, @rp-entry-arrow-width, @rp-entry-arrow-width * 1.5, inherit);
- top: (@review-off-width / 2) - @rp-entry-arrow-width;
- left: -@rp-entry-arrow-width;
- content: '';
-
- .rp-comment-self & {
- .triangle(right, @rp-entry-arrow-width, @rp-entry-arrow-width * 1.5, inherit);
- right: -@rp-entry-arrow-width;
- left: auto;
- }
-
- }
+ .rp-comment-content {
+ margin: 0;
+ color: @rp-type-darkgrey;
+ overflow-x: auto; // Long words, like links can overflow without this.
}
- .rp-comment-content {
- margin: 0;
- color: @rp-type-darkgrey;
- }
-
- .rp-comment-metadata {
- color: @rp-type-blue;
- font-size: @rp-small-font-size;
- margin: 0;
- }
-
- .rp-comment-reply {
- padding: 0 5px;
-
- .rp-state-overview & {
- padding: 3px 0 0;
- }
+
+ .rp-comment-resolver {
+ color: @rp-type-blue;
+ }
+ .rp-comment-resolver-content {
+ font-style: italic;
+ margin: 0;
}
- .rp-comment-resolved-description {
- padding: 5px;
-
- .rp-state-overview & {
- padding: 0px;
- }
- }
+ .rp-comment-reply {
+ padding: 0 5px;
+ }
.rp-add-comment-btn {
.rp-button();
@@ -424,26 +457,12 @@
border-radius: 3px;
border: solid 1px @rp-border-grey;
resize: vertical;
+ color: @rp-type-darkgrey;
+ margin-top: 3px;
+ overflow-x: hidden;
+ min-height: 3em;
}
-.rp-avatar {
- border-radius: 3px;
- font-weight: @rp-semibold-weight;
- font-size: @rp-icon-large-size;
- line-height: 1.2;
- text-transform: uppercase;
- color: #FFF;
- width: 1.3em;
- height: 1.3em;
- text-align: center;
- flex-grow: 0;
- flex-shrink: 0;
-
- .rp-state-overview & {
- display: none;
- }
-}
-
.rp-icon-delete {
display: inline-block;
line-height: 1;
@@ -456,6 +475,26 @@
}
}
+.rp-resolved-comment {
+ border-left: solid @rp-entry-ribbon-width @rp-yellow;
+ border-radius: 3px;
+ background-color: #FFF;
+ margin-bottom: 5px;
+}
+ .rp-resolved-comment-context {
+ background-color: lighten(@rp-yellow, 35%);
+ padding: 4px 5px;
+ }
+ .rp-resolved-comment-context-file {
+ font-weight: @rp-semibold-weight;
+ }
+
+ .rp-resolved-comment-context-quote {
+ color: #000;
+ font-family: @font-family-monospace;
+ margin: 0;
+ }
+
.rp-entry-callout {
.rp-state-current-file & {
position: absolute;
@@ -524,10 +563,24 @@
padding: 2px 5px;
border-top: solid 1px @rp-border-grey;
border-bottom: solid 1px @rp-border-grey;
- background-color: #FFF;
+ background-color: @rp-bg-dim-blue;
margin-top: 10px;
font-weight: @rp-semibold-weight;
- border-left: solid @rp-entry-ribbon-width currentColor;
+ text-align: center;
+}
+
+.rp-comment-wrapper {
+ transition: .35s opacity ease-out .2s;
+
+ &-resolving {
+ opacity: 0;
+ }
+}
+
+.rp-loading,
+.rp-empty {
+ text-align: center;
+ padding: 5px;
}
.rp-nav {
@@ -548,6 +601,7 @@
z-index: 2;
}
.rp-nav-item {
+ display: block;
color: lighten(@rp-type-blue, 25%);
flex: 0 0 50%;
border-top: solid 3px transparent;
@@ -599,6 +653,7 @@
.rp-toggle {
display: inline-block;
vertical-align: middle;
+ padding-left: 5px;
}
.rp-toggle-hidden-input {
display: none;
@@ -647,7 +702,7 @@
.track-changes-marker-callout {
border-radius: 0;
position: absolute;
- .rp-state-overview & {
+ .rp-state-overview &, .rp-loading-threads & {
display: none;
}
}
@@ -664,6 +719,9 @@
.track-changes-marker {
border-radius: 0;
position: absolute;
+ .rp-loading-threads & {
+ display: none;
+ }
}
.track-changes-comment-marker {
@@ -676,13 +734,25 @@
border-left: 2px dotted @rp-red;
margin-left: -1px;
}
+
+ .ace_dark {
+ .track-changes-comment-marker {
+ background-color: @rp-yellow-on-dark
+ }
+ .track-changes-added-marker {
+ background-color: @rp-green-on-dark;
+ }
+ }
}
.review-icon {
- position: absolute;
+ display: inline-block;
background: url('/img/review-icon-sprite.png') top/30px no-repeat;
width: 30px;
- height: 30px;
+
+ &::before {
+ content: '\00a0'; // Non-breakable space. A non-breakable character here makes this icon work like font-awesome.
+ }
.toolbar .btn-full-height:hover & {
background-position-y: -30px;
@@ -692,9 +762,126 @@
.toolbar .btn-full-height:active & {
background-position-y: -60px;
}
+}
- & + .toolbar-label {
- margin-left: 34px;
+.resolved-comments-toggle {
+ font-size: 14px;
+ color: lighten(@rp-type-blue, 25%);
+ border: solid 1px @rp-border-grey;
+ border-radius: 3px;
+ padding: 0 4px;
+ display: block;
+ height: 22px;
+ width: 22px;
+ line-height: 1.4;
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ color: @rp-type-blue;
+ }
+}
+
+.resolved-comments-backdrop {
+ display: none;
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+
+ &-visible {
+ display: block;
+ }
+}
+
+.resolved-comments-dropdown {
+ display: none;
+ position: absolute;
+ width: 300px;
+ left: -150px;
+ max-height: ~"calc(100vh - 100px)";
+ margin-top: @rp-entry-arrow-width * 1.5;
+ margin-left: 1em;
+ background-color: @rp-bg-blue;
+ text-align: left;
+ align-items: stretch;
+ justify-content: center;
+ border-radius: 3px;
+ box-shadow: 0 0 20px 10px rgba(0, 0, 0, .3);
+
+ &::before {
+ content: '';
+ .triangle(top, @rp-entry-arrow-width * 3, @rp-entry-arrow-width * 1.5, @rp-bg-blue);
+ top: -@rp-entry-ribbon-width * 2;
+ left: 50%;
+ margin-left: -@rp-entry-arrow-width * .75;
}
+ &-open {
+ display: flex;
+ }
+}
+ .resolved-comments-scroller {
+ flex: 0 0 auto; // Can't use 100% in the flex-basis key here, IE won't account for padding.
+ width: 100%; // We need to set the width explicitly, as flex-basis won't work.
+ max-height: ~"calc(100vh - 100px)"; // We also need to explicitly set the max-height, IE won't compute the flex-determined height.
+ padding: 5px;
+ overflow-y: auto;
+ }
+
+.rp-collapse-toggle {
+ color: @rp-type-blue;
+ font-weight: @rp-semibold-weight;
+
+ &:hover,
+ &:focus {
+ color: darken(@rp-type-blue, 5%);
+ text-decoration: none;
+ }
+}
+
+.rp-track-changes-indicator {
+ display: none;
+ position: absolute;
+ top: 0;
+ right: @review-off-width;
+ padding: 5px 10px;
+ background-color: rgba(240, 240, 240, 0.9);
+ color: @rp-type-blue;
+ text-align: center;
+ border-bottom-left-radius: 3px;
+ font-size: 10px;
+ z-index: 2;
+ white-space: nowrap;
+
+ &.rp-track-changes-indicator-on-dark {
+ background-color: rgba(88, 88, 88, .8);
+ color: #FFF;
+
+ &:hover,
+ &:focus {
+ background-color: rgba(88, 88, 88, 1);
+ color: #FFF;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ outline: 0;
+ text-decoration: none;
+ background-color: rgba(240, 240, 240, 1);
+ color: @rp-type-blue;
+ }
+
+ .rp-size-mini & {
+ display: block;
+ }
+}
+
+// Helper class for elements which aren't treated as flex-items by IE10, e.g:
+// * inline items;
+// * unknown elements (elements which aren't standard DOM elements, such as custom element directives)
+.rp-flex-block {
+ display: block;
}
diff --git a/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee
index 49e8292f97..daa0da0531 100644
--- a/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee
@@ -10,18 +10,21 @@ expect = require("chai").expect
describe 'AnnouncementsHandler', ->
beforeEach ->
- @user_id = "some_id"
+ @user =
+ _id:"some_id"
+ email: "someone@gmail.com"
@AnalyticsManager =
getLastOccurance: sinon.stub()
@BlogHandler =
getLatestAnnouncements:sinon.stub()
+ @settings = {}
@handler = SandboxedModule.require modulePath, requires:
"../Analytics/AnalyticsManager":@AnalyticsManager
"../Blog/BlogHandler":@BlogHandler
+ "settings-sharelatex":@settings
"logger-sharelatex":
log:->
-
describe "getUnreadAnnouncements", ->
beforeEach ->
@stubbedAnnouncements = [
@@ -44,7 +47,7 @@ describe 'AnnouncementsHandler', ->
it "should mark all announcements as read is false", (done)->
@AnalyticsManager.getLastOccurance.callsArgWith(2, null, [])
- @handler.getUnreadAnnouncements @user_id, (err, announcements)=>
+ @handler.getUnreadAnnouncements @user, (err, announcements)=>
announcements[0].read.should.equal false
announcements[1].read.should.equal false
announcements[2].read.should.equal false
@@ -53,7 +56,7 @@ describe 'AnnouncementsHandler', ->
it "should should be sorted again to ensure correct order", (done)->
@AnalyticsManager.getLastOccurance.callsArgWith(2, null, [])
- @handler.getUnreadAnnouncements @user_id, (err, announcements)=>
+ @handler.getUnreadAnnouncements @user, (err, announcements)=>
announcements[3].should.equal @stubbedAnnouncements[2]
announcements[2].should.equal @stubbedAnnouncements[3]
announcements[1].should.equal @stubbedAnnouncements[1]
@@ -62,7 +65,7 @@ describe 'AnnouncementsHandler', ->
it "should return older ones marked as read as well", (done)->
@AnalyticsManager.getLastOccurance.callsArgWith(2, null, {segmentation:{blogPostId:"/2014/04/12/title-date-irrelivant"}})
- @handler.getUnreadAnnouncements @user_id, (err, announcements)=>
+ @handler.getUnreadAnnouncements @user, (err, announcements)=>
announcements[0].id.should.equal @stubbedAnnouncements[0].id
announcements[0].read.should.equal false
@@ -79,7 +82,7 @@ describe 'AnnouncementsHandler', ->
it "should return all of them marked as read", (done)->
@AnalyticsManager.getLastOccurance.callsArgWith(2, null, {segmentation:{blogPostId:"/2016/11/01/introducting-latex-code-checker"}})
- @handler.getUnreadAnnouncements @user_id, (err, announcements)=>
+ @handler.getUnreadAnnouncements @user, (err, announcements)=>
announcements[0].read.should.equal true
announcements[1].read.should.equal true
announcements[2].read.should.equal true
@@ -87,3 +90,70 @@ describe 'AnnouncementsHandler', ->
done()
+ describe "with custom domain announcements", ->
+ beforeEach ->
+ @stubbedDomainSpecificAnn = [
+ {
+ domains: ["gmail.com", 'yahoo.edu']
+ title: "some message"
+ excerpt: "read this"
+ url:"http://www.sharelatex.com/i/somewhere"
+ id:"iaaa"
+ date: new Date(1308369600000).toString()
+ }
+ ]
+
+ @handler._domainSpecificAnnouncements = sinon.stub().returns(@stubbedDomainSpecificAnn)
+
+ it "should insert the domain specific in the correct place", (done)->
+ @AnalyticsManager.getLastOccurance.callsArgWith(2, null, [])
+ @handler.getUnreadAnnouncements @user, (err, announcements)=>
+ announcements[4].should.equal @stubbedAnnouncements[2]
+ announcements[3].should.equal @stubbedAnnouncements[3]
+ announcements[2].should.equal @stubbedAnnouncements[1]
+ announcements[1].should.equal @stubbedDomainSpecificAnn[0]
+ announcements[0].should.equal @stubbedAnnouncements[0]
+ done()
+
+ describe "_domainSpecificAnnouncements", ->
+ beforeEach ->
+ @settings.domainAnnouncements = [
+ {
+ domains: ["gmail.com", 'yahoo.edu']
+ title: "some message"
+ excerpt: "read this"
+ url:"http://www.sharelatex.com/i/somewhere"
+ id:"id1"
+ date: new Date(1308369600000).toString()
+ }, {
+ domains: ["gmail.com", 'yahoo.edu']
+ title: "some message"
+ excerpt: "read this"
+ url:"http://www.sharelatex.com/i/somewhere"
+ date: new Date(1308369600000).toString()
+ }, {
+ domains: ["gmail.com", 'yahoo.edu']
+ title: "some message"
+ excerpt: "read this"
+ url:"http://www.sharelatex.com/i/somewhere"
+ id:"id3"
+ date: new Date(1308369600000).toString()
+ }
+ ]
+
+ it "should filter announcments which don't have an id", (done) ->
+ result = @handler._domainSpecificAnnouncements "someone@gmail.com"
+ result.length.should.equal 2
+ result[0].id.should.equal "id1"
+ result[1].id.should.equal "id3"
+ done()
+
+
+ it "should match on domain", (done) ->
+ @settings.domainAnnouncements[2].domains = ["yahoo.com"]
+ result = @handler._domainSpecificAnnouncements "someone@gmail.com"
+ result.length.should.equal 1
+ result[0].id.should.equal "id1"
+ done()
+
+
diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
index 72265eac11..94e930c7b1 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"
@@ -543,6 +550,15 @@ describe "AuthenticationController", ->
@AuthenticationController._setRedirectInSession(@req, '/somewhere/specific')
expect(@req.session.postLoginRedirect).to.equal "/somewhere/specific"
+ describe 'with a js path', ->
+
+ beforeEach ->
+ @req = {session: {}}
+
+ it 'should not set the redirect', ->
+ @AuthenticationController._setRedirectInSession(@req, '/js/something.js')
+ expect(@req.session.postLoginRedirect).to.equal undefined
+
describe '_getRedirectFromSession', ->
beforeEach ->
@req = {session: {postLoginRedirect: "/a?b=c"}}
diff --git a/services/web/test/UnitTests/coffee/Chat/ChatApiHandlerTests.coffee b/services/web/test/UnitTests/coffee/Chat/ChatApiHandlerTests.coffee
new file mode 100644
index 0000000000..ea569b8a53
--- /dev/null
+++ b/services/web/test/UnitTests/coffee/Chat/ChatApiHandlerTests.coffee
@@ -0,0 +1,92 @@
+should = require('chai').should()
+SandboxedModule = require('sandboxed-module')
+assert = require('assert')
+path = require('path')
+sinon = require('sinon')
+modulePath = path.join __dirname, "../../../../app/js/Features/Chat/ChatApiHandler"
+expect = require("chai").expect
+
+describe "ChatApiHandler", ->
+ beforeEach ->
+ @settings =
+ apis:
+ chat:
+ internal_url:"chat.sharelatex.env"
+ @request = sinon.stub()
+ @ChatApiHandler = SandboxedModule.require modulePath, requires:
+ "settings-sharelatex": @settings
+ "logger-sharelatex": { log: sinon.stub(), error: sinon.stub() }
+ "request": @request
+ @project_id = "3213213kl12j"
+ @user_id = "2k3jlkjs9"
+ @content = "my message here"
+ @callback = sinon.stub()
+
+ describe "sendGlobalMessage", ->
+ describe "successfully", ->
+ beforeEach ->
+ @message = { "mock": "message" }
+ @request.callsArgWith(1, null, {statusCode: 200}, @message)
+ @ChatApiHandler.sendGlobalMessage @project_id, @user_id, @content, @callback
+
+ it "should post the data to the chat api", ->
+ @request.calledWith({
+ url: "#{@settings.apis.chat.internal_url}/project/#{@project_id}/messages"
+ method: "POST"
+ json:
+ content: @content
+ user_id: @user_id
+ }).should.equal true
+
+ it "should return the message from the post", ->
+ @callback.calledWith(null, @message).should.equal true
+
+ describe "with a non-success status code", ->
+ beforeEach ->
+ @request.callsArgWith(1, null, {statusCode: 500})
+ @ChatApiHandler.sendGlobalMessage @project_id, @user_id, @content, @callback
+
+ it "should return an error", ->
+ error = new Error()
+ error.statusCode = 500
+ @callback.calledWith(error).should.equal true
+
+ describe "getGlobalMessages", ->
+ beforeEach ->
+ @messages = [{ "mock": "message" }]
+ @limit = 30
+ @before = "1234"
+
+ describe "successfully", ->
+ beforeEach ->
+ @request.callsArgWith(1, null, {statusCode: 200}, @messages)
+ @ChatApiHandler.getGlobalMessages @project_id, @limit, @before, @callback
+
+ it "should make get request for room to chat api", ->
+ @request.calledWith({
+ method: "GET"
+ url: "#{@settings.apis.chat.internal_url}/project/#{@project_id}/messages"
+ qs:
+ limit: @limit
+ before: @before
+ json: true
+ }).should.equal true
+
+ it "should return the messages from the request", ->
+ @callback.calledWith(null, @messages).should.equal true
+
+ describe "with failure error code", ->
+ beforeEach ->
+ @request.callsArgWith(1, null, {statusCode: 500}, null)
+ @ChatApiHandler.getGlobalMessages @project_id, @limit, @before, @callback
+
+ it "should return an error", ->
+ error = new Error()
+ error.statusCode = 500
+ @callback.calledWith(error).should.equal true
+
+
+
+
+
+
diff --git a/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee b/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee
index a491e4b499..851eb47f09 100644
--- a/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee
@@ -7,75 +7,76 @@ modulePath = path.join __dirname, "../../../../app/js/Features/Chat/ChatControll
expect = require("chai").expect
describe "ChatController", ->
-
beforeEach ->
-
- @user_id = 'ier_'
+ @user_id = 'mock-user-id'
@settings = {}
- @ChatHandler =
- sendMessage:sinon.stub()
- getMessages:sinon.stub()
-
+ @ChatApiHandler = {}
@EditorRealTimeController =
emitToRoom:sinon.stub().callsArgWith(3)
-
@AuthenticationController =
getLoggedInUserId: sinon.stub().returns(@user_id)
@ChatController = SandboxedModule.require modulePath, requires:
- "settings-sharelatex":@settings
- "logger-sharelatex": log:->
- "./ChatHandler":@ChatHandler
- "../Editor/EditorRealTimeController":@EditorRealTimeController
+ "settings-sharelatex": @settings
+ "logger-sharelatex": log: ->
+ "./ChatApiHandler": @ChatApiHandler
+ "../Editor/EditorRealTimeController": @EditorRealTimeController
'../Authentication/AuthenticationController': @AuthenticationController
- @query =
- before:"some time"
-
+ '../User/UserInfoManager': @UserInfoManager = {}
+ '../User/UserInfoController': @UserInfoController = {}
+ '../Comments/CommentsController': @CommentsController = {}
@req =
params:
- Project_id:@project_id
- session:
- user:
- _id:@user_id
- body:
- content:@messageContent
+ project_id: @project_id
@res =
- set:sinon.stub()
+ json: sinon.stub()
+ send: sinon.stub()
describe "sendMessage", ->
-
- it "should tell the chat handler about the message", (done)->
- @ChatHandler.sendMessage.callsArgWith(3)
- @res.send = =>
- @ChatHandler.sendMessage.calledWith(@project_id, @user_id, @messageContent).should.equal true
- done()
+ beforeEach ->
+ @req.body =
+ content: @content = "message-content"
+ @UserInfoManager.getPersonalInfo = sinon.stub().yields(null, @user = {"unformatted": "user"})
+ @UserInfoController.formatPersonalInfo = sinon.stub().returns(@formatted_user = {"formatted": "user"})
+ @ChatApiHandler.sendGlobalMessage = sinon.stub().yields(null, @message = {"mock": "message", user_id: @user_id})
@ChatController.sendMessage @req, @res
- it "should tell the editor real time controller about the update with the data from the chat handler", (done)->
- @chatMessage =
- content:"hello world"
- @ChatHandler.sendMessage.callsArgWith(3, null, @chatMessage)
- @res.send = =>
- @EditorRealTimeController.emitToRoom.calledWith(@project_id, "new-chat-message", @chatMessage).should.equal true
- done()
- @ChatController.sendMessage @req, @res
+ it "should look up the user", ->
+ @UserInfoManager.getPersonalInfo
+ .calledWith(@user_id)
+ .should.equal true
+
+ it "should format and inject the user into the message", ->
+ @UserInfoController.formatPersonalInfo
+ .calledWith(@user)
+ .should.equal true
+ @message.user.should.deep.equal @formatted_user
+
+ it "should tell the chat handler about the message", ->
+ @ChatApiHandler.sendGlobalMessage
+ .calledWith(@project_id, @user_id, @content)
+ .should.equal true
+
+ it "should tell the editor real time controller about the update with the data from the chat handler", ->
+ @EditorRealTimeController.emitToRoom
+ .calledWith(@project_id, "new-chat-message", @message)
+ .should.equal true
+
+ it "should return a 204 status code", ->
+ @res.send.calledWith(204).should.equal true
describe "getMessages", ->
beforeEach ->
- @req.query = @query
-
- it "should ask the chat handler about the request", (done)->
-
- @ChatHandler.getMessages.callsArgWith(2)
- @res.send = =>
- @ChatHandler.getMessages.calledWith(@project_id, @query).should.equal true
- done()
+ @req.query =
+ limit: @limit = "30"
+ before: @before = "12345"
+ @CommentsController._injectUserInfoIntoThreads = sinon.stub().yields()
+ @ChatApiHandler.getGlobalMessages = sinon.stub().yields(null, @messages = ["mock", "messages"])
@ChatController.getMessages @req, @res
- it "should return the messages", (done)->
- messages = [{content:"hello"}]
- @ChatHandler.getMessages.callsArgWith(2, null, messages)
- @res.send = (sentMessages)=>
- @res.set.calledWith('Content-Type', 'application/json').should.equal true
- sentMessages.should.deep.equal messages
- done()
- @ChatController.getMessages @req, @res
+ it "should ask the chat handler about the request", ->
+ @ChatApiHandler.getGlobalMessages
+ .calledWith(@project_id, @limit, @before)
+ .should.equal true
+
+ it "should return the messages", ->
+ @res.json.calledWith(@messages).should.equal true
\ No newline at end of file
diff --git a/services/web/test/UnitTests/coffee/Chat/ChatHandlerTests.coffee b/services/web/test/UnitTests/coffee/Chat/ChatHandlerTests.coffee
deleted file mode 100644
index 22b6a575cc..0000000000
--- a/services/web/test/UnitTests/coffee/Chat/ChatHandlerTests.coffee
+++ /dev/null
@@ -1,89 +0,0 @@
-should = require('chai').should()
-SandboxedModule = require('sandboxed-module')
-assert = require('assert')
-path = require('path')
-sinon = require('sinon')
-modulePath = path.join __dirname, "../../../../app/js/Features/Chat/ChatHandler"
-expect = require("chai").expect
-
-describe "ChatHandler", ->
-
- beforeEach ->
-
- @settings =
- apis:
- chat:
- internal_url:"chat.sharelatex.env"
- @request = sinon.stub()
- @ChatHandler = SandboxedModule.require modulePath, requires:
- "settings-sharelatex":@settings
- "logger-sharelatex": log:->
- "request": @request
- @project_id = "3213213kl12j"
- @user_id = "2k3jlkjs9"
- @messageContent = "my message here"
-
- describe "sending message", ->
-
- beforeEach ->
- @messageResponse =
- message:"Details"
- @request.callsArgWith(1, null, null, @messageResponse)
-
- it "should post the data to the chat api", (done)->
-
- @ChatHandler.sendMessage @project_id, @user_id, @messageContent, (err)=>
- @opts =
- method:"post"
- json:
- content:@messageContent
- user_id:@user_id
- uri:"#{@settings.apis.chat.internal_url}/room/#{@project_id}/messages"
- @request.calledWith(@opts).should.equal true
- done()
-
- it "should return the message from the post", (done)->
- @ChatHandler.sendMessage @project_id, @user_id, @messageContent, (err, returnedMessage)=>
- returnedMessage.should.equal @messageResponse
- done()
-
- describe "get messages", ->
-
- beforeEach ->
- @returnedMessages = [{content:"hello world"}]
- @request.callsArgWith(1, null, null, @returnedMessages)
- @query = {}
-
- it "should make get request for room to chat api", (done)->
-
- @ChatHandler.getMessages @project_id, @query, (err)=>
- @opts =
- method:"get"
- uri:"#{@settings.apis.chat.internal_url}/room/#{@project_id}/messages"
- qs:{}
- @request.calledWith(@opts).should.equal true
- done()
-
- it "should make get request for room to chat api with query string", (done)->
- @query = {limit:5, before:12345, ignore:"this"}
-
- @ChatHandler.getMessages @project_id, @query, (err)=>
- @opts =
- method:"get"
- uri:"#{@settings.apis.chat.internal_url}/room/#{@project_id}/messages"
- qs:
- limit:5
- before:12345
- @request.calledWith(@opts).should.equal true
- done()
-
- it "should return the messages from the request", (done)->
- @ChatHandler.getMessages @project_id, @query, (err, returnedMessages)=>
- returnedMessages.should.equal @returnedMessages
- done()
-
-
-
-
-
-
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 28bf1ab6a2..453296b3d6 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -14,11 +14,20 @@ describe "CollaboratorsInviteController", ->
@user =
_id: 'id'
@AnalyticsManger = recordEvent: sinon.stub()
+ @sendingUser = null
@AuthenticationController =
- getSessionUser: (req) => req.session.user
+ getSessionUser: (req) =>
+ @sendingUser = req.session.user
+ return @sendingUser
+
+ @RateLimiter =
+ addCount: sinon.stub
+
+ @LimitationsManager = {}
+
@CollaboratorsInviteController = SandboxedModule.require modulePath, requires:
"../Project/ProjectGetter": @ProjectGetter = {}
- '../Subscription/LimitationsManager' : @LimitationsManager = {}
+ '../Subscription/LimitationsManager' : @LimitationsManager
'../User/UserGetter': @UserGetter = {getUser: sinon.stub()}
"./CollaboratorsHandler": @CollaboratorsHandler = {}
"./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
@@ -28,6 +37,7 @@ describe "CollaboratorsInviteController", ->
"../Analytics/AnalyticsManager": @AnalyticsManger
'../Authentication/AuthenticationController': @AuthenticationController
'settings-sharelatex': @settings = {}
+ "../../infrastructure/RateLimiter":@RateLimiter
@res = new MockResponse()
@req = new MockRequest()
@@ -104,15 +114,11 @@ describe "CollaboratorsInviteController", ->
describe 'when all goes well', ->
beforeEach ->
- @_checkShouldInviteEmail = sinon.stub(
- @CollaboratorsInviteController, '_checkShouldInviteEmail'
- ).callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(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])
@@ -122,8 +128,8 @@ describe "CollaboratorsInviteController", ->
@LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
it 'should have called _checkShouldInviteEmail', ->
- @_checkShouldInviteEmail.callCount.should.equal 1
- @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
+ @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
+ @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
it 'should have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
@@ -136,22 +142,18 @@ describe "CollaboratorsInviteController", ->
describe 'when the user is not allowed to add more collaborators', ->
beforeEach ->
- @_checkShouldInviteEmail = sinon.stub(
- @CollaboratorsInviteController, '_checkShouldInviteEmail'
- ).callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(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
+ @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0
+ @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal false
it 'should not have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
@@ -159,23 +161,19 @@ describe "CollaboratorsInviteController", ->
describe 'when canAddXCollaborators produces an error', ->
beforeEach ->
- @_checkShouldInviteEmail = sinon.stub(
- @CollaboratorsInviteController, '_checkShouldInviteEmail'
- ).callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(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
+ @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0
+ @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal false
it 'should not have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
@@ -183,16 +181,12 @@ describe "CollaboratorsInviteController", ->
describe 'when inviteToProject produces an error', ->
beforeEach ->
- @_checkShouldInviteEmail = sinon.stub(
- @CollaboratorsInviteController, '_checkShouldInviteEmail'
- ).callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(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
@@ -202,8 +196,8 @@ describe "CollaboratorsInviteController", ->
@LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
it 'should have called _checkShouldInviteEmail', ->
- @_checkShouldInviteEmail.callCount.should.equal 1
- @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
+ @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
+ @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
it 'should have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
@@ -212,22 +206,18 @@ describe "CollaboratorsInviteController", ->
describe 'when _checkShouldInviteEmail disallows the invite', ->
beforeEach ->
- @_checkShouldInviteEmail = sinon.stub(
- @CollaboratorsInviteController, '_checkShouldInviteEmail'
- ).callsArgWith(1, null, false)
+ @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, false)
+ @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true)
@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
+ @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
+ @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
it 'should not have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
@@ -235,26 +225,66 @@ describe "CollaboratorsInviteController", ->
describe 'when _checkShouldInviteEmail produces an error', ->
beforeEach ->
- @_checkShouldInviteEmail = sinon.stub(
- @CollaboratorsInviteController, '_checkShouldInviteEmail'
- ).callsArgWith(1, new Error('woops'))
+ @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, new Error('woops'))
+ @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true)
@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
+ @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1
+ @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@targetEmail).should.equal true
it 'should not have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
+ describe 'when the user invites themselves to the project', ->
+
+ beforeEach ->
+ @req.session.user = {_id: 'abc', email: 'me@example.com'}
+ @req.body.email = 'me@example.com'
+ @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true)
+ @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
+ @CollaboratorsInviteController.inviteToProject @req, @res, @next
+
+
+ it 'should reject action, return json response with error code', ->
+ @res.json.callCount.should.equal 1
+ ({invite: null, error: 'cannot_invite_self'}).should.deep.equal(@res.json.firstCall.args[0])
+
+ it 'should not have called canAddXCollaborators', ->
+ @LimitationsManager.canAddXCollaborators.callCount.should.equal 0
+
+ it 'should not have called _checkShouldInviteEmail', ->
+ @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0
+
+ it 'should not have called inviteToProject', ->
+ @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
+
+ it 'should not have called emitToRoom', ->
+ @EditorRealTimeController.emitToRoom.callCount.should.equal 0
+
+ describe 'when _checkRateLimit returns false', ->
+
+ beforeEach ->
+ @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, false)
+ @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
+ @CollaboratorsInviteController.inviteToProject @req, @res, @next
+
+ it 'should send a 429 response', ->
+ @res.sendStatus.calledWith(429).should.equal true
+
+ it 'should not call inviteToProject', ->
+ @CollaboratorsInviteHandler.inviteToProject.called.should.equal false
+
+ it 'should not call emitToRoom', ->
+ @EditorRealTimeController.emitToRoom.called.should.equal false
+
describe "viewInvite", ->
beforeEach ->
@@ -671,13 +701,13 @@ describe "CollaboratorsInviteController", ->
beforeEach ->
@email = 'user@example.com'
- @call = (callback) =>
- @CollaboratorsInviteController._checkShouldInviteEmail @email, callback
describe 'when we should be restricting to existing accounts', ->
beforeEach ->
@settings.restrictInvitesToExistingAccounts = true
+ @call = (callback) =>
+ @CollaboratorsInviteController._checkShouldInviteEmail @email, callback
describe 'when user account is present', ->
@@ -722,18 +752,43 @@ describe "CollaboratorsInviteController", ->
expect(shouldAllow).to.equal undefined
done()
- describe 'when we should not be restricting', ->
+ describe '_checkRateLimit', ->
+ beforeEach ->
+ @settings.restrictInvitesToExistingAccounts = false
+ @sendingUserId = "32312313"
+ @LimitationsManager.allowedNumberOfCollaboratorsForUser = sinon.stub()
+ @LimitationsManager.allowedNumberOfCollaboratorsForUser.withArgs(@sendingUserId).yields(null, 17)
- beforeEach ->
- @settings.restrictInvitesToExistingAccounts = false
+ it 'should callback with `true` when rate limit under', (done) ->
+ @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=>
+ @RateLimiter.addCount.called.should.equal true
+ result.should.equal true
+ done()
- it 'should callback with `true`', (done) ->
- @call (err, shouldAllow) =>
- expect(err).to.equal null
- expect(shouldAllow).to.equal true
- done()
+ it 'should callback with `false` when rate limit hit', (done) ->
+ @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false)
+ @CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=>
+ @RateLimiter.addCount.called.should.equal true
+ result.should.equal false
+ done()
+
+ it 'should call rate limiter with 10x the collaborators', (done) ->
+ @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=>
+ @RateLimiter.addCount.args[0][0].throttle.should.equal(170)
+ done()
- it 'should not have called getUser', (done) ->
- @call (err, shouldAllow) =>
- @UserGetter.getUser.callCount.should.equal 0
- done()
+ it 'should call rate limiter with 200 when collaborators is -1', (done) ->
+ @LimitationsManager.allowedNumberOfCollaboratorsForUser.withArgs(@sendingUserId).yields(null, -1)
+ @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=>
+ @RateLimiter.addCount.args[0][0].throttle.should.equal(200)
+ done()
+
+ it 'should call rate limiter with 10 when user has no collaborators set', (done) ->
+ @LimitationsManager.allowedNumberOfCollaboratorsForUser.withArgs(@sendingUserId).yields(null)
+ @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
+ @CollaboratorsInviteController._checkRateLimit @sendingUserId, (err, result)=>
+ @RateLimiter.addCount.args[0][0].throttle.should.equal(10)
+ done()
\ No newline at end of file
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index ac94fcf10d..177c42d4ba 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -185,7 +185,7 @@ describe "CollaboratorsInviteHandler", ->
describe '_sendMessages', ->
beforeEach ->
- @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(3, null)
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(4, null)
@CollaboratorsInviteHandler._trySendInviteNotification = sinon.stub().callsArgWith(3, null)
@call = (callback) =>
@CollaboratorsInviteHandler._sendMessages @projectId, @sendingUser, @fakeInvite, callback
@@ -213,7 +213,7 @@ describe "CollaboratorsInviteHandler", ->
describe 'when CollaboratorsEmailHandler.notifyUserOfProjectInvite produces an error', ->
beforeEach ->
- @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(3, new Error('woops'))
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(4, new Error('woops'))
it 'should produce an error', (done) ->
@call (err, invite) =>
diff --git a/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee b/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee
new file mode 100644
index 0000000000..e55f0d04da
--- /dev/null
+++ b/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee
@@ -0,0 +1,284 @@
+should = require('chai').should()
+SandboxedModule = require('sandboxed-module')
+assert = require('assert')
+path = require('path')
+sinon = require('sinon')
+modulePath = path.join __dirname, "../../../../app/js/Features/Comments/CommentsController"
+expect = require("chai").expect
+
+describe "CommentsController", ->
+ beforeEach ->
+ @user_id = 'mock-user-id'
+ @settings = {}
+ @ChatApiHandler = {}
+ @EditorRealTimeController =
+ emitToRoom:sinon.stub()
+ @AuthenticationController =
+ getLoggedInUserId: sinon.stub().returns(@user_id)
+ @CommentsController = SandboxedModule.require modulePath, requires:
+ "settings-sharelatex": @settings
+ "logger-sharelatex": log: ->
+ "../Chat/ChatApiHandler": @ChatApiHandler
+ "../Editor/EditorRealTimeController": @EditorRealTimeController
+ '../Authentication/AuthenticationController': @AuthenticationController
+ '../User/UserInfoManager': @UserInfoManager = {}
+ '../User/UserInfoController': @UserInfoController = {}
+ "../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler = {}
+ @req = {}
+ @res =
+ json: sinon.stub()
+ send: sinon.stub()
+
+ describe "sendComment", ->
+ beforeEach ->
+ @req.params =
+ project_id: @project_id = "mock-project-id"
+ thread_id: @thread_id = "mock-thread-id"
+ @req.body =
+ content: @content = "message-content"
+ @UserInfoManager.getPersonalInfo = sinon.stub().yields(null, @user = {"unformatted": "user"})
+ @UserInfoController.formatPersonalInfo = sinon.stub().returns(@formatted_user = {"formatted": "user"})
+ @ChatApiHandler.sendComment = sinon.stub().yields(null, @message = {"mock": "message", user_id: @user_id})
+ @CommentsController.sendComment @req, @res
+
+ it "should look up the user", ->
+ @UserInfoManager.getPersonalInfo
+ .calledWith(@user_id)
+ .should.equal true
+
+ it "should format and inject the user into the comment", ->
+ @UserInfoController.formatPersonalInfo
+ .calledWith(@user)
+ .should.equal true
+ @message.user.should.deep.equal @formatted_user
+
+ it "should tell the chat handler about the message", ->
+ @ChatApiHandler.sendComment
+ .calledWith(@project_id, @thread_id, @user_id, @content)
+ .should.equal true
+
+ it "should tell the editor real time controller about the update with the data from the chat handler", ->
+ @EditorRealTimeController.emitToRoom
+ .calledWith(@project_id, "new-comment", @thread_id, @message)
+ .should.equal true
+
+ it "should return a 204 status code", ->
+ @res.send.calledWith(204).should.equal true
+
+ describe "getThreads", ->
+ beforeEach ->
+ @req.params =
+ project_id: @project_id = "mock-project-id"
+ @ChatApiHandler.getThreads = sinon.stub().yields(null, @threads = {"mock", "threads"})
+ @CommentsController._injectUserInfoIntoThreads = sinon.stub().yields(null, @threads)
+ @CommentsController.getThreads @req, @res
+
+ it "should ask the chat handler about the request", ->
+ @ChatApiHandler.getThreads
+ .calledWith(@project_id)
+ .should.equal true
+
+ it "should inject the user details into the threads", ->
+ @CommentsController._injectUserInfoIntoThreads
+ .calledWith(@threads)
+ .should.equal true
+
+ it "should return the messages", ->
+ @res.json.calledWith(@threads).should.equal true
+
+ describe "resolveThread", ->
+ beforeEach ->
+ @req.params =
+ project_id: @project_id = "mock-project-id"
+ thread_id: @thread_id = "mock-thread-id"
+ @ChatApiHandler.resolveThread = sinon.stub().yields()
+ @UserInfoManager.getPersonalInfo = sinon.stub().yields(null, @user = {"unformatted": "user"})
+ @UserInfoController.formatPersonalInfo = sinon.stub().returns(@formatted_user = {"formatted": "user"})
+ @CommentsController.resolveThread @req, @res
+
+ it "should ask the chat handler to resolve the thread", ->
+ @ChatApiHandler.resolveThread
+ .calledWith(@project_id, @thread_id)
+ .should.equal true
+
+ it "should look up the user", ->
+ @UserInfoManager.getPersonalInfo
+ .calledWith(@user_id)
+ .should.equal true
+
+ it "should tell the client the comment was resolved", ->
+ @EditorRealTimeController.emitToRoom
+ .calledWith(@project_id, "resolve-thread", @thread_id, @formatted_user)
+ .should.equal true
+
+ it "should return a success code", ->
+ @res.send.calledWith(204).should.equal
+
+ describe "reopenThread", ->
+ beforeEach ->
+ @req.params =
+ project_id: @project_id = "mock-project-id"
+ thread_id: @thread_id = "mock-thread-id"
+ @ChatApiHandler.reopenThread = sinon.stub().yields()
+ @CommentsController.reopenThread @req, @res
+
+ it "should ask the chat handler to reopen the thread", ->
+ @ChatApiHandler.reopenThread
+ .calledWith(@project_id, @thread_id)
+ .should.equal true
+
+ it "should tell the client the comment was resolved", ->
+ @EditorRealTimeController.emitToRoom
+ .calledWith(@project_id, "reopen-thread", @thread_id)
+ .should.equal true
+
+ it "should return a success code", ->
+ @res.send.calledWith(204).should.equal
+
+ describe "deleteThread", ->
+ beforeEach ->
+ @req.params =
+ project_id: @project_id = "mock-project-id"
+ doc_id: @doc_id = "mock-doc-id"
+ thread_id: @thread_id = "mock-thread-id"
+ @DocumentUpdaterHandler.deleteThread = sinon.stub().yields()
+ @ChatApiHandler.deleteThread = sinon.stub().yields()
+ @CommentsController.deleteThread @req, @res
+
+ it "should ask the doc udpater to delete the thread", ->
+ @DocumentUpdaterHandler.deleteThread
+ .calledWith(@project_id, @doc_id, @thread_id)
+ .should.equal true
+
+ it "should ask the chat handler to delete the thread", ->
+ @ChatApiHandler.deleteThread
+ .calledWith(@project_id, @thread_id)
+ .should.equal true
+
+ it "should tell the client the thread was deleted", ->
+ @EditorRealTimeController.emitToRoom
+ .calledWith(@project_id, "delete-thread", @thread_id)
+ .should.equal true
+
+ it "should return a success code", ->
+ @res.send.calledWith(204).should.equal
+
+ describe "editMessage", ->
+ beforeEach ->
+ @req.params =
+ project_id: @project_id = "mock-project-id"
+ thread_id: @thread_id = "mock-thread-id"
+ message_id: @message_id = "mock-thread-id"
+ @req.body =
+ content: @content = "mock-content"
+ @ChatApiHandler.editMessage = sinon.stub().yields()
+ @CommentsController.editMessage @req, @res
+
+ it "should ask the chat handler to edit the comment", ->
+ @ChatApiHandler.editMessage
+ .calledWith(@project_id, @thread_id, @message_id, @content)
+ .should.equal true
+
+ it "should tell the client the comment was edited", ->
+ @EditorRealTimeController.emitToRoom
+ .calledWith(@project_id, "edit-message", @thread_id, @message_id, @content)
+ .should.equal true
+
+ it "should return a success code", ->
+ @res.send.calledWith(204).should.equal
+
+ describe "deleteMessage", ->
+ beforeEach ->
+ @req.params =
+ project_id: @project_id = "mock-project-id"
+ thread_id: @thread_id = "mock-thread-id"
+ message_id: @message_id = "mock-thread-id"
+ @ChatApiHandler.deleteMessage = sinon.stub().yields()
+ @CommentsController.deleteMessage @req, @res
+
+ it "should ask the chat handler to deleted the message", ->
+ @ChatApiHandler.deleteMessage
+ .calledWith(@project_id, @thread_id, @message_id)
+ .should.equal true
+
+ it "should tell the client the message was deleted", ->
+ @EditorRealTimeController.emitToRoom
+ .calledWith(@project_id, "delete-message", @thread_id, @message_id)
+ .should.equal true
+
+ it "should return a success code", ->
+ @res.send.calledWith(204).should.equal
+
+ describe "_injectUserInfoIntoThreads", ->
+ beforeEach ->
+ @users = {
+ "user_id_1": {
+ "mock": "user_1"
+ }
+ "user_id_2": {
+ "mock": "user_2"
+ }
+ }
+ @UserInfoManager.getPersonalInfo = (user_id, callback) =>
+ return callback(null, @users[user_id])
+ sinon.spy @UserInfoManager, "getPersonalInfo"
+ @UserInfoController.formatPersonalInfo = (user) ->
+ return { "formatted": user["mock"] }
+
+ it "should inject a user object into messaged and resolved data", (done) ->
+ @CommentsController._injectUserInfoIntoThreads {
+ thread1: {
+ resolved: true
+ resolved_by_user_id: "user_id_1"
+ messages: [{
+ user_id: "user_id_1"
+ content: "foo"
+ }, {
+ user_id: "user_id_2"
+ content: "bar"
+ }]
+ },
+ thread2: {
+ messages: [{
+ user_id: "user_id_1"
+ content: "baz"
+ }]
+ }
+ }, (error, threads) ->
+ expect(threads).to.deep.equal {
+ thread1: {
+ resolved: true
+ resolved_by_user_id: "user_id_1"
+ resolved_by_user: { "formatted": "user_1" }
+ messages: [{
+ user_id: "user_id_1"
+ user: { "formatted": "user_1" }
+ content: "foo"
+ }, {
+ user_id: "user_id_2"
+ user: { "formatted": "user_2" }
+ content: "bar"
+ }]
+ },
+ thread2: {
+ messages: [{
+ user_id: "user_id_1"
+ user: { "formatted": "user_1" }
+ content: "baz"
+ }]
+ }
+ }
+ done()
+
+ it "should only need to look up each user once", (done) ->
+ @CommentsController._injectUserInfoIntoThreads [{
+ messages: [{
+ user_id: "user_id_1"
+ content: "foo"
+ }, {
+ user_id: "user_id_1"
+ content: "bar"
+ }]
+ }], (error, threads) =>
+ @UserInfoManager.getPersonalInfo.calledOnce.should.equal true
+ done()
\ No newline at end of file
diff --git a/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee b/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee
index e288c46aea..abcc55a0b9 100644
--- a/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee
@@ -57,12 +57,13 @@ describe "DocstoreManager", ->
@lines = ["mock", "doc", "lines"]
@rev = 5
@version = 42
+ @ranges = { "mock": "ranges" }
@modified = true
describe "with a successful response code", ->
beforeEach ->
@request.post = sinon.stub().callsArgWith(1, null, statusCode: 204, { modified: @modified, rev: @rev })
- @DocstoreManager.updateDoc @project_id, @doc_id, @lines, @version, @callback
+ @DocstoreManager.updateDoc @project_id, @doc_id, @lines, @version, @ranges, @callback
it "should update the doc in the docstore api", ->
@request.post
@@ -71,6 +72,7 @@ describe "DocstoreManager", ->
json:
lines: @lines
version: @version
+ ranges: @ranges
})
.should.equal true
@@ -80,7 +82,7 @@ describe "DocstoreManager", ->
describe "with a failed response code", ->
beforeEach ->
@request.post = sinon.stub().callsArgWith(1, null, statusCode: 500, "")
- @DocstoreManager.updateDoc @project_id, @doc_id, @lines, @version, @callback
+ @DocstoreManager.updateDoc @project_id, @doc_id, @lines, @version, @ranges, @callback
it "should call the callback with an error", ->
@callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true
@@ -100,6 +102,7 @@ describe "DocstoreManager", ->
lines: @lines = ["mock", "doc", "lines"]
rev: @rev = 5
version: @version = 42
+ ranges: @ranges = { "mock": "ranges" }
describe "with a successful response code", ->
beforeEach ->
@@ -115,7 +118,7 @@ describe "DocstoreManager", ->
.should.equal true
it "should call the callback with the lines, version and rev", ->
- @callback.calledWith(null, @lines, @rev, @version).should.equal true
+ @callback.calledWith(null, @lines, @rev, @version, @ranges).should.equal true
describe "with a failed response code", ->
beforeEach ->
@@ -148,7 +151,7 @@ describe "DocstoreManager", ->
.should.equal true
it "should call the callback with the lines, version and rev", ->
- @callback.calledWith(null, @lines, @rev, @version).should.equal true
+ @callback.calledWith(null, @lines, @rev, @version, @ranges).should.equal true
describe "getAllDocs", ->
describe "with a successful response code", ->
@@ -183,6 +186,38 @@ describe "DocstoreManager", ->
}, "error getting all docs from docstore")
.should.equal true
+ describe "getAllRanges", ->
+ describe "with a successful response code", ->
+ beforeEach ->
+ @request.get = sinon.stub().callsArgWith(1, null, statusCode: 204, @docs = [{ _id: "mock-doc-id", ranges: "mock-ranges" }])
+ @DocstoreManager.getAllRanges @project_id, @callback
+
+ it "should get all the project doc ranges in the docstore api", ->
+ @request.get
+ .calledWith({
+ url: "#{@settings.apis.docstore.url}/project/#{@project_id}/ranges"
+ json: true
+ })
+ .should.equal true
+
+ it "should call the callback with the docs", ->
+ @callback.calledWith(null, @docs).should.equal true
+
+ describe "with a failed response code", ->
+ beforeEach ->
+ @request.get = sinon.stub().callsArgWith(1, null, statusCode: 500, "")
+ @DocstoreManager.getAllRanges @project_id, @callback
+
+ it "should call the callback with an error", ->
+ @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true
+
+ it "should log the error", ->
+ @logger.error
+ .calledWith({
+ err: new Error("docstore api responded with a non-success code: 500")
+ project_id: @project_id
+ }, "error getting all doc ranges from docstore")
+ .should.equal true
describe "archiveProject", ->
describe "with a successful response code", ->
diff --git a/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee b/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
index aaae05219b..681915abc6 100644
--- a/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
@@ -8,8 +8,7 @@ path = require 'path'
_ = require 'underscore'
modulePath = path.join __dirname, '../../../../app/js/Features/DocumentUpdater/DocumentUpdaterHandler'
-describe 'DocumentUpdaterHandler - Flushing documents :', ->
-
+describe 'DocumentUpdaterHandler', ->
beforeEach ->
@project_id = "project-id-923"
@doc_id = "doc-id-394"
@@ -267,6 +266,7 @@ describe 'DocumentUpdaterHandler - Flushing documents :', ->
lines: @lines
version: @version
ops: @ops = ["mock-op-1", "mock-op-2"]
+ ranges: @ranges = {"mock":"ranges"}
@fromVersion = 2
@request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body)
@handler.getDocument @project_id, @doc_id, @fromVersion, @callback
@@ -276,7 +276,7 @@ describe 'DocumentUpdaterHandler - Flushing documents :', ->
@request.get.calledWith(url).should.equal true
it "should call the callback with the lines and version", ->
- @callback.calledWith(null, @lines, @version, @ops).should.equal true
+ @callback.calledWith(null, @lines, @version, @ranges, @ops).should.equal true
describe "when the document updater API returns an error", ->
beforeEach ->
@@ -295,3 +295,73 @@ describe 'DocumentUpdaterHandler - Flushing documents :', ->
@callback
.calledWith(new Error("doc updater returned failure status code: 500"))
.should.equal true
+
+ describe "acceptChange", ->
+ beforeEach ->
+ @change_id = "mock-change-id-1"
+ @callback = sinon.stub()
+
+ describe "successfully", ->
+ beforeEach ->
+ @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body)
+ @handler.acceptChange @project_id, @doc_id, @change_id, @callback
+
+ it 'should accept the change in the document updater', ->
+ url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}/change/#{@change_id}/accept"
+ @request.post.calledWith(url).should.equal true
+
+ it "should call the callback", ->
+ @callback.calledWith(null).should.equal true
+
+ describe "when the document updater API returns an error", ->
+ beforeEach ->
+ @request.post = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null)
+ @handler.acceptChange @project_id, @doc_id, @change_id, @callback
+
+ it "should return an error to the callback", ->
+ @callback.calledWith(@error).should.equal true
+
+ describe "when the document updater returns a failure error code", ->
+ beforeEach ->
+ @request.post = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "")
+ @handler.acceptChange @project_id, @doc_id, @change_id, @callback
+
+ it "should return the callback with an error", ->
+ @callback
+ .calledWith(new Error("doc updater returned failure status code: 500"))
+ .should.equal true
+
+ describe "deleteThread", ->
+ beforeEach ->
+ @thread_id = "mock-thread-id-1"
+ @callback = sinon.stub()
+
+ describe "successfully", ->
+ beforeEach ->
+ @request.del = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body)
+ @handler.deleteThread @project_id, @doc_id, @thread_id, @callback
+
+ it 'should delete the thread in the document updater', ->
+ url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}/comment/#{@thread_id}"
+ @request.del.calledWith(url).should.equal true
+
+ it "should call the callback", ->
+ @callback.calledWith(null).should.equal true
+
+ describe "when the document updater API returns an error", ->
+ beforeEach ->
+ @request.del = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null)
+ @handler.deleteThread @project_id, @doc_id, @thread_id, @callback
+
+ it "should return an error to the callback", ->
+ @callback.calledWith(@error).should.equal true
+
+ describe "when the document updater returns a failure error code", ->
+ beforeEach ->
+ @request.del = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "")
+ @handler.deleteThread @project_id, @doc_id, @thread_id, @callback
+
+ it "should return the callback with an error", ->
+ @callback
+ .calledWith(new Error("doc updater returned failure status code: 500"))
+ .should.equal true
\ No newline at end of file
diff --git a/services/web/test/UnitTests/coffee/DocumentUpdater/GetNumberOfDocsInMemoryTests.coffee b/services/web/test/UnitTests/coffee/DocumentUpdater/GetNumberOfDocsInMemoryTests.coffee
deleted file mode 100644
index 213fd2257b..0000000000
--- a/services/web/test/UnitTests/coffee/DocumentUpdater/GetNumberOfDocsInMemoryTests.coffee
+++ /dev/null
@@ -1,43 +0,0 @@
-path = require("path")
-sinon = require("sinon")
-SandboxedModule = require('sandboxed-module')
-
-modulePath = path.join __dirname, '../../../../app/js/Features/DocumentUpdater/DocumentUpdaterHandler'
-
-describe "getNumberOfDocsInMemory", ->
- beforeEach ->
- @host = "doc.updater"
- @noOfDocs = 42
- @callback = sinon.stub()
- @DocumentUpdateHandler = SandboxedModule.require modulePath, requires:
- "redis-sharelatex" :
- createClient: () ->
- auth:->
- "soa-req-id": null
- "logger-sharelatex": @logger =
- log: sinon.stub()
- error: sinon.stub()
- "../../infrastructure/Metrics" : @metrics
- "../../Features/Project/ProjectLocator": @ProjectLocator = {}
- "../../models/Project":Project:{}
- "request" : defaults: () => @request = {}
- "settings-sharelatex":
- apis: documentupdater: url: @host
- redis: web:{}
-
-
- @request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, JSON.stringify(total: @noOfDocs))
- @DocumentUpdateHandler.getNumberOfDocsInMemory @callback
-
- it "should call the doc updater", ->
- @request.get
- .calledWith("#{@host}/total")
- .should.equal true
-
- it "should return the number of docs", ->
- @callback
- .calledWith(null, @noOfDocs)
- .should.equal true
-
-
-
diff --git a/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee b/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee
index a554319baa..fedfa1c1b3 100644
--- a/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee
@@ -23,6 +23,7 @@ describe "DocumentController", ->
@doc_id = "doc-id-123"
@doc_lines = ["one", "two", "three"]
@version = 42
+ @ranges = {"mock": "ranges"}
@rev = 5
describe "getDocument", ->
@@ -33,7 +34,7 @@ describe "DocumentController", ->
describe "when the document exists", ->
beforeEach ->
- @ProjectEntityHandler.getDoc = sinon.stub().callsArgWith(2, null, @doc_lines, @rev, @version)
+ @ProjectEntityHandler.getDoc = sinon.stub().callsArgWith(2, null, @doc_lines, @rev, @version, @ranges)
@DocumentController.getDocument(@req, @res, @next)
it "should get the document from Mongo", ->
@@ -46,6 +47,7 @@ describe "DocumentController", ->
@res.body.should.equal JSON.stringify
lines: @doc_lines
version: @version
+ ranges: @ranges
describe "when the document doesn't exist", ->
beforeEach ->
@@ -68,11 +70,12 @@ describe "DocumentController", ->
@req.body =
lines: @doc_lines
version: @version
+ ranges: @ranges
@DocumentController.setDocument(@req, @res, @next)
it "should update the document in Mongo", ->
@ProjectEntityHandler.updateDocLines
- .calledWith(@project_id, @doc_id, @doc_lines, @version)
+ .calledWith(@project_id, @doc_id, @doc_lines, @version, @ranges)
.should.equal true
it "should return a successful response", ->
diff --git a/services/web/test/UnitTests/coffee/Email/EmailSenderTests.coffee b/services/web/test/UnitTests/coffee/Email/EmailSenderTests.coffee
index 4f08dd6790..beba91fcb3 100644
--- a/services/web/test/UnitTests/coffee/Email/EmailSenderTests.coffee
+++ b/services/web/test/UnitTests/coffee/Email/EmailSenderTests.coffee
@@ -10,6 +10,9 @@ describe "EmailSender", ->
beforeEach ->
+ @RateLimiter =
+ addCount:sinon.stub()
+
@settings =
email:
transport: "ses"
@@ -21,11 +24,15 @@ describe "EmailSender", ->
@sesClient =
sendMail: sinon.stub()
+
@ses =
createTransport: => @sesClient
+
+
@sender = SandboxedModule.require modulePath, requires:
'nodemailer': @ses
"settings-sharelatex":@settings
+ '../../infrastructure/RateLimiter':@RateLimiter
"logger-sharelatex":
log:->
warn:->
@@ -84,6 +91,29 @@ describe "EmailSender", ->
args.replyTo.should.equal @opts.replyTo
done()
+
+ it "should not send an email when the rate limiter says no", (done)->
+ @opts.sendingUser_id = "12321312321"
+ @RateLimiter.addCount.callsArgWith(1, null, false)
+ @sender.sendEmail @opts, =>
+ @sesClient.sendMail.called.should.equal false
+ done()
+
+ it "should send the email when the rate limtier says continue", (done)->
+ @sesClient.sendMail.callsArgWith(1)
+ @opts.sendingUser_id = "12321312321"
+ @RateLimiter.addCount.callsArgWith(1, null, true)
+ @sender.sendEmail @opts, =>
+ @sesClient.sendMail.called.should.equal true
+ done()
+
+ it "should not check the rate limiter when there is no sendingUser_id", (done)->
+ @sesClient.sendMail.callsArgWith(1)
+ @sender.sendEmail @opts, =>
+ @sesClient.sendMail.called.should.equal true
+ @RateLimiter.addCount.called.should.equal false
+ done()
+
describe 'with plain-text email content', () ->
beforeEach ->
diff --git a/services/web/test/UnitTests/coffee/TrackChanges/TrackChangesControllerTests.coffee b/services/web/test/UnitTests/coffee/History/HistoryControllerTests.coffee
similarity index 83%
rename from services/web/test/UnitTests/coffee/TrackChanges/TrackChangesControllerTests.coffee
rename to services/web/test/UnitTests/coffee/History/HistoryControllerTests.coffee
index bcc57b58b8..577aae6a9d 100644
--- a/services/web/test/UnitTests/coffee/TrackChanges/TrackChangesControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/History/HistoryControllerTests.coffee
@@ -1,21 +1,21 @@
chai = require('chai')
chai.should()
sinon = require("sinon")
-modulePath = "../../../../app/js/Features/TrackChanges/TrackChangesController"
+modulePath = "../../../../app/js/Features/History/HistoryController"
SandboxedModule = require('sandboxed-module')
-describe "TrackChangesController", ->
+describe "HistoryController", ->
beforeEach ->
@user_id = "user-id-123"
@AuthenticationController =
getLoggedInUserId: sinon.stub().returns(@user_id)
- @TrackChangesController = SandboxedModule.require modulePath, requires:
+ @HistoryController = SandboxedModule.require modulePath, requires:
"request" : @request = sinon.stub()
"settings-sharelatex": @settings = {}
"logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub()}
"../Authentication/AuthenticationController": @AuthenticationController
- describe "proxyToTrackChangesApi", ->
+ describe "proxyToHistoryApi", ->
beforeEach ->
@req = { url: "/mock/url", method: "POST" }
@res = "mock-res"
@@ -28,7 +28,7 @@ describe "TrackChangesController", ->
pipe: sinon.stub()
on: (event, handler) -> @events[event] = handler
@request.returns @proxy
- @TrackChangesController.proxyToTrackChangesApi @req, @res, @next
+ @HistoryController.proxyToHistoryApi @req, @res, @next
describe "successfully", ->
it "should get the user id", ->
diff --git a/services/web/test/UnitTests/coffee/TrackChanges/TrackChangesManagerTests.coffee b/services/web/test/UnitTests/coffee/History/HistoryManagerTests.coffee
similarity index 81%
rename from services/web/test/UnitTests/coffee/TrackChanges/TrackChangesManagerTests.coffee
rename to services/web/test/UnitTests/coffee/History/HistoryManagerTests.coffee
index 90b36f89c5..65b22812ea 100644
--- a/services/web/test/UnitTests/coffee/TrackChanges/TrackChangesManagerTests.coffee
+++ b/services/web/test/UnitTests/coffee/History/HistoryManagerTests.coffee
@@ -2,12 +2,12 @@ chai = require('chai')
expect = chai.expect
chai.should()
sinon = require("sinon")
-modulePath = "../../../../app/js/Features/TrackChanges/TrackChangesManager"
+modulePath = "../../../../app/js/Features/History/HistoryManager"
SandboxedModule = require('sandboxed-module')
-describe "TrackChangesManager", ->
+describe "HistoryManager", ->
beforeEach ->
- @TrackChangesManager = SandboxedModule.require modulePath, requires:
+ @HistoryManager = SandboxedModule.require modulePath, requires:
"request" : @request = sinon.stub()
"settings-sharelatex": @settings =
apis:
@@ -22,7 +22,7 @@ describe "TrackChangesManager", ->
describe "with a successful response code", ->
beforeEach ->
@request.post = sinon.stub().callsArgWith(1, null, statusCode: 204, "")
- @TrackChangesManager.flushProject @project_id, @callback
+ @HistoryManager.flushProject @project_id, @callback
it "should flush the project in the track changes api", ->
@request.post
@@ -35,7 +35,7 @@ describe "TrackChangesManager", ->
describe "with a failed response code", ->
beforeEach ->
@request.post = sinon.stub().callsArgWith(1, null, statusCode: 500, "")
- @TrackChangesManager.flushProject @project_id, @callback
+ @HistoryManager.flushProject @project_id, @callback
it "should call the callback with an error", ->
@callback.calledWith(new Error("track-changes api responded with a non-success code: 500")).should.equal true
@@ -52,12 +52,12 @@ describe "TrackChangesManager", ->
it "should call the post endpoint", (done)->
@request.post.callsArgWith(1, null, {})
- @TrackChangesManager.archiveProject @project_id, (err)=>
+ @HistoryManager.archiveProject @project_id, (err)=>
@request.post.calledWith("#{@settings.apis.trackchanges.url}/project/#{@project_id}/archive")
done()
it "should return an error on a non success", (done)->
@request.post.callsArgWith(1, null, {statusCode:500})
- @TrackChangesManager.archiveProject @project_id, (err)=>
+ @HistoryManager.archiveProject @project_id, (err)=>
expect(err).to.exist
done()
\ No newline at end of file
diff --git a/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee b/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee
index 89c6479734..d11507361c 100644
--- a/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee
@@ -145,18 +145,27 @@ describe "PasswordResetController", ->
done()
@PasswordResetController.setNewUserPassword @req, @res
- it "should login user if login_after is set", (done) ->
- @UserGetter.getUser = sinon.stub().callsArgWith(2, null, { email: "joe@example.com" })
- @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true, @user_id = "user-id-123")
- @req.body.login_after = "true"
- @AuthenticationController.doLogin = (options, req, res, next)=>
- @UserGetter.getUser.calledWith(@user_id).should.equal true
- expect(options).to.deep.equal {
- email: "joe@example.com",
- password: @password
- }
+ describe 'when login_after is set', ->
+
+ beforeEach ->
+ @UserGetter.getUser = sinon.stub().callsArgWith(2, null, { email: "joe@example.com" })
+ @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true, @user_id = "user-id-123")
+ @req.body.login_after = "true"
+ @res.json = sinon.stub()
+ @AuthenticationController.afterLoginSessionSetup = sinon.stub().callsArgWith(2, null)
+ @AuthenticationController._getRedirectFromSession = sinon.stub().returns('/some/path')
+
+ it "should login user if login_after is set", (done) ->
+ @PasswordResetController.setNewUserPassword @req, @res
+ @AuthenticationController.afterLoginSessionSetup.callCount.should.equal 1
+ @AuthenticationController.afterLoginSessionSetup.calledWith(
+ @req,
+ {email: 'joe@example.com'}
+ ).should.equal true
+ @AuthenticationController._getRedirectFromSession.callCount.should.equal 1
+ @res.json.callCount.should.equal 1
+ @res.json.calledWith({redir: '/some/path'}).should.equal true
done()
- @PasswordResetController.setNewUserPassword @req, @res
describe "renderSetPasswordForm", ->
diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee
index 5a0c860ab2..f3dcda07cf 100644
--- a/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee
@@ -382,7 +382,9 @@ describe 'ProjectEntityHandler', ->
beforeEach ->
@lines = ["mock", "doc", "lines"]
@rev = 5
- @DocstoreManager.getDoc = sinon.stub().callsArgWith(3, null, @lines, @rev)
+ @version = 42
+ @ranges = {"mock": "ranges"}
+ @DocstoreManager.getDoc = sinon.stub().callsArgWith(3, null, @lines, @rev, @version, @ranges)
@ProjectEntityHandler.getDoc project_id, doc_id, @callback
it "should call the docstore", ->
@@ -391,7 +393,7 @@ describe 'ProjectEntityHandler', ->
.should.equal true
it "should call the callback with the lines, version and rev", ->
- @callback.calledWith(null, @lines, @rev).should.equal true
+ @callback.calledWith(null, @lines, @rev, @version, @ranges).should.equal true
describe 'addDoc', ->
beforeEach ->
@@ -590,6 +592,7 @@ describe 'ProjectEntityHandler', ->
_id: doc_id
}
@version = 42
+ @ranges = {"mock":"ranges"}
@ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project)
@projectLocator.findElement = sinon.stub().callsArgWith(1, null, @doc, {fileSystem: @path})
@tpdsUpdateSender.addDoc = sinon.stub().callsArg(1)
@@ -599,7 +602,7 @@ describe 'ProjectEntityHandler', ->
describe "when the doc has been modified", ->
beforeEach ->
@DocstoreManager.updateDoc = sinon.stub().yields(null, true, @rev = 5)
- @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @callback
+ @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @ranges, @callback
it "should get the project without doc lines", ->
@ProjectGetter.getProjectWithoutDocLines
@@ -617,7 +620,7 @@ describe 'ProjectEntityHandler', ->
it "should update the doc in the docstore", ->
@DocstoreManager.updateDoc
- .calledWith(project_id, doc_id, @lines, @version)
+ .calledWith(project_id, doc_id, @lines, @version, @ranges)
.should.equal true
it "should mark the project as updated", ->
@@ -642,7 +645,7 @@ describe 'ProjectEntityHandler', ->
describe "when the doc has not been modified", ->
beforeEach ->
@DocstoreManager.updateDoc = sinon.stub().yields(null, false, @rev = 5)
- @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @callback
+ @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @ranges, @callback
it "should not mark the project as updated", ->
@projectUpdater.markAsUpdated.called.should.equal false
@@ -656,7 +659,7 @@ describe 'ProjectEntityHandler', ->
describe "when the project is not found", ->
beforeEach ->
@ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, null)
- @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @callback
+ @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @ranges, @version, @callback
it "should return a not found error", ->
@callback.calledWith(new Errors.NotFoundError()).should.equal true
@@ -664,7 +667,7 @@ describe 'ProjectEntityHandler', ->
describe "when the doc is not found", ->
beforeEach ->
@projectLocator.findElement = sinon.stub().callsArgWith(1, null, null, null)
- @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @callback
+ @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @ranges, @version, @callback
it "should log out the error", ->
@logger.error
diff --git a/services/web/test/UnitTests/coffee/Security/LoginRateLimiterTests.coffee b/services/web/test/UnitTests/coffee/Security/LoginRateLimiterTests.coffee
index 2c4dc59262..bbb4dcd675 100644
--- a/services/web/test/UnitTests/coffee/Security/LoginRateLimiterTests.coffee
+++ b/services/web/test/UnitTests/coffee/Security/LoginRateLimiterTests.coffee
@@ -1,78 +1,74 @@
SandboxedModule = require('sandboxed-module')
sinon = require('sinon')
require('chai').should()
+expect = require('chai').expect
modulePath = require('path').join __dirname, '../../../../app/js/Features/Security/LoginRateLimiter'
-buildKey = (k)->
- return "LoginRateLimit:#{k}"
describe "LoginRateLimiter", ->
+
beforeEach ->
@email = "bob@bob.com"
- @incrStub = sinon.stub()
- @getStub = sinon.stub()
- @execStub = sinon.stub()
- @expireStub = sinon.stub()
- @delStub = sinon.stub().callsArgWith(1)
-
- @rclient =
- auth:->
- del: @delStub
- multi: =>
- incr: @incrStub
- expire: @expireStub
- get: @getStub
- exec: @execStub
+ @RateLimiter =
+ clearRateLimit: sinon.stub()
+ addCount: sinon.stub()
@LoginRateLimiter = SandboxedModule.require modulePath, requires:
- 'redis-sharelatex' : createClient: () => @rclient
- "settings-sharelatex":{redis:{}}
-
+ '../../infrastructure/RateLimiter': @RateLimiter
+
describe "processLoginRequest", ->
- it "should inc the counter for login requests in redis", (done)->
- @execStub.callsArgWith(0, "null", ["",""])
- @LoginRateLimiter.processLoginRequest @email, =>
- @incrStub.calledWith(buildKey(@email)).should.equal true
+ beforeEach ->
+ @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
+
+ it 'should call RateLimiter.addCount', (done) ->
+ @LoginRateLimiter.processLoginRequest @email, (err, allow) =>
+ @RateLimiter.addCount.callCount.should.equal 1
+ expect(@RateLimiter.addCount.lastCall.args[0].endpointName).to.equal 'login'
+ expect(@RateLimiter.addCount.lastCall.args[0].subjectName).to.equal @email
done()
- it "should set a expire", (done)->
- @execStub.callsArgWith(0, "null", ["",""])
- @LoginRateLimiter.processLoginRequest @email, =>
- @expireStub.calledWith(buildKey(@email), 60 * 2).should.equal true
- done()
+ describe 'when login is allowed', ->
- it "should return true if the count is below 10", (done)->
- @execStub.callsArgWith(0, "null", ["", 9])
- @LoginRateLimiter.processLoginRequest @email, (err, isAllowed)=>
- isAllowed.should.equal true
- done()
+ beforeEach ->
+ @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
- it "should return true if the count is 10", (done)->
- @execStub.callsArgWith(0, "null", ["", 10])
- @LoginRateLimiter.processLoginRequest @email, (err, isAllowed)=>
- isAllowed.should.equal true
- done()
+ it 'should call pass allow=true', (done) ->
+ @LoginRateLimiter.processLoginRequest @email, (err, allow) =>
+ expect(err).to.equal null
+ expect(allow).to.equal true
+ done()
- it "should return false if the count is above 10", (done)->
- @execStub.callsArgWith(0, "null", ["", 11])
- @LoginRateLimiter.processLoginRequest @email, (err, isAllowed)=>
- isAllowed.should.equal false
- done()
+ describe 'when login is blocked', ->
+ beforeEach ->
+ @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false)
- describe "smoke test user", ->
-
- it "should have a higher limit", (done)->
- done()
+ it 'should call pass allow=false', (done) ->
+ @LoginRateLimiter.processLoginRequest @email, (err, allow) =>
+ expect(err).to.equal null
+ expect(allow).to.equal false
+ done()
+ describe 'when addCount produces an error', ->
+ beforeEach ->
+ @RateLimiter.addCount = sinon.stub().callsArgWith(1, new Error('woops'))
+ it 'should produce an error', (done) ->
+ @LoginRateLimiter.processLoginRequest @email, (err, allow) =>
+ expect(err).to.not.equal null
+ expect(err).to.be.instanceof Error
+ done()
describe "recordSuccessfulLogin", ->
- it "should delete the user key", (done)->
+ beforeEach ->
+ @RateLimiter.clearRateLimit = sinon.stub().callsArgWith 2, null
+
+ it "should call clearRateLimit", (done)->
@LoginRateLimiter.recordSuccessfulLogin @email, =>
- @delStub.calledWith(buildKey(@email)).should.equal true
- done()
\ No newline at end of file
+ @RateLimiter.clearRateLimit.callCount.should.equal 1
+ @RateLimiter.clearRateLimit.calledWith('login', @email).should.equal true
+ done()
diff --git a/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee
index 93f00afad5..f81433e156 100644
--- a/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee
@@ -6,17 +6,17 @@ Settings = require("settings-sharelatex")
describe "LimitationsManager", ->
beforeEach ->
- @project = { _id: "project-id" }
- @user = { _id: "user-id", features:{} }
+ @project = { _id: @project_id = "project-id" }
+ @user = { _id: @user_id = "user-id", features:{} }
@Project =
findById: (project_id, fields, callback) =>
if project_id == @project_id
callback null, @project
else
callback null, null
- @User =
- findById: (user_id, callback) =>
- if user_id == @user.id
+ @UserGetter =
+ getUser: (user_id, filter, callback) =>
+ if user_id == @user_id
callback null, @user
else
callback null, null
@@ -26,7 +26,7 @@ describe "LimitationsManager", ->
@LimitationsManager = SandboxedModule.require modulePath, requires:
'../../models/Project' : Project: @Project
- '../../models/User' : User: @User
+ '../User/UserGetter' : @UserGetter
'./SubscriptionLocator':@SubscriptionLocator
'settings-sharelatex' : @Settings = {}
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {}
@@ -37,6 +37,7 @@ describe "LimitationsManager", ->
describe "when the project is owned by a user without a subscription", ->
beforeEach ->
@Settings.defaultPlanCode = collaborators: 23
+ @project.owner_ref = @user_id
delete @user.features
@callback = sinon.stub()
@LimitationsManager.allowedNumberOfCollaboratorsInProject(@project_id, @callback)
@@ -46,6 +47,7 @@ describe "LimitationsManager", ->
describe "when the project is owned by a user with a subscription", ->
beforeEach ->
+ @project.owner_ref = @user_id
@user.features =
collaborators: 21
@callback = sinon.stub()
@@ -53,6 +55,27 @@ describe "LimitationsManager", ->
it "should return the number of collaborators the user is allowed", ->
@callback.calledWith(null, @user.features.collaborators).should.equal true
+
+ describe "allowedNumberOfCollaboratorsForUser", ->
+ describe "when the user has no features", ->
+ beforeEach ->
+ @Settings.defaultPlanCode = collaborators: 23
+ delete @user.features
+ @callback = sinon.stub()
+ @LimitationsManager.allowedNumberOfCollaboratorsForUser(@user_id, @callback)
+
+ it "should return the default number", ->
+ @callback.calledWith(null, @Settings.defaultPlanCode.collaborators).should.equal true
+
+ describe "when the user has features", ->
+ beforeEach ->
+ @user.features =
+ collaborators: 21
+ @callback = sinon.stub()
+ @LimitationsManager.allowedNumberOfCollaboratorsForUser(@user_id, @callback)
+
+ it "should return the number of collaborators the user is allowed", ->
+ @callback.calledWith(null, @user.features.collaborators).should.equal true
describe "canAddXCollaborators", ->
beforeEach ->
diff --git a/services/web/test/UnitTests/coffee/TrackChanges/RangesManagerTests.coffee b/services/web/test/UnitTests/coffee/TrackChanges/RangesManagerTests.coffee
new file mode 100644
index 0000000000..b9c95040c1
--- /dev/null
+++ b/services/web/test/UnitTests/coffee/TrackChanges/RangesManagerTests.coffee
@@ -0,0 +1,55 @@
+should = require('chai').should()
+SandboxedModule = require('sandboxed-module')
+assert = require('assert')
+sinon = require('sinon')
+path = require "path"
+modulePath = path.join __dirname, "../../../../app/js/Features/TrackChanges/RangesManager"
+expect = require("chai").expect
+
+describe "RangesManager", ->
+ beforeEach ->
+ @RangesManager = SandboxedModule.require modulePath, requires:
+ "../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler = {}
+ "../Docstore/DocstoreManager": @DocstoreManager = {}
+ "../User/UserInfoManager": @UserInfoManager = {}
+
+ describe "getAllChangesUsers", ->
+ beforeEach ->
+ @project_id = "mock-project-id"
+ @user_id1 = "mock-user-id-1"
+ @user_id1 = "mock-user-id-2"
+ @docs = [{
+ ranges:
+ changes: [{
+ op: { i: "foo", p: 42 }
+ metadata:
+ user_id: @user_id1
+ }, {
+ op: { i: "bar", p: 102 }
+ metadata:
+ user_id: @user_id2
+ }]
+ }, {
+ ranges:
+ changes: [{
+ op: { i: "baz", p: 3 }
+ metadata:
+ user_id: @user_id1
+ }]
+ }]
+ @users = {}
+ @users[@user_id1] = {"mock": "user-1"}
+ @users[@user_id2] = {"mock": "user-2"}
+ @UserInfoManager.getPersonalInfo = (user_id, callback) => callback null, @users[user_id]
+ sinon.spy @UserInfoManager, "getPersonalInfo"
+ @RangesManager.getAllRanges = sinon.stub().yields(null, @docs)
+
+ it "should return an array of unique users", (done) ->
+ @RangesManager.getAllChangesUsers @project_id, (error, users) =>
+ users.should.deep.equal [{"mock": "user-1"}, {"mock": "user-2"}]
+ done()
+
+ it "should only call getPersonalInfo once for each user", (done) ->
+ @RangesManager.getAllChangesUsers @project_id, (error, users) =>
+ @UserInfoManager.getPersonalInfo.calledTwice.should.equal true
+ done()
\ No newline at end of file
diff --git a/services/web/test/UnitTests/coffee/User/UserInfoControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserInfoControllerTests.coffee
index df895e630d..37a1c034f0 100644
--- a/services/web/test/UnitTests/coffee/User/UserInfoControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/User/UserInfoControllerTests.coffee
@@ -93,18 +93,18 @@ describe "UserInfoController", ->
first_name: @user.first_name
last_name: @user.last_name
email: @user.email
- @UserInfoController._formatPersonalInfo = sinon.stub().callsArgWith(1, null, @formattedInfo)
+ @UserInfoController.formatPersonalInfo = sinon.stub().returns(@formattedInfo)
@UserInfoController.sendFormattedPersonalInfo @user, @res
it "should format the user details for the response", ->
- @UserInfoController._formatPersonalInfo
+ @UserInfoController.formatPersonalInfo
.calledWith(@user)
.should.equal true
it "should send the formatted details back to the client", ->
@res.body.should.equal JSON.stringify(@formattedInfo)
- describe "_formatPersonalInfo", ->
+ describe "formatPersonalInfo", ->
it "should return the correctly formatted data", ->
@user =
_id: ObjectId()
@@ -115,14 +115,13 @@ describe "UserInfoController", ->
signUpDate: new Date()
role:"student"
institution:"sheffield"
- @UserInfoController._formatPersonalInfo @user, (error, info) =>
- expect(info).to.deep.equal {
- id: @user._id.toString()
- first_name: @user.first_name
- last_name: @user.last_name
- email: @user.email
- signUpDate: @user.signUpDate
- role: @user.role
- institution: @user.institution
- }
+ expect(@UserInfoController.formatPersonalInfo(@user)).to.deep.equal {
+ id: @user._id.toString()
+ first_name: @user.first_name
+ last_name: @user.last_name
+ email: @user.email
+ signUpDate: @user.signUpDate
+ role: @user.role
+ institution: @user.institution
+ }
diff --git a/services/web/test/UnitTests/coffee/infrastructure/RateLimterTests.coffee b/services/web/test/UnitTests/coffee/infrastructure/RateLimterTests.coffee
index 21c0d96f4f..06efac5d8b 100644
--- a/services/web/test/UnitTests/coffee/infrastructure/RateLimterTests.coffee
+++ b/services/web/test/UnitTests/coffee/infrastructure/RateLimterTests.coffee
@@ -6,7 +6,7 @@ expect = chai.expect
modulePath = "../../../../app/js/infrastructure/RateLimiter.js"
SandboxedModule = require('sandboxed-module')
-describe "FileStoreHandler", ->
+describe "RateLimiter", ->
beforeEach ->
@settings =
@@ -15,23 +15,27 @@ describe "FileStoreHandler", ->
port:"1234"
host:"somewhere"
password: "password"
- @redbackInstance =
- addCount: sinon.stub()
+ @rclient =
+ incr: sinon.stub()
+ get: sinon.stub()
+ expire: sinon.stub()
+ exec: sinon.stub()
+ @rclient.multi = sinon.stub().returns(@rclient)
+ @RedisWrapper =
+ client: sinon.stub().returns(@rclient)
- @redback =
- createRateLimit: sinon.stub().returns(@redbackInstance)
- @redis =
- createClient: ->
- return auth:->
+ @limiterFn = sinon.stub()
+ @RollingRateLimiter = (opts) =>
+ return @limiterFn
@limiter = SandboxedModule.require modulePath, requires:
+ "rolling-rate-limiter": @RollingRateLimiter
"settings-sharelatex":@settings
"logger-sharelatex" : @logger = {log:sinon.stub(), err:sinon.stub()}
- "redis-sharelatex": @redis
- "redback": use: => @redback
+ "./RedisWrapper": @RedisWrapper
@endpointName = "compiles"
- @subject = "some project id"
+ @subject = "some-project-id"
@timeInterval = 20
@throttleLimit = 5
@@ -40,43 +44,48 @@ describe "FileStoreHandler", ->
subjectName: @subject
throttle: @throttleLimit
timeInterval: @timeInterval
+ @key = "RateLimiter:#{@endpointName}:{#{@subject}}"
- describe "addCount", ->
+
+
+ describe 'when action is permitted', ->
beforeEach ->
- @redbackInstance.addCount.callsArgWith(2, null, 10)
+ @limiterFn = sinon.stub().callsArgWith(1, null, 0, 22)
- it "should use correct namespace", (done)->
- @limiter.addCount @details, =>
- @redback.createRateLimit.calledWith(@endpointName).should.equal true
+ it 'should not produce and error', (done) ->
+ @limiter.addCount {}, (err, should) ->
+ expect(err).to.equal null
done()
- it "should only call it once", (done)->
- @limiter.addCount @details, =>
- @redbackInstance.addCount.callCount.should.equal 1
+ it 'should callback with true', (done) ->
+ @limiter.addCount {}, (err, should) ->
+ expect(should).to.equal true
done()
- it "should use the subjectName", (done)->
- @limiter.addCount @details, =>
- @redbackInstance.addCount.calledWith(@details.subjectName, @details.timeInterval).should.equal true
+ describe 'when action is not permitted', ->
+
+ beforeEach ->
+ @limiterFn = sinon.stub().callsArgWith(1, null, 4000, 0)
+
+ it 'should not produce and error', (done) ->
+ @limiter.addCount {}, (err, should) ->
+ expect(err).to.equal null
done()
- it "should return true if the count is less than throttle", (done)->
- @details.throttle = 100
- @limiter.addCount @details, (err, canProcess)=>
- canProcess.should.equal true
+ it 'should callback with false', (done) ->
+ @limiter.addCount {}, (err, should) ->
+ expect(should).to.equal false
done()
- it "should return true if the count is less than throttle", (done)->
- @details.throttle = 1
- @limiter.addCount @details, (err, canProcess)=>
- canProcess.should.equal false
- done()
+ describe 'when limiter produces an error', ->
- it "should return false if the limit is matched", (done)->
- @details.throttle = 10
- @limiter.addCount @details, (err, canProcess)=>
- canProcess.should.equal false
- done()
+ beforeEach ->
+ @limiterFn = sinon.stub().callsArgWith(1, new Error('woops'))
+ it 'should produce and error', (done) ->
+ @limiter.addCount {}, (err, should) ->
+ expect(err).to.not.equal null
+ expect(err).to.be.instanceof Error
+ done()
diff --git a/services/web/test/UnitTests/coffee/infrastructure/RedisWrapperTests.coffee b/services/web/test/UnitTests/coffee/infrastructure/RedisWrapperTests.coffee
new file mode 100644
index 0000000000..83ea202dcd
--- /dev/null
+++ b/services/web/test/UnitTests/coffee/infrastructure/RedisWrapperTests.coffee
@@ -0,0 +1,66 @@
+assert = require("chai").assert
+sinon = require('sinon')
+chai = require('chai')
+should = chai.should()
+expect = chai.expect
+modulePath = "../../../../app/js/infrastructure/RedisWrapper.js"
+SandboxedModule = require('sandboxed-module')
+
+describe 'RedisWrapper', ->
+
+ beforeEach ->
+ @featureName = 'somefeature'
+ @settings =
+ redis:
+ web:
+ port:"1234"
+ host:"somewhere"
+ password: "password"
+ somefeature: {}
+ @normalRedisInstance =
+ thisIsANormalRedisInstance: true
+ n: 1
+ @clusterRedisInstance =
+ thisIsAClusterRedisInstance: true
+ n: 2
+ @redis =
+ createClient: sinon.stub().returns(@normalRedisInstance)
+ @ioredis =
+ Cluster: sinon.stub().returns(@clusterRedisInstance)
+ @logger = {log: sinon.stub()}
+
+ @RedisWrapper = SandboxedModule.require modulePath, requires:
+ 'logger-sharelatex': @logger
+ 'settings-sharelatex': @settings
+ 'redis-sharelatex': @redis
+ 'ioredis': @ioredis
+
+ describe 'client', ->
+
+ beforeEach ->
+ @call = () =>
+ @RedisWrapper.client(@featureName)
+
+ describe 'when feature uses cluster', ->
+
+ beforeEach ->
+ @settings.redis.somefeature =
+ cluster: [1, 2, 3]
+
+ it 'should return a cluster client', ->
+ client = @call()
+ expect(client).to.equal @clusterRedisInstance
+ expect(client.__is_redis_cluster).to.equal true
+
+ describe 'when feature uses normal redis', ->
+
+ beforeEach ->
+ @settings.redis.somefeature =
+ port:"1234"
+ host:"somewhere"
+ password: "password"
+
+ it 'should return a regular redis client', ->
+ client = @call()
+ expect(client).to.equal @normalRedisInstance
+ expect(client.__is_redis_cluster).to.equal undefined
diff --git a/services/web/test/acceptance/coffee/RegistrationTests.coffee b/services/web/test/acceptance/coffee/RegistrationTests.coffee
index 2bf96f86de..20ea0a31b1 100644
--- a/services/web/test/acceptance/coffee/RegistrationTests.coffee
+++ b/services/web/test/acceptance/coffee/RegistrationTests.coffee
@@ -1,9 +1,11 @@
expect = require("chai").expect
+assert = require("chai").assert
async = require("async")
User = require "./helpers/User"
request = require "./helpers/request"
settings = require "settings-sharelatex"
redis = require "./helpers/redis"
+_ = require 'lodash'
@@ -32,6 +34,41 @@ tryLoginThroughRegistrationForm = (user, email, password, callback=(err, respons
}, callback
+describe "LoginRateLimit", ->
+
+ before ->
+ @user = new User()
+ @badEmail = 'bademail@example.com'
+ @badPassword = 'badpassword'
+
+ it 'should rate limit login attempts after 10 within two minutes', (done) ->
+ @user.request.get '/login', (err, res, body) =>
+ async.timesSeries(
+ 15
+ , (n, cb) =>
+ @user.getCsrfToken (error) =>
+ return cb(error) if error?
+ @user.request.post {
+ url: "/login"
+ json:
+ email: @badEmail
+ password: @badPassword
+ }, (err, response, body) =>
+ cb(null, body?.message?.text)
+ , (err, results) =>
+ # ten incorrect-credentials messages, then five rate-limit messages
+ expect(results.length).to.equal 15
+ assert.deepEqual(
+ results,
+ _.concat(
+ _.fill([1..10], 'Your email or password is incorrect. Please try again'),
+ _.fill([1..5], 'This account has had too many login requests. Please wait 2 minutes before trying to log in again')
+ )
+ )
+ done()
+ )
+
+
describe "LoginViaRegistration", ->
before (done) ->