From e383e491619b3478423776520b23fd4d14105d19 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 20 Jul 2016 14:04:14 +0100
Subject: [PATCH 001/378] Add CollaboratorsInviteController and routes
---
.../CollaboratorsInviteController.coffee | 11 +++++++++
.../CollaboratorsInviteHandler.coffee | 9 +++++++
.../Collaborators/CollaboratorsRouter.coffee | 24 +++++++++++++++++++
3 files changed, 44 insertions(+)
create mode 100644 services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
create mode 100644 services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
new file mode 100644
index 0000000000..e33b73ff90
--- /dev/null
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -0,0 +1,11 @@
+CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
+
+module.exports = CollaboratorsInviteController =
+
+ inviteToProject: (req, res) ->
+
+ revokeInvite: (req, res) ->
+
+ viewInvite: (req, res) ->
+
+ acceptInvite: (req, res) ->
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
new file mode 100644
index 0000000000..3b4c9617c6
--- /dev/null
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -0,0 +1,9 @@
+module.experts = CollaboratorsInviteHandler =
+
+ inviteToProject: (callback) ->
+
+ revokeInvite: (callback) ->
+
+ viewInvite: (callback) ->
+
+ acceptInvite: (callback) ->
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
index 34a6da9a02..c2357994a3 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
@@ -1,6 +1,7 @@
CollaboratorsController = require('./CollaboratorsController')
AuthenticationController = require('../Authentication/AuthenticationController')
AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear')
+CollaboratorsInviteController = require('./CollaboratorsInviteController')
module.exports =
apply: (webRouter, apiRouter) ->
@@ -8,3 +9,26 @@ module.exports =
webRouter.post '/project/:Project_id/users', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.addUserToProject
webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject
+
+ # invites
+ webRouter.post(
+ '/project/:Project_id/invite',
+ AuthorizationMiddlewear.ensureUserCanAdminProject,
+ CollaboratorsInviteController.inviteToProject
+ )
+
+ webRouter.delete(
+ '/project/:Project_id/invite/:invite_id',
+ AuthorizationMiddlewear.ensureUserCanAdminProject,
+ CollaboratorsInviteController.revokeInvite
+ )
+
+ webRouter.get(
+ '/project/:Project_id/invite/token/:token_id',
+ CollaboratorsInviteController.viewInvite
+ )
+
+ webRouter.post(
+ '/project/:Project_id/invite/:invite_id/accept',
+ CollaboratorsInviteController.acceptInvite
+ )
From a4c7db5f20518b62328ad2485c13acc795c18cc4 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 20 Jul 2016 14:14:56 +0100
Subject: [PATCH 002/378] skeleton of `inviteToProject` function
---
.../CollaboratorsInviteController.coffee | 33 ++++++++++++++++---
1 file changed, 29 insertions(+), 4 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index e33b73ff90..675399c898 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -1,11 +1,36 @@
+ProjectGetter = require "../Project/ProjectGetter"
+LimitationsManager = require "../Subscription/LimitationsManager"
+UserGetter = require "../User/UserGetter"
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
+mimelib = require("mimelib")
+logger = require('logger-sharelatex')
module.exports = CollaboratorsInviteController =
- inviteToProject: (req, res) ->
+ inviteToProject: (req, res, next) ->
+ projectId = req.params.Project_id
+ email = req.body.email
+ sendingUserId = req.session?.user_id
+ logger.log {projectId, email, sendingUserId}, "inviting to project"
+ LimitationsManager.canAddXCollaborators project_id, 1, (error, allowed) =>
+ return next(error) if error?
+ if !allowed
+ logger.log {projectId, email, sendingUserId}, "not allowed to invite any more users to this project"
+ return res.json {}
+ {email, privileges} = req.body
+ email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
+ if !email? or email == ""
+ logger.log {projectId, email, sendingUserId}, "invalid email address"
+ return res.status(400).send("invalid email address")
+ CollaboratorsInviteHandler.inviteToProject projectId, sendingUserId, email, priveleges, (err, invite) ->
+ if err?
+ logger.err {projectId, email, sendingUserId}, "error creating project invite"
+ return next(err)
+ logger.log {projectId, email, sendingUserId}, "invite created"
+ return res.json {inviteId: invite._id}
- revokeInvite: (req, res) ->
+ revokeInvite: (req, res, next) ->
- viewInvite: (req, res) ->
+ viewInvite: (req, res, next) ->
- acceptInvite: (req, res) ->
+ acceptInvite: (req, res, next) ->
From 5b22be8a0b5ab1eccfe80bdf4c80a503e3b2e3bf Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 20 Jul 2016 15:22:48 +0100
Subject: [PATCH 003/378] Further scaffolding
---
.../CollaboratorsInviteController.coffee | 13 ++++++++++++-
.../Collaborators/CollaboratorsInviteHandler.coffee | 8 ++++----
.../Collaborators/CollaboratorsRouter.coffee | 2 +-
3 files changed, 17 insertions(+), 6 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 675399c898..985adb6704 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -15,7 +15,7 @@ module.exports = CollaboratorsInviteController =
LimitationsManager.canAddXCollaborators project_id, 1, (error, allowed) =>
return next(error) if error?
if !allowed
- logger.log {projectId, email, sendingUserId}, "not allowed to invite any more users to this project"
+ logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
return res.json {}
{email, privileges} = req.body
email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
@@ -30,7 +30,18 @@ module.exports = CollaboratorsInviteController =
return res.json {inviteId: invite._id}
revokeInvite: (req, res, next) ->
+ projectId = req.params.Project_id
+ inviteId = req.params.invite_id
+ logger.log {projectId, inviteId}, "revoking invite"
+ CollaboratorsInviteHandler.revokeInvite projectId, inviteId, (err) ->
+ if err?
+ logger.err {projectId, inviteId}, "error revoking invite"
+ return next(err)
+ res.status(201).send()
viewInvite: (req, res, next) ->
+ projectId = req.params.Project_id
+ token = req.params.token
+
acceptInvite: (req, res, next) ->
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 3b4c9617c6..f3d09effd4 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -1,9 +1,9 @@
module.experts = CollaboratorsInviteHandler =
- inviteToProject: (callback) ->
+ inviteToProject: (projectId, sendingUserId, email, priveleges, callback=(err,invite)->) ->
- revokeInvite: (callback) ->
+ revokeInvite: (projectId, inviteId, callback=(err)->) ->
- viewInvite: (callback) ->
+ getInviteByToken: (projectId, tokenString, callback=(err,invite)->) ->
- acceptInvite: (callback) ->
+ acceptInvite: (projectId, inviteId, callback=(err)->) ->
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
index c2357994a3..c687a1e487 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
@@ -24,7 +24,7 @@ module.exports =
)
webRouter.get(
- '/project/:Project_id/invite/token/:token_id',
+ '/project/:Project_id/invite/token/:token',
CollaboratorsInviteController.viewInvite
)
From f7c2fa37abb6c29935b3cf82c22733d75f447f5c Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 20 Jul 2016 16:44:22 +0100
Subject: [PATCH 004/378] Fill out `getInviteByToken`
---
.../Collaborators/CollaboratorsInviteController.coffee | 8 ++++++++
services/web/app/views/project/invite.jade | 1 +
2 files changed, 9 insertions(+)
create mode 100644 services/web/app/views/project/invite.jade
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 985adb6704..95f8e35e97 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -42,6 +42,14 @@ module.exports = CollaboratorsInviteController =
viewInvite: (req, res, next) ->
projectId = req.params.Project_id
token = req.params.token
+ CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) ->
+ if err?
+ logger.err {projectId, token}, "error getting invite by token"
+ return next(err)
+ if !invite
+ logger.log {projectId, token}, "no invite found for token"
+ return res.redirect("/")
+ res.render "project/invite", {invite}
acceptInvite: (req, res, next) ->
diff --git a/services/web/app/views/project/invite.jade b/services/web/app/views/project/invite.jade
new file mode 100644
index 0000000000..705cea4fe8
--- /dev/null
+++ b/services/web/app/views/project/invite.jade
@@ -0,0 +1 @@
+h1 Invite TEST
\ No newline at end of file
From 0f2600b1987ef6a946d74915347f1b74d8962329 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 21 Jul 2016 09:32:14 +0100
Subject: [PATCH 005/378] finish out skeleton of invite controller
---
.../CollaboratorsInviteController.coffee | 11 ++++++++++-
.../Features/Collaborators/CollaboratorsRouter.coffee | 2 ++
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 95f8e35e97..beac6f2b45 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -42,6 +42,7 @@ module.exports = CollaboratorsInviteController =
viewInvite: (req, res, next) ->
projectId = req.params.Project_id
token = req.params.token
+ currentUser = req.session.user
CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) ->
if err?
logger.err {projectId, token}, "error getting invite by token"
@@ -51,5 +52,13 @@ module.exports = CollaboratorsInviteController =
return res.redirect("/")
res.render "project/invite", {invite}
-
acceptInvite: (req, res, next) ->
+ projectId = req.params.Project_id
+ inviteId = req.params.inviteId
+ currentUser = req.session.user
+ logger.log {projectId, inviteId}, "accepting invite"
+ CollaboratorsInviteHandler.acceptInvite projectId, inviteId, currentUser, (err) ->
+ if err?
+ logger.err {projectId, token}, "error getting invite by token"
+ return next(err)
+ rest.status(201).send()
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
index c687a1e487..9412f794ba 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
@@ -25,10 +25,12 @@ module.exports =
webRouter.get(
'/project/:Project_id/invite/token/:token',
+ AuthenticationController.requireLogin(),
CollaboratorsInviteController.viewInvite
)
webRouter.post(
'/project/:Project_id/invite/:invite_id/accept',
+ AuthenticationController.requireLogin(),
CollaboratorsInviteController.acceptInvite
)
From 049cced4fd7ab91f045eff1a55cf71aa1be41b64 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 21 Jul 2016 09:42:37 +0100
Subject: [PATCH 006/378] copy helper functions from CollaboratorsHandler
---
.../CollaboratorsInviteHandler.coffee | 80 +++++++++++++++++++
1 file changed, 80 insertions(+)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index f3d09effd4..7187f0937f 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -1,5 +1,85 @@
+UserCreator = require('../User/UserCreator')
+Project = require("../../models/Project").Project
+mimelib = require("mimelib")
+logger = require('logger-sharelatex')
+UserGetter = require "../User/UserGetter"
+ContactManager = require "../Contacts/ContactManager"
+CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
+Async = require "async"
+PrivilegeLevels = require "../Authorization/PrivilegeLevels"
+Errors = require "../Errors/Errors"
+
module.experts = CollaboratorsInviteHandler =
+ # helpers
+ getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
+ Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) ->
+ return callback(error) if error?
+ return callback new Errors.NotFoundError("no project found with id #{project_id}") if !project?
+ members = []
+ members.push { id: project.owner_ref.toString(), privilegeLevel: PrivilegeLevels.OWNER }
+ for member_id in project.readOnly_refs or []
+ members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_ONLY }
+ for member_id in project.collaberator_refs or []
+ members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_AND_WRITE }
+ return callback null, members
+
+ getMemberIds: (project_id, callback = (error, member_ids) ->) ->
+ CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
+ return callback(error) if error?
+ return callback null, members.map (m) -> m.id
+
+ getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
+ CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
+ return callback(error) if error?
+ result = []
+ async.mapLimit members, 3,
+ (member, cb) ->
+ UserGetter.getUser member.id, (error, user) ->
+ return cb(error) if error?
+ if user?
+ result.push { user: user, privilegeLevel: member.privilegeLevel }
+ cb()
+ (error) ->
+ return callback(error) if error?
+ callback null, result
+
+ getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) ->
+ # In future if the schema changes and getting all member ids is more expensive (multiple documents)
+ # then optimise this.
+ CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
+ return callback(error) if error?
+ for member in members
+ if member.id == user_id?.toString()
+ return callback null, member.privilegeLevel
+ return callback null, PrivilegeLevels.NONE
+
+ getMemberCount: (project_id, callback = (error, count) ->) ->
+ CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
+ return callback(error) if error?
+ return callback null, (members or []).length
+
+ getCollaboratorCount: (project_id, callback = (error, count) ->) ->
+ CollaboratorsInviteHandler.getMemberCount project_id, (error, count) ->
+ return callback(error) if error?
+ return callback null, count - 1 # Don't count project owner
+
+ isUserMemberOfProject: (user_id, project_id, callback = (error, isMember, privilegeLevel) ->) ->
+ CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
+ return callback(error) if error?
+ for member in members
+ if member.id.toString() == user_id.toString()
+ return callback null, true, member.privilegeLevel
+ return callback null, false, null
+
+ getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) ->
+ Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=>
+ return callback(err) if err?
+ Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=>
+ return callback(err) if err?
+ callback(null, readAndWriteProjects, readOnlyProjects)
+
+ # public functions
inviteToProject: (projectId, sendingUserId, email, priveleges, callback=(err,invite)->) ->
revokeInvite: (projectId, inviteId, callback=(err)->) ->
From c3e51dd7734cee220ada4cb3c17882abb0ed1a54 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 21 Jul 2016 09:50:52 +0100
Subject: [PATCH 007/378] Revert "copy helper functions from
CollaboratorsHandler"
This reverts commit 0d5acd7bade584e4ff119dc22e5d5d3b3175dae2.
---
.../CollaboratorsInviteHandler.coffee | 80 -------------------
1 file changed, 80 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 7187f0937f..f3d09effd4 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -1,85 +1,5 @@
-UserCreator = require('../User/UserCreator')
-Project = require("../../models/Project").Project
-mimelib = require("mimelib")
-logger = require('logger-sharelatex')
-UserGetter = require "../User/UserGetter"
-ContactManager = require "../Contacts/ContactManager"
-CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
-Async = require "async"
-PrivilegeLevels = require "../Authorization/PrivilegeLevels"
-Errors = require "../Errors/Errors"
-
module.experts = CollaboratorsInviteHandler =
- # helpers
- getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
- Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) ->
- return callback(error) if error?
- return callback new Errors.NotFoundError("no project found with id #{project_id}") if !project?
- members = []
- members.push { id: project.owner_ref.toString(), privilegeLevel: PrivilegeLevels.OWNER }
- for member_id in project.readOnly_refs or []
- members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_ONLY }
- for member_id in project.collaberator_refs or []
- members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_AND_WRITE }
- return callback null, members
-
- getMemberIds: (project_id, callback = (error, member_ids) ->) ->
- CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
- return callback(error) if error?
- return callback null, members.map (m) -> m.id
-
- getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
- CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
- return callback(error) if error?
- result = []
- async.mapLimit members, 3,
- (member, cb) ->
- UserGetter.getUser member.id, (error, user) ->
- return cb(error) if error?
- if user?
- result.push { user: user, privilegeLevel: member.privilegeLevel }
- cb()
- (error) ->
- return callback(error) if error?
- callback null, result
-
- getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) ->
- # In future if the schema changes and getting all member ids is more expensive (multiple documents)
- # then optimise this.
- CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
- return callback(error) if error?
- for member in members
- if member.id == user_id?.toString()
- return callback null, member.privilegeLevel
- return callback null, PrivilegeLevels.NONE
-
- getMemberCount: (project_id, callback = (error, count) ->) ->
- CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
- return callback(error) if error?
- return callback null, (members or []).length
-
- getCollaboratorCount: (project_id, callback = (error, count) ->) ->
- CollaboratorsInviteHandler.getMemberCount project_id, (error, count) ->
- return callback(error) if error?
- return callback null, count - 1 # Don't count project owner
-
- isUserMemberOfProject: (user_id, project_id, callback = (error, isMember, privilegeLevel) ->) ->
- CollaboratorsInviteHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
- return callback(error) if error?
- for member in members
- if member.id.toString() == user_id.toString()
- return callback null, true, member.privilegeLevel
- return callback null, false, null
-
- getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) ->
- Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=>
- return callback(err) if err?
- Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=>
- return callback(err) if err?
- callback(null, readAndWriteProjects, readOnlyProjects)
-
- # public functions
inviteToProject: (projectId, sendingUserId, email, priveleges, callback=(err,invite)->) ->
revokeInvite: (projectId, inviteId, callback=(err)->) ->
From 23a9aadba5780faf2e7de67955bb355974272caa Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 21 Jul 2016 10:08:22 +0100
Subject: [PATCH 008/378] start tests for invite controller
---
.../CollaboratorsInviteHandler.coffee | 11 ++++
.../CollaboratorsInviteControllerTests.coffee | 57 +++++++++++++++++++
2 files changed, 68 insertions(+)
create mode 100644 services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index f3d09effd4..f22bcb4c4f 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -1,3 +1,14 @@
+UserCreator = require('../User/UserCreator')
+Project = require("../../models/Project").Project
+mimelib = require("mimelib")
+logger = require('logger-sharelatex')
+UserGetter = require "../User/UserGetter"
+ContactManager = require "../Contacts/ContactManager"
+CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
+Async = require "async"
+PrivilegeLevels = require "../Authorization/PrivilegeLevels"
+Errors = require "../Errors/Errors"
+
module.experts = CollaboratorsInviteHandler =
inviteToProject: (projectId, sendingUserId, email, priveleges, callback=(err,invite)->) ->
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
new file mode 100644
index 0000000000..fb9e77f952
--- /dev/null
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -0,0 +1,57 @@
+sinon = require('sinon')
+chai = require('chai')
+should = chai.should()
+expect = chai.expect
+modulePath = "../../../../app/js/Features/Collaborators/CollaboratorsInviteController.js"
+SandboxedModule = require('sandboxed-module')
+events = require "events"
+MockRequest = require "../helpers/MockRequest"
+MockResponse = require "../helpers/MockResponse"
+ObjectId = require("mongojs").ObjectId
+
+describe "CollaboratorsInviteController", ->
+ beforeEach ->
+ @CollaboratorsInviteController = SandboxedModule.require modulePath, requires:
+ "../Project/ProjectGetter": @ProjectGetter = {}
+ "./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
+ "../Editor/EditorRealTimeController": @EditorRealTimeController = {}
+ '../Subscription/LimitationsManager' : @LimitationsManager = {}
+ '../Project/ProjectEditorHandler' : @ProjectEditorHandler = {}
+ '../User/UserGetter': @UserGetter = {}
+ @res = new MockResponse()
+ @req = new MockRequest()
+
+ @project_id = "project-id-123"
+ @callback = sinon.stub()
+
+ describe "viewInvite", ->
+
+ beforeEach ->
+ @req.params =
+ Project_id: @project_id
+ token: "some-opaque-token"
+ @req.session =
+ user: _id: @current_user_id = "current-user-id"
+ @res.render = sinon.stub()
+ @invite = {
+ _id: ObjectId(),
+ token: "htnseuthaouse",
+ sendingUserId: ObjectId(),
+ projectId: @projectId,
+ targetEmail: 'user@example.com'
+ createdAt: new Date(),
+ expiresAt: new Date()
+ }
+ @CollaboratorsInviteHandler.getInviteByToken = sinon.stub().callsArgWith(2, null, @invite)
+ @callback = sinon.stub()
+ @next = sinon.stub()
+
+ describe 'when the token is valid', ->
+
+ beforeEach ->
+ @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, @invite)
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
+
+ it 'should render the view template', ->
+ @res.render.callCount.should.equal 1
+ @res.render.firstCall.args[0].should.equal 'project/invite'
From 3311b43644b7c429867617ba0ddfbfd1002ad186 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 21 Jul 2016 13:31:54 +0100
Subject: [PATCH 009/378] more tests for invite controller
---
.../CollaboratorsInviteController.coffee | 18 +-
.../CollaboratorsInviteHandler.coffee | 2 +-
.../CollaboratorsInviteControllerTests.coffee | 222 +++++++++++++++++-
3 files changed, 231 insertions(+), 11 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index beac6f2b45..bb9e013abe 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -10,19 +10,19 @@ module.exports = CollaboratorsInviteController =
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
email = req.body.email
- sendingUserId = req.session?.user_id
+ sendingUserId = req.session?.user?._id
logger.log {projectId, email, sendingUserId}, "inviting to project"
- LimitationsManager.canAddXCollaborators project_id, 1, (error, allowed) =>
+ LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
return next(error) if error?
if !allowed
logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
- return res.json {}
+ return res.json {inviteId: null}
{email, privileges} = req.body
email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
if !email? or email == ""
logger.log {projectId, email, sendingUserId}, "invalid email address"
- return res.status(400).send("invalid email address")
- CollaboratorsInviteHandler.inviteToProject projectId, sendingUserId, email, priveleges, (err, invite) ->
+ return res.sendStatus(400)
+ CollaboratorsInviteHandler.inviteToProject projectId, sendingUserId, email, privileges, (err, invite) ->
if err?
logger.err {projectId, email, sendingUserId}, "error creating project invite"
return next(err)
@@ -37,7 +37,7 @@ module.exports = CollaboratorsInviteController =
if err?
logger.err {projectId, inviteId}, "error revoking invite"
return next(err)
- res.status(201).send()
+ res.sendStatus(201)
viewInvite: (req, res, next) ->
projectId = req.params.Project_id
@@ -49,7 +49,7 @@ module.exports = CollaboratorsInviteController =
return next(err)
if !invite
logger.log {projectId, token}, "no invite found for token"
- return res.redirect("/")
+ return res.sendStatus(404)
res.render "project/invite", {invite}
acceptInvite: (req, res, next) ->
@@ -59,6 +59,6 @@ module.exports = CollaboratorsInviteController =
logger.log {projectId, inviteId}, "accepting invite"
CollaboratorsInviteHandler.acceptInvite projectId, inviteId, currentUser, (err) ->
if err?
- logger.err {projectId, token}, "error getting invite by token"
+ logger.err {projectId, inviteId}, "error getting invite by token"
return next(err)
- rest.status(201).send()
+ res.sendStatus(201)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index f22bcb4c4f..2d9932c093 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -11,7 +11,7 @@ Errors = require "../Errors/Errors"
module.experts = CollaboratorsInviteHandler =
- inviteToProject: (projectId, sendingUserId, email, priveleges, callback=(err,invite)->) ->
+ inviteToProject: (projectId, sendingUserId, email, privileges, callback=(err,invite)->) ->
revokeInvite: (projectId, inviteId, callback=(err)->) ->
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index fb9e77f952..3a7ad86761 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -18,12 +18,106 @@ describe "CollaboratorsInviteController", ->
'../Subscription/LimitationsManager' : @LimitationsManager = {}
'../Project/ProjectEditorHandler' : @ProjectEditorHandler = {}
'../User/UserGetter': @UserGetter = {}
+ 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
@res = new MockResponse()
@req = new MockRequest()
@project_id = "project-id-123"
@callback = sinon.stub()
+ describe 'inviteToProject', ->
+
+ beforeEach ->
+ @targetEmail = "user@example.com"
+ @req.params =
+ Project_id: @project_id
+ @req.session =
+ user: _id: @current_user_id = "current-user-id"
+ @req.body =
+ email: @targetEmail
+ privileges: @privileges = "readAndWrite"
+ @res.json = sinon.stub()
+ @res.sendStatus = sinon.stub()
+ @invite = {
+ _id: ObjectId(),
+ token: "htnseuthaouse",
+ sendingUserId: @current_user_id,
+ projectId: @targetEmail,
+ targetEmail: 'user@example.com'
+ createdAt: new Date(),
+ expiresAt: new Date()
+ }
+ @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
+ @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite)
+ @callback = sinon.stub()
+ @next = sinon.stub()
+
+ describe 'when all goes well', ->
+
+ beforeEach ->
+ @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
+ @CollaboratorsInviteController.inviteToProject @req, @res, @next
+
+ it 'should produce json response', ->
+ @res.json.callCount.should.equal 1
+ ({inviteId: @invite._id}).should.deep.equal(@res.json.firstCall.args[0])
+
+ it 'should have called canAddXCollaborators', ->
+ @LimitationsManager.canAddXCollaborators.callCount.should.equal 1
+ @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
+
+ it 'should have called inviteToProject', ->
+ @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
+ @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true
+
+ describe 'when the user is not allowed to add more collaborators', ->
+
+ beforeEach ->
+ @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false)
+ @CollaboratorsInviteController.inviteToProject @req, @res, @next
+
+ it 'should produce json response without inviteId', ->
+ @res.json.callCount.should.equal 1
+ ({inviteId: null}).should.deep.equal(@res.json.firstCall.args[0])
+
+ it 'should not have called inviteToProject', ->
+ @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
+
+ describe 'when canAddXCollaborators produces an error', ->
+
+ beforeEach ->
+ @err = new Error('woops')
+ @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err)
+ @CollaboratorsInviteController.inviteToProject @req, @res, @next
+
+ it 'should call next with an error', ->
+ @next.callCount.should.equal 1
+ @next.calledWith(@err).should.equal true
+
+ it 'should not have called inviteToProject', ->
+ @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
+
+ describe 'when inviteToProject produces an error', ->
+
+ beforeEach ->
+ @err = new Error('woops')
+ @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err)
+ @CollaboratorsInviteController.inviteToProject @req, @res, @next
+
+ it 'should call next with an error', ->
+ @next.callCount.should.equal 1
+ @next.calledWith(@err).should.equal true
+
+ it 'should have called canAddXCollaborators', ->
+ @LimitationsManager.canAddXCollaborators.callCount.should.equal 1
+ @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true
+
+ it 'should have called inviteToProject', ->
+ @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
+ @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true
+
+ ##
+
describe "viewInvite", ->
beforeEach ->
@@ -33,6 +127,7 @@ describe "CollaboratorsInviteController", ->
@req.session =
user: _id: @current_user_id = "current-user-id"
@res.render = sinon.stub()
+ @res.sendStatus = sinon.stub()
@invite = {
_id: ObjectId(),
token: "htnseuthaouse",
@@ -54,4 +149,129 @@ describe "CollaboratorsInviteController", ->
it 'should render the view template', ->
@res.render.callCount.should.equal 1
- @res.render.firstCall.args[0].should.equal 'project/invite'
+ @res.render.calledWith('project/invite').should.equal true
+
+ it 'should not call next', ->
+ @next.callCount.should.equal 0
+
+ it 'should call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+
+ describe 'when the getInviteByToken produces an error', ->
+
+ beforeEach ->
+ @err = new Error('woops')
+ @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, @err)
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
+
+ it 'should call next with the error', ->
+ @next.callCount.should.equal 1
+ @next.calledWith(@err).should.equal true
+
+ it 'should call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+
+ describe 'when the getInviteByToken does not produce an invite', ->
+
+ beforeEach ->
+ @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null)
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
+
+ it 'should produce a 404 response', ->
+ @res.sendStatus.callCount.should.equal 1
+ @res.sendStatus.calledWith(404).should.equal true
+
+ it 'should not render the view template', ->
+ @res.render.callCount.should.equal 0
+
+ it 'should not call next', ->
+ @next.callCount.should.equal 0
+
+ it 'should call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+
+ describe "revokeInvite", ->
+
+ beforeEach ->
+ @req.params =
+ Project_id: @project_id
+ invite_id: @invite_id = "thuseoautoh"
+ @req.session =
+ user: _id: @current_user_id = "current-user-id"
+ @res.render = sinon.stub()
+ @res.sendStatus = sinon.stub()
+ @CollaboratorsInviteHandler.revokeInvite = sinon.stub().callsArgWith(2, null)
+ @callback = sinon.stub()
+ @next = sinon.stub()
+
+ describe 'when revokeInvite does not produce an error', ->
+
+ beforeEach ->
+ @CollaboratorsInviteController.revokeInvite @req, @res, @next
+
+ it 'should produce a 201 response', ->
+ @res.sendStatus.callCount.should.equal 1
+ @res.sendStatus.calledWith(201).should.equal true
+
+ it 'should have called revokeInvite', ->
+ @CollaboratorsInviteHandler.revokeInvite.callCount.should.equal 1
+
+ describe 'when revokeInvite produces an error', ->
+
+ beforeEach ->
+ @err = new Error('woops')
+ @CollaboratorsInviteHandler.revokeInvite = sinon.stub().callsArgWith(2, @err)
+ @CollaboratorsInviteController.revokeInvite @req, @res, @next
+
+ it 'should not produce a 201 response', ->
+ @res.sendStatus.callCount.should.equal 0
+
+ it 'should call next with the error', ->
+ @next.callCount.should.equal 1
+ @next.calledWith(@err).should.equal true
+
+ it 'should have called revokeInvite', ->
+ @CollaboratorsInviteHandler.revokeInvite.callCount.should.equal 1
+
+ describe "acceptInvite", ->
+
+ beforeEach ->
+ @req.params =
+ Project_id: @project_id
+ invite_id: @invite_id = "thuseoautoh"
+ @req.session =
+ user: _id: @current_user_id = "current-user-id"
+ @res.render = sinon.stub()
+ @res.sendStatus = sinon.stub()
+ @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, null)
+ @callback = sinon.stub()
+ @next = sinon.stub()
+
+ describe 'when acceptInvite does not produce an error', ->
+
+ beforeEach ->
+ @CollaboratorsInviteController.acceptInvite @req, @res, @next
+
+ it 'should produce a 201 response', ->
+ @res.sendStatus.callCount.should.equal 1
+ @res.sendStatus.calledWith(201).should.equal true
+
+ it 'should have called acceptInvite', ->
+ @CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1
+
+ describe 'when revokeInvite produces an error', ->
+
+ beforeEach ->
+ @err = new Error('woops')
+ @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, @err)
+ @CollaboratorsInviteController.acceptInvite @req, @res, @next
+
+ it 'should not produce a 201 response', ->
+ @res.sendStatus.callCount.should.equal 0
+
+ it 'should call next with the error', ->
+ @next.callCount.should.equal 1
+ @next.calledWith(@err).should.equal true
+
+ it 'should have called acceptInvite', ->
+ @CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1
From 4db9d5a46606c34fcb82ea5244415f9ff2bd1f46 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 21 Jul 2016 13:34:20 +0100
Subject: [PATCH 010/378] remove whatespace and comment
---
.../Collaborators/CollaboratorsInviteControllerTests.coffee | 2 --
1 file changed, 2 deletions(-)
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 3a7ad86761..aa8261f881 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -116,8 +116,6 @@ describe "CollaboratorsInviteController", ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
@CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true
- ##
-
describe "viewInvite", ->
beforeEach ->
From e0562a230155aba48e19f4986dc0f79f8cd2afa1 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 21 Jul 2016 14:20:09 +0100
Subject: [PATCH 011/378] Update ProjectInvite model
---
.../app/coffee/models/ProjectInvite.coffee | 22 ++++++++++++-------
1 file changed, 14 insertions(+), 8 deletions(-)
diff --git a/services/web/app/coffee/models/ProjectInvite.coffee b/services/web/app/coffee/models/ProjectInvite.coffee
index 3349dafa9b..1181454d74 100644
--- a/services/web/app/coffee/models/ProjectInvite.coffee
+++ b/services/web/app/coffee/models/ProjectInvite.coffee
@@ -4,15 +4,21 @@ Settings = require 'settings-sharelatex'
Schema = mongoose.Schema
ObjectId = Schema.ObjectId
+THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30
+
+makeExpirationDate = () ->
+ nowInMillis = Date.now()
+ new Date(nowInMillis + (1000 * THIRTY_DAYS_IN_SECONDS))
+
+
ProjectInviteSchema = new Schema
- project_id: ObjectId
- from_user_id: ObjectId
- privilegeLevel: String
- # For existing users
- to_user_id: ObjectId
- # For non-existant users
- hashed_token: String
email: String
+ token: String
+ sendingUserId: ObjectId
+ projectId: ObjectId
+ privileges: String
+ createdAt: {type: Date, default: Date.now}
+ expiresAt: {type: Date, default: makeExpirationDate}
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10)
@@ -20,4 +26,4 @@ ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema)
mongoose.model 'ProjectInvite', ProjectInviteSchema
exports.ProjectInvite = ProjectInvite
-exports.ProjectInviteSchema = ProjectInviteSchema
\ No newline at end of file
+exports.ProjectInviteSchema = ProjectInviteSchema
From d9c6df0e4765d181c863d03cab88e58b07ce5c6a Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 21 Jul 2016 15:56:41 +0100
Subject: [PATCH 012/378] start adding the ProjectInvite workflow.
---
.../CollaboratorsEmailHandler.coffee | 18 +++++++++++++++-
.../CollaboratorsInviteHandler.coffee | 21 +++++++++++++++++++
.../coffee/Features/Email/EmailBuilder.coffee | 20 ++++++++++++++++++
3 files changed, 58 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
index e9beb1bb43..f5d08054d4 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
@@ -3,6 +3,7 @@ EmailHandler = require("../Email/EmailHandler")
Settings = require "settings-sharelatex"
module.exports =
+
notifyUserOfProjectShare: (project_id, email, callback)->
Project
.findOne(_id: project_id )
@@ -22,4 +23,19 @@ module.exports =
"rs=ci" # referral source = collaborator invite
].join("&")
owner: project.owner_ref
- EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
\ No newline at end of file
+ EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
+
+ notifyUserOfProjectInvite: (project_id, email, invite, callback)->
+ Project
+ .findOne(_id: project_id )
+ .select("name owner_ref")
+ .populate('owner_ref')
+ .exec (err, project)->
+ emailOptions =
+ to: email
+ replyTo: project.owner_ref.email
+ project:
+ name: project.name
+ inviteUrl: "#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}"
+ owner: project.owner_ref
+ EmailHandler.sendEmail "projectInvite", emailOptions, callback
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 2d9932c093..9812a9b987 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -1,5 +1,6 @@
UserCreator = require('../User/UserCreator')
Project = require("../../models/Project").Project
+ProjectInvite = require("../../models/ProjectInvite").ProjectInvite
mimelib = require("mimelib")
logger = require('logger-sharelatex')
UserGetter = require "../User/UserGetter"
@@ -8,10 +9,30 @@ CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
Async = require "async"
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
Errors = require "../Errors/Errors"
+Crypto = require 'crypto'
module.experts = CollaboratorsInviteHandler =
inviteToProject: (projectId, sendingUserId, email, privileges, callback=(err,invite)->) ->
+ logger.log {projectId, sendingUserId, email, privileges}, "adding invite"
+ Crypto.randomBytes 24, (err, buffer) ->
+ if err?
+ logger.err {err, projectId, sendingUserId, email}, "error generating random token"
+ return callback(err)
+ token = buffer.toString('hex')
+ invite = new ProjectInvite {
+ email: email
+ token: token
+ sendingUserId: sendingUserId
+ projectId: projectId
+ privileges: privileges
+ }
+ ProjectInvite.save (err) ->
+ if err?
+ logger.err {err, projectId, sendingUserId, email}, "error saving token"
+ return callback(err)
+ CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, email, invite
+ callback(null, invite)
revokeInvite: (projectId, inviteId, callback=(err)->) ->
diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee
index 4bcf0c671d..dab9fda839 100644
--- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee
+++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee
@@ -87,6 +87,26 @@ templates.projectSharedWithYou =
#{settings.appName}
"""
+templates.projectInvite =
+ subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
+ layout: NotificationEmailLayout
+ type:"notification"
+ compiledTemplate: _.template """
+Hi, <%= owner.email %> wants to share '<%= project.name %>' with you
+
+
+
+ Thank you
+ #{settings.appName}
+"""
templates.completeJoinGroupAccount =
subject: _.template "Verify Email to join <%= group_name %> group"
From 546517db90294a864491d9e5507d9e1959f7b3a9 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 21 Jul 2016 16:19:15 +0100
Subject: [PATCH 013/378] revokeInvite and getInviteByToken functions.
---
.../CollaboratorsInviteHandler.coffee | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 9812a9b987..b8416dc93e 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -35,7 +35,23 @@ module.experts = CollaboratorsInviteHandler =
callback(null, invite)
revokeInvite: (projectId, inviteId, callback=(err)->) ->
+ logger.log {projectId, inviteId}, "removing invite"
+ ProjectInvite.remove {projectId: projectId, _id: inviteId}, (err) ->
+ if err?
+ logger.err {err, projectId, inviteId}, "error removing invite"
+ return callback(err)
+ callback(null)
getInviteByToken: (projectId, tokenString, callback=(err,invite)->) ->
+ logger.log {projectId, tokenString}, "fetching invite by token"
+ ProjectInvite.findOne {projectId: projectId, token: tokenString}, (err, invite) ->
+ if err?
+ logger.err {err, projectId, inviteId}, "error fetching invite"
+ return callback(err)
+ now = new Date()
+ if invite.expiresAt < now
+ logger.log {projectId, inviteId, expiresAt: invite.expiresAt}, "invite expired"
+ return callback(null, null)
+ callback(null, invite)
acceptInvite: (projectId, inviteId, callback=(err)->) ->
From 11394447904b4166e797b94a35a1daa7800a5555 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 22 Jul 2016 09:27:00 +0100
Subject: [PATCH 014/378] add token to body of `acceptInvite` action.
---
.../Collaborators/CollaboratorsInviteController.coffee | 3 ++-
.../Collaborators/CollaboratorsInviteControllerTests.coffee | 6 ++++--
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index bb9e013abe..4b48ce01c9 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -55,9 +55,10 @@ module.exports = CollaboratorsInviteController =
acceptInvite: (req, res, next) ->
projectId = req.params.Project_id
inviteId = req.params.inviteId
+ {token} = req.body
currentUser = req.session.user
logger.log {projectId, inviteId}, "accepting invite"
- CollaboratorsInviteHandler.acceptInvite projectId, inviteId, currentUser, (err) ->
+ CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) ->
if err?
logger.err {projectId, inviteId}, "error getting invite by token"
return next(err)
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index aa8261f881..5ee04aeadf 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -239,9 +239,11 @@ describe "CollaboratorsInviteController", ->
invite_id: @invite_id = "thuseoautoh"
@req.session =
user: _id: @current_user_id = "current-user-id"
+ @req.body =
+ token: "thsueothaueotauahsuet"
@res.render = sinon.stub()
@res.sendStatus = sinon.stub()
- @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, null)
+ @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, null)
@callback = sinon.stub()
@next = sinon.stub()
@@ -261,7 +263,7 @@ describe "CollaboratorsInviteController", ->
beforeEach ->
@err = new Error('woops')
- @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, @err)
+ @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, @err)
@CollaboratorsInviteController.acceptInvite @req, @res, @next
it 'should not produce a 201 response', ->
From 9fba98cd4554c91864db4985114b39da79f28406 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 22 Jul 2016 11:38:00 +0100
Subject: [PATCH 015/378] Accept invite, and start testing the invite handler.
---
.../CollaboratorsInviteController.coffee | 2 +-
.../CollaboratorsInviteHandler.coffee | 70 +++++++++++++++++--
.../CollaboratorsInviteHandlerTests.coffee | 69 ++++++++++++++++++
3 files changed, 135 insertions(+), 6 deletions(-)
create mode 100644 services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 4b48ce01c9..1cc36d6e14 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -60,6 +60,6 @@ module.exports = CollaboratorsInviteController =
logger.log {projectId, inviteId}, "accepting invite"
CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) ->
if err?
- logger.err {projectId, inviteId}, "error getting invite by token"
+ logger.err {projectId, inviteId}, "error accepting invite by token"
return next(err)
res.sendStatus(201)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index b8416dc93e..9d23605ef8 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -1,9 +1,7 @@
-UserCreator = require('../User/UserCreator')
Project = require("../../models/Project").Project
ProjectInvite = require("../../models/ProjectInvite").ProjectInvite
mimelib = require("mimelib")
logger = require('logger-sharelatex')
-UserGetter = require "../User/UserGetter"
ContactManager = require "../Contacts/ContactManager"
CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
Async = require "async"
@@ -11,7 +9,7 @@ PrivilegeLevels = require "../Authorization/PrivilegeLevels"
Errors = require "../Errors/Errors"
Crypto = require 'crypto'
-module.experts = CollaboratorsInviteHandler =
+module.exports = CollaboratorsInviteHandler =
inviteToProject: (projectId, sendingUserId, email, privileges, callback=(err,invite)->) ->
logger.log {projectId, sendingUserId, email, privileges}, "adding invite"
@@ -27,7 +25,7 @@ module.experts = CollaboratorsInviteHandler =
projectId: projectId
privileges: privileges
}
- ProjectInvite.save (err) ->
+ invite.save (err, invite) ->
if err?
logger.err {err, projectId, sendingUserId, email}, "error saving token"
return callback(err)
@@ -48,10 +46,72 @@ module.experts = CollaboratorsInviteHandler =
if err?
logger.err {err, projectId, inviteId}, "error fetching invite"
return callback(err)
+ if !invite
+ err = new Errors.NotFoundError("no invite found for token")
+ logger.err {err, projectId, token: tokenString}, "no invite found"
+ return callback(err)
now = new Date()
+ # TODO: re-assess whether we should return null or a notfounderror
if invite.expiresAt < now
logger.log {projectId, inviteId, expiresAt: invite.expiresAt}, "invite expired"
return callback(null, null)
callback(null, invite)
- acceptInvite: (projectId, inviteId, callback=(err)->) ->
+ acceptInvite: (projectId, inviteId, tokenString, user, callback=(err)->) ->
+ Project.findOne {_id: projectId}, (err, project) ->
+ if err?
+ logger.err {err, projectId}, "error finding project"
+ return callback(err)
+ if !project
+ err = new Errors.NotFoundError("no project found for invite")
+ logger.log {err, projectId, inviteId}, "no project found"
+ return callback(err)
+ # TODO: check if we need to cast the ids to ObjectId
+ ProjectInvite.findOne {_id: inviteId, projectId: projectId, token: token}, (err, invite) ->
+ if err?
+ logger.err {err, projectId, inviteId}, "error finding invite"
+ return callback(err)
+ if !invite
+ err = new Errors.NotFoundError("no matching invite found")
+ logger.log {err, projectId, inviteId}, "no matching invite found"
+ return callback(err)
+
+ now = new Date()
+ if invite.expiresAt < now
+ err = new Errors.NotFoundError("invite expired")
+ logger.log {err, projectId, inviteId, expiresAt: invite.expiresAt}, "invite expired"
+ return callback(err)
+
+ # do the thing
+ existing_users = (project.collaberator_refs or [])
+ existing_users = existing_users.concat(project.readOnly_refs or [])
+ existing_users = existing_users.map (u) -> u.toString()
+ if existing_users.indexOf(user._id.toString()) > -1
+ return callback null # User already in Project
+
+ privilegeLevel = invite.privileges
+
+ if privilegeLevel == PrivilegeLevels.READ_AND_WRITE
+ level = {"collaberator_refs": user._id}
+ logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user"
+ else if privilegeLevel == PrivilegeLevels.READ_ONLY
+ level = {"readOnly_refs": user._id}
+ logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user"
+ else
+ return callback(new Error("unknown privilegeLevel: #{privilegeLevel}"))
+
+ ContactManager.addContact invite.sendingUserId, user._id
+
+ Project.update { _id: project._id }, { $addToSet: level }, (error) ->
+ return callback(error) if error?
+ # Flush to TPDS in background to add files to collaborator's Dropbox
+ ProjectEntityHandler = require("../Project/ProjectEntityHandler")
+ ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, (error) ->
+ if error?
+ logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator"
+ # Remove invite
+ ProjectInvite.remove {_id: inviteId}, (err) ->
+ if err?
+ logger.err {err, projectId, inviteId}, "error removing invite"
+ return callback(err)
+ callback()
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
new file mode 100644
index 0000000000..77c768cf47
--- /dev/null
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -0,0 +1,69 @@
+sinon = require('sinon')
+chai = require('chai')
+should = chai.should()
+expect = chai.expect
+modulePath = "../../../../app/js/Features/Collaborators/CollaboratorsInviteHandler.js"
+SandboxedModule = require('sandboxed-module')
+events = require "events"
+ObjectId = require("mongojs").ObjectId
+
+describe "CollaboratorsInviteHandler", ->
+ beforeEach ->
+ @ProjectInvite = class ProjectInvite
+ constructor: (options={}) ->
+ this._id = ObjectId()
+ for k,v of options
+ this[k] = v
+ this
+ save: sinon.stub()
+ findOne: sinon.stub()
+ @Project = {}
+ @CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires:
+ 'settings-sharelatex': @settings = {}
+ 'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
+ './CollaboratorsEmailHandler': @CollaboratorsEmailHandler = {}
+ '../Contacts/ContactManager': @ContactManager = {}
+ '../../models/Project': {Project: @Project}
+ '../../models/ProjectInvite': {ProjectInvite: @ProjectInvite}
+
+ @projectId = ObjectId()
+ @sendingUserId = ObjectId()
+ @email = "user@example.com"
+ @userId = ObjectId()
+ @privileges = "readAndWrite"
+
+ describe 'inviteToProject', ->
+
+ beforeEach ->
+ @ProjectInvite::save = sinon.spy (cb) -> cb(null, this)
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub()
+ @call = (callback) =>
+ @CollaboratorsInviteHandler.inviteToProject @projectId, @sendingUserId, @email, @privileges, callback
+
+ describe 'when all goes well', ->
+
+ beforeEach ->
+
+ it 'should not produce an error', (done) ->
+ @call (err, invite) =>
+ expect(err).to.not.be.instanceof Error
+ done()
+
+ it 'should produce the invite object', (done) ->
+ @call (err, invite) =>
+ expect(invite).to.not.equal null
+ expect(invite).to.not.equal undefined
+ expect(invite).to.be.instanceof Object
+ expect(invite).to.have.all.keys ['_id', 'email', 'token', 'sendingUserId', 'projectId', 'privileges']
+ done()
+
+ it 'should have called ProjectInvite.save', (done) ->
+ @call (err, invite) =>
+ @ProjectInvite::save.callCount.should.equal 1
+ done()
+
+ it 'should have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) ->
+ @call (err, invite) =>
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @email).should.equal true
+ done()
From f866bd03bc3e9d2a181759754d35a9728b9d979f Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 22 Jul 2016 11:53:55 +0100
Subject: [PATCH 016/378] Spy on the randomBytes function
---
.../CollaboratorsInviteHandlerTests.coffee | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index 77c768cf47..d38d7a2a72 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -6,6 +6,7 @@ modulePath = "../../../../app/js/Features/Collaborators/CollaboratorsInviteHandl
SandboxedModule = require('sandboxed-module')
events = require "events"
ObjectId = require("mongojs").ObjectId
+Crypto = require('crypto')
describe "CollaboratorsInviteHandler", ->
beforeEach ->
@@ -18,6 +19,7 @@ describe "CollaboratorsInviteHandler", ->
save: sinon.stub()
findOne: sinon.stub()
@Project = {}
+ @Crypto = Crypto
@CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires:
'settings-sharelatex': @settings = {}
'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
@@ -25,6 +27,7 @@ describe "CollaboratorsInviteHandler", ->
'../Contacts/ContactManager': @ContactManager = {}
'../../models/Project': {Project: @Project}
'../../models/ProjectInvite': {ProjectInvite: @ProjectInvite}
+ 'crypto': @Crypto
@projectId = ObjectId()
@sendingUserId = ObjectId()
@@ -36,10 +39,14 @@ describe "CollaboratorsInviteHandler", ->
beforeEach ->
@ProjectInvite::save = sinon.spy (cb) -> cb(null, this)
+ @randomBytesSpy = sinon.spy(@Crypto, 'randomBytes')
@CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub()
@call = (callback) =>
@CollaboratorsInviteHandler.inviteToProject @projectId, @sendingUserId, @email, @privileges, callback
+ afterEach ->
+ @randomBytesSpy.restore()
+
describe 'when all goes well', ->
beforeEach ->
@@ -57,6 +64,11 @@ describe "CollaboratorsInviteHandler", ->
expect(invite).to.have.all.keys ['_id', 'email', 'token', 'sendingUserId', 'projectId', 'privileges']
done()
+ it 'should have generated a random token', (done) ->
+ @call (err, invite) =>
+ @randomBytesSpy.callCount.should.equal 1
+ done()
+
it 'should have called ProjectInvite.save', (done) ->
@call (err, invite) =>
@ProjectInvite::save.callCount.should.equal 1
From c9cfcddbe9ffaeff7bc8fd0450b949c1fddf439c Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 22 Jul 2016 11:54:16 +0100
Subject: [PATCH 017/378] test error case for inviteToProject
---
.../CollaboratorsInviteHandlerTests.coffee | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index d38d7a2a72..f4e3db64da 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -79,3 +79,13 @@ describe "CollaboratorsInviteHandler", ->
@CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1
@CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @email).should.equal true
done()
+
+ describe 'when saving model produces an error', ->
+
+ beforeEach ->
+ @ProjectInvite::save = sinon.spy (cb) -> cb(new Error('woops'), this)
+
+ it 'should produce an error', (done) ->
+ @call (err, invite) =>
+ expect(err).to.be.instanceof Error
+ done()
From e34b124c73269c11bbec7f12433343f240532dd8 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 22 Jul 2016 13:33:21 +0100
Subject: [PATCH 018/378] Test `revokeInvite`
---
.../CollaboratorsInviteHandlerTests.coffee | 37 ++++++++++++++++++-
1 file changed, 36 insertions(+), 1 deletion(-)
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index f4e3db64da..e97b98dc3a 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -17,7 +17,8 @@ describe "CollaboratorsInviteHandler", ->
this[k] = v
this
save: sinon.stub()
- findOne: sinon.stub()
+ @findOne: sinon.stub()
+ @remove: sinon.stub()
@Project = {}
@Crypto = Crypto
@CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires:
@@ -33,6 +34,7 @@ describe "CollaboratorsInviteHandler", ->
@sendingUserId = ObjectId()
@email = "user@example.com"
@userId = ObjectId()
+ @inviteId = ObjectId()
@privileges = "readAndWrite"
describe 'inviteToProject', ->
@@ -89,3 +91,36 @@ describe "CollaboratorsInviteHandler", ->
@call (err, invite) =>
expect(err).to.be.instanceof Error
done()
+
+ describe 'revokeInvite', ->
+
+ beforeEach ->
+ @ProjectInvite.remove.callsArgWith(1, null)
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub()
+ @call = (callback) =>
+ @CollaboratorsInviteHandler.revokeInvite @projectId, @inviteId, callback
+
+ describe 'when all goes well', ->
+
+ beforeEach ->
+
+ it 'should not produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.not.be.instanceof Error
+ done()
+
+ it 'should call ProjectInvite.remove', (done) ->
+ @call (err) =>
+ @ProjectInvite.remove.callCount.should.equal 1
+ @ProjectInvite.remove.calledWith({projectId: @projectId, _id: @inviteId}).should.equal true
+ done()
+
+ describe 'when remove produces an error', ->
+
+ beforeEach ->
+ @ProjectInvite.remove.callsArgWith(1, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ done()
From b201f1a37ab40d8edad69013a0b3d6a812b16a12 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 22 Jul 2016 14:21:34 +0100
Subject: [PATCH 019/378] Test getInviteByToken.
---
.../CollaboratorsInviteController.coffee | 1 +
.../CollaboratorsInviteHandler.coffee | 4 +-
.../CollaboratorsInviteHandlerTests.coffee | 82 ++++++++++++++++++-
3 files changed, 84 insertions(+), 3 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 1cc36d6e14..76657c1f60 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -47,6 +47,7 @@ module.exports = CollaboratorsInviteController =
if err?
logger.err {projectId, token}, "error getting invite by token"
return next(err)
+ # TODO: should we render an expired view instead?
if !invite
logger.log {projectId, token}, "no invite found for token"
return res.sendStatus(404)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 9d23605ef8..9e92fce26c 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -44,7 +44,7 @@ module.exports = CollaboratorsInviteHandler =
logger.log {projectId, tokenString}, "fetching invite by token"
ProjectInvite.findOne {projectId: projectId, token: tokenString}, (err, invite) ->
if err?
- logger.err {err, projectId, inviteId}, "error fetching invite"
+ logger.err {err, projectId}, "error fetching invite"
return callback(err)
if !invite
err = new Errors.NotFoundError("no invite found for token")
@@ -53,7 +53,7 @@ module.exports = CollaboratorsInviteHandler =
now = new Date()
# TODO: re-assess whether we should return null or a notfounderror
if invite.expiresAt < now
- logger.log {projectId, inviteId, expiresAt: invite.expiresAt}, "invite expired"
+ logger.log {projectId, inviteId: invite._id, expiresAt: invite.expiresAt}, "invite expired"
return callback(null, null)
callback(null, invite)
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index e97b98dc3a..728a75894d 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -35,6 +35,7 @@ describe "CollaboratorsInviteHandler", ->
@email = "user@example.com"
@userId = ObjectId()
@inviteId = ObjectId()
+ @token = 'hnhteaosuhtaeosuahs'
@privileges = "readAndWrite"
describe 'inviteToProject', ->
@@ -56,6 +57,7 @@ describe "CollaboratorsInviteHandler", ->
it 'should not produce an error', (done) ->
@call (err, invite) =>
expect(err).to.not.be.instanceof Error
+ expect(err).to.be.oneOf [null, undefined]
done()
it 'should produce the invite object', (done) ->
@@ -96,7 +98,6 @@ describe "CollaboratorsInviteHandler", ->
beforeEach ->
@ProjectInvite.remove.callsArgWith(1, null)
- @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub()
@call = (callback) =>
@CollaboratorsInviteHandler.revokeInvite @projectId, @inviteId, callback
@@ -107,6 +108,7 @@ describe "CollaboratorsInviteHandler", ->
it 'should not produce an error', (done) ->
@call (err) =>
expect(err).to.not.be.instanceof Error
+ expect(err).to.be.oneOf [null, undefined]
done()
it 'should call ProjectInvite.remove', (done) ->
@@ -124,3 +126,81 @@ describe "CollaboratorsInviteHandler", ->
@call (err) =>
expect(err).to.be.instanceof Error
done()
+
+ describe 'getInviteByToken', ->
+
+ beforeEach ->
+ @theDarkFuture = new Date()
+ @theDarkFuture.setYear(40000)
+ @fakeInvite =
+ _id: @inviteId
+ email: @email
+ token: @token
+ sendingUserId: @sendingUserId
+ projectId: @projectId
+ privileges: @privileges
+ createdAt: new Date()
+ expiresAt: @theDarkFuture
+ @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
+ @call = (callback) =>
+ @CollaboratorsInviteHandler.getInviteByToken @projectId, @token, callback
+
+ describe 'when all goes well', ->
+
+ beforeEach ->
+
+ it 'should not produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.not.be.instanceof Error
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should produce the invite object', (done) ->
+ @call (err, invite) =>
+ expect(invite).to.deep.equal @fakeInvite
+ done()
+
+ it 'should call ProjectInvite.findOne', (done) ->
+ @call (err) =>
+ @ProjectInvite.findOne.callCount.should.equal 1
+ @ProjectInvite.findOne.calledWith({projectId: @projectId, token: @token}).should.equal true
+ done()
+
+ describe 'when findOne produces an error', ->
+
+ beforeEach ->
+ @ProjectInvite.findOne.callsArgWith(1, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ done()
+
+ describe 'when findOne does not find an invite', ->
+
+ beforeEach ->
+ @ProjectInvite.findOne.callsArgWith(1, null, null)
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ done()
+
+ describe 'when the invite is expired', ->
+
+ beforeEach ->
+ @theDeepPast = new Date()
+ @theDeepPast.setYear(1977)
+ @fakeInvite.expiresAt = @theDeepPast
+ @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
+
+ it 'should not produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.not.be.instanceof Error
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should not produce an invite object', (done) ->
+ @call (err, invite) =>
+ expect(invite).to.be.oneOf [null, undefined]
+ done()
From 9e0c44573a1ae0e548a6a1758bb6d73dc9e5240f Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 22 Jul 2016 16:08:56 +0100
Subject: [PATCH 020/378] Remove `expiresAt`, use mongo TTL instead.
---
.../CollaboratorsInviteController.coffee | 2 +-
.../CollaboratorsInviteHandler.coffee | 6 -----
.../app/coffee/models/ProjectInvite.coffee | 12 ++++-----
.../CollaboratorsInviteControllerTests.coffee | 2 --
.../CollaboratorsInviteHandlerTests.coffee | 25 ++++---------------
5 files changed, 12 insertions(+), 35 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 76657c1f60..1c8e3adec7 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -47,7 +47,7 @@ module.exports = CollaboratorsInviteController =
if err?
logger.err {projectId, token}, "error getting invite by token"
return next(err)
- # TODO: should we render an expired view instead?
+ # TODO: render a not-valid view instead
if !invite
logger.log {projectId, token}, "no invite found for token"
return res.sendStatus(404)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 9e92fce26c..80c9f10f8a 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -47,13 +47,7 @@ module.exports = CollaboratorsInviteHandler =
logger.err {err, projectId}, "error fetching invite"
return callback(err)
if !invite
- err = new Errors.NotFoundError("no invite found for token")
logger.err {err, projectId, token: tokenString}, "no invite found"
- return callback(err)
- now = new Date()
- # TODO: re-assess whether we should return null or a notfounderror
- if invite.expiresAt < now
- logger.log {projectId, inviteId: invite._id, expiresAt: invite.expiresAt}, "invite expired"
return callback(null, null)
callback(null, invite)
diff --git a/services/web/app/coffee/models/ProjectInvite.coffee b/services/web/app/coffee/models/ProjectInvite.coffee
index 1181454d74..decea99f46 100644
--- a/services/web/app/coffee/models/ProjectInvite.coffee
+++ b/services/web/app/coffee/models/ProjectInvite.coffee
@@ -1,14 +1,12 @@
mongoose = require 'mongoose'
Settings = require 'settings-sharelatex'
+
Schema = mongoose.Schema
ObjectId = Schema.ObjectId
-THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30
-makeExpirationDate = () ->
- nowInMillis = Date.now()
- new Date(nowInMillis + (1000 * THIRTY_DAYS_IN_SECONDS))
+THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30
ProjectInviteSchema = new Schema
@@ -17,13 +15,15 @@ ProjectInviteSchema = new Schema
sendingUserId: ObjectId
projectId: ObjectId
privileges: String
- createdAt: {type: Date, default: Date.now}
- expiresAt: {type: Date, default: makeExpirationDate}
+ createdAt: {type: Date, default: Date.now, index: {expiresAfterSeconds: THIRTY_DAYS_IN_SECONDS}}
+
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10)
+
ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema)
+
mongoose.model 'ProjectInvite', ProjectInviteSchema
exports.ProjectInvite = ProjectInvite
exports.ProjectInviteSchema = ProjectInviteSchema
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 5ee04aeadf..38d07269df 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -45,7 +45,6 @@ describe "CollaboratorsInviteController", ->
projectId: @targetEmail,
targetEmail: 'user@example.com'
createdAt: new Date(),
- expiresAt: new Date()
}
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
@CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite)
@@ -133,7 +132,6 @@ describe "CollaboratorsInviteController", ->
projectId: @projectId,
targetEmail: 'user@example.com'
createdAt: new Date(),
- expiresAt: new Date()
}
@CollaboratorsInviteHandler.getInviteByToken = sinon.stub().callsArgWith(2, null, @invite)
@callback = sinon.stub()
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index 728a75894d..8d7814a4c9 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -130,8 +130,6 @@ describe "CollaboratorsInviteHandler", ->
describe 'getInviteByToken', ->
beforeEach ->
- @theDarkFuture = new Date()
- @theDarkFuture.setYear(40000)
@fakeInvite =
_id: @inviteId
email: @email
@@ -140,7 +138,6 @@ describe "CollaboratorsInviteHandler", ->
projectId: @projectId
privileges: @privileges
createdAt: new Date()
- expiresAt: @theDarkFuture
@ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
@call = (callback) =>
@CollaboratorsInviteHandler.getInviteByToken @projectId, @token, callback
@@ -150,7 +147,7 @@ describe "CollaboratorsInviteHandler", ->
beforeEach ->
it 'should not produce an error', (done) ->
- @call (err) =>
+ @call (err, invite) =>
expect(err).to.not.be.instanceof Error
expect(err).to.be.oneOf [null, undefined]
done()
@@ -161,7 +158,7 @@ describe "CollaboratorsInviteHandler", ->
done()
it 'should call ProjectInvite.findOne', (done) ->
- @call (err) =>
+ @call (err, invite) =>
@ProjectInvite.findOne.callCount.should.equal 1
@ProjectInvite.findOne.calledWith({projectId: @projectId, token: @token}).should.equal true
done()
@@ -172,7 +169,7 @@ describe "CollaboratorsInviteHandler", ->
@ProjectInvite.findOne.callsArgWith(1, new Error('woops'))
it 'should produce an error', (done) ->
- @call (err) =>
+ @call (err, invite) =>
expect(err).to.be.instanceof Error
done()
@@ -181,26 +178,14 @@ describe "CollaboratorsInviteHandler", ->
beforeEach ->
@ProjectInvite.findOne.callsArgWith(1, null, null)
- it 'should produce an error', (done) ->
- @call (err) =>
- expect(err).to.be.instanceof Error
- done()
-
- describe 'when the invite is expired', ->
-
- beforeEach ->
- @theDeepPast = new Date()
- @theDeepPast.setYear(1977)
- @fakeInvite.expiresAt = @theDeepPast
- @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
-
it 'should not produce an error', (done) ->
- @call (err) =>
+ @call (err, invite) =>
expect(err).to.not.be.instanceof Error
expect(err).to.be.oneOf [null, undefined]
done()
it 'should not produce an invite object', (done) ->
@call (err, invite) =>
+ expect(invite).to.not.be.instanceof Error
expect(invite).to.be.oneOf [null, undefined]
done()
From 78570817d59eb3791f033e93b530c9362958f2d0 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 22 Jul 2016 16:28:00 +0100
Subject: [PATCH 021/378] Render a separate template if the invite is not
found.
---
.../CollaboratorsInviteController.coffee | 4 ++--
services/web/app/views/project/invite/not-valid.jade | 1 +
.../views/project/{invite.jade => invite/show.jade} | 0
.../CollaboratorsInviteControllerTests.coffee | 11 ++++-------
4 files changed, 7 insertions(+), 9 deletions(-)
create mode 100644 services/web/app/views/project/invite/not-valid.jade
rename services/web/app/views/project/{invite.jade => invite/show.jade} (100%)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 1c8e3adec7..c3c73a2165 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -50,8 +50,8 @@ module.exports = CollaboratorsInviteController =
# TODO: render a not-valid view instead
if !invite
logger.log {projectId, token}, "no invite found for token"
- return res.sendStatus(404)
- res.render "project/invite", {invite}
+ return res.render "project/invite/not-valid"
+ res.render "project/invite/show", {invite}
acceptInvite: (req, res, next) ->
projectId = req.params.Project_id
diff --git a/services/web/app/views/project/invite/not-valid.jade b/services/web/app/views/project/invite/not-valid.jade
new file mode 100644
index 0000000000..6eb6306b16
--- /dev/null
+++ b/services/web/app/views/project/invite/not-valid.jade
@@ -0,0 +1 @@
+h1 Invite Not Valid TEST
\ No newline at end of file
diff --git a/services/web/app/views/project/invite.jade b/services/web/app/views/project/invite/show.jade
similarity index 100%
rename from services/web/app/views/project/invite.jade
rename to services/web/app/views/project/invite/show.jade
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 38d07269df..6e1cb4f11a 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -145,7 +145,7 @@ describe "CollaboratorsInviteController", ->
it 'should render the view template', ->
@res.render.callCount.should.equal 1
- @res.render.calledWith('project/invite').should.equal true
+ @res.render.calledWith('project/invite/show').should.equal true
it 'should not call next', ->
@next.callCount.should.equal 0
@@ -173,12 +173,9 @@ describe "CollaboratorsInviteController", ->
@CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null)
@CollaboratorsInviteController.viewInvite @req, @res, @next
- it 'should produce a 404 response', ->
- @res.sendStatus.callCount.should.equal 1
- @res.sendStatus.calledWith(404).should.equal true
-
- it 'should not render the view template', ->
- @res.render.callCount.should.equal 0
+ it 'should render the not-valid view template', ->
+ @res.render.callCount.should.equal 1
+ @res.render.calledWith('project/invite/not-valid').should.equal true
it 'should not call next', ->
@next.callCount.should.equal 0
From 78a410c39d0c7047fc79f20fb54730a1e64b3aad Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 25 Jul 2016 09:07:47 +0100
Subject: [PATCH 022/378] Remove `expiresAt` logic from `acceptInvite`
---
.../Collaborators/CollaboratorsInviteHandler.coffee | 6 ------
1 file changed, 6 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 80c9f10f8a..50d75482e6 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -70,12 +70,6 @@ module.exports = CollaboratorsInviteHandler =
logger.log {err, projectId, inviteId}, "no matching invite found"
return callback(err)
- now = new Date()
- if invite.expiresAt < now
- err = new Errors.NotFoundError("invite expired")
- logger.log {err, projectId, inviteId, expiresAt: invite.expiresAt}, "invite expired"
- return callback(err)
-
# do the thing
existing_users = (project.collaberator_refs or [])
existing_users = existing_users.concat(project.readOnly_refs or [])
From 5438f39f9ebeac546a4f9d4f78ab0557342c640b Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 25 Jul 2016 09:58:08 +0100
Subject: [PATCH 023/378] Start testing `acceptInvite`
---
.../CollaboratorsInviteHandler.coffee | 6 +-
.../CollaboratorsInviteHandlerTests.coffee | 88 +++++++++++++++++--
2 files changed, 82 insertions(+), 12 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 50d75482e6..597fe8c082 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -61,7 +61,7 @@ module.exports = CollaboratorsInviteHandler =
logger.log {err, projectId, inviteId}, "no project found"
return callback(err)
# TODO: check if we need to cast the ids to ObjectId
- ProjectInvite.findOne {_id: inviteId, projectId: projectId, token: token}, (err, invite) ->
+ ProjectInvite.findOne {_id: inviteId, projectId: projectId, token: tokenString}, (err, invite) ->
if err?
logger.err {err, projectId, inviteId}, "error finding invite"
return callback(err)
@@ -94,9 +94,9 @@ module.exports = CollaboratorsInviteHandler =
return callback(error) if error?
# Flush to TPDS in background to add files to collaborator's Dropbox
ProjectEntityHandler = require("../Project/ProjectEntityHandler")
- ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, (error) ->
+ ProjectEntityHandler.flushProjectToThirdPartyDataStore project._id, (error) ->
if error?
- logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator"
+ logger.error {err: error, project_id: project._id, user_id}, "error flushing to TPDS after adding collaborator"
# Remove invite
ProjectInvite.remove {_id: inviteId}, (err) ->
if err?
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index 8d7814a4c9..61513ec8fa 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -19,7 +19,11 @@ describe "CollaboratorsInviteHandler", ->
save: sinon.stub()
@findOne: sinon.stub()
@remove: sinon.stub()
- @Project = {}
+ @Project = class Project
+ constructor: () ->
+ this
+ @findOne: sinon.stub()
+ @update: sinon.stub()
@Crypto = Crypto
@CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires:
'settings-sharelatex': @settings = {}
@@ -28,15 +32,27 @@ describe "CollaboratorsInviteHandler", ->
'../Contacts/ContactManager': @ContactManager = {}
'../../models/Project': {Project: @Project}
'../../models/ProjectInvite': {ProjectInvite: @ProjectInvite}
+ "../Project/ProjectEntityHandler": @ProjectEntityHandler = {}
'crypto': @Crypto
@projectId = ObjectId()
@sendingUserId = ObjectId()
@email = "user@example.com"
@userId = ObjectId()
+ @user =
+ _id: @userId
+ email: 'someone@example.com'
@inviteId = ObjectId()
@token = 'hnhteaosuhtaeosuahs'
@privileges = "readAndWrite"
+ @fakeInvite =
+ _id: @inviteId
+ email: @email
+ token: @token
+ sendingUserId: @sendingUserId
+ projectId: @projectId
+ privileges: @privileges
+ createdAt: new Date()
describe 'inviteToProject', ->
@@ -130,14 +146,6 @@ describe "CollaboratorsInviteHandler", ->
describe 'getInviteByToken', ->
beforeEach ->
- @fakeInvite =
- _id: @inviteId
- email: @email
- token: @token
- sendingUserId: @sendingUserId
- projectId: @projectId
- privileges: @privileges
- createdAt: new Date()
@ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
@call = (callback) =>
@CollaboratorsInviteHandler.getInviteByToken @projectId, @token, callback
@@ -189,3 +197,65 @@ describe "CollaboratorsInviteHandler", ->
expect(invite).to.not.be.instanceof Error
expect(invite).to.be.oneOf [null, undefined]
done()
+
+ describe 'acceptInvite', ->
+
+ beforeEach ->
+ @fakeProject =
+ _id: @projectId
+ collaberator_refs: []
+ readOnly_refs: []
+ @Project.findOne.callsArgWith(1, null, @fakeProject)
+ @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
+ @ContactManager.addContact = sinon.stub()
+ @Project.update.callsArgWith(2, null)
+ @ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon.stub().callsArgWith(1, null)
+ @ProjectInvite.remove.callsArgWith(1, null)
+ @call = (callback) =>
+ @CollaboratorsInviteHandler.acceptInvite @projectId, @inviteId, @token, @user, callback
+
+ describe 'when all goes well', ->
+
+ beforeEach ->
+
+ it 'should not produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.not.be.instanceof Error
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should have called Project.findOne', (done) ->
+ @call (err) =>
+ @Project.findOne.callCount.should.equal 1
+ @Project.findOne.calledWith({_id: @projectId}).should.equal true
+ done()
+
+ it 'should have called ProjectInvite.findOne', (done) ->
+ @call (err) =>
+ @ProjectInvite.findOne.callCount.should.equal 1
+ @ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId, token: @token}).should.equal true
+ done()
+
+ it 'should have called ContactManager.addContact', (done) ->
+ @call (err) =>
+ @ContactManager.addContact.callCount.should.equal 1
+ @ContactManager.addContact.calledWith(@sendingUserId, @userId).should.equal true
+ done()
+
+ it 'should have called Project.update, adding the user to callaberator_refs', (done) ->
+ @call (err) =>
+ @Project.update.callCount.should.equal 1
+ @Project.update.calledWith({_id: @projectId}, {$addToSet: {"collaberator_refs": @userId}}).should.equal true
+ done()
+
+ it 'should have called ProjectEntityHandler.flushProjectToThirdPartyDataStore', (done) ->
+ @call (err) =>
+ @ProjectEntityHandler.flushProjectToThirdPartyDataStore.callCount.should.equal 1
+ @ProjectEntityHandler.flushProjectToThirdPartyDataStore.calledWith(@projectId).should.equal true
+ done()
+
+ it 'should have called ProjectInvite.remove', (done) ->
+ @call (err) =>
+ @ProjectInvite.remove.callCount.should.equal 1
+ @ProjectInvite.remove.calledWith({_id: @inviteId}).should.equal true
+ done()
From ccf684cf07c20fe66cb64e5305bbcba517129c59 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 25 Jul 2016 10:19:20 +0100
Subject: [PATCH 024/378] test `acceptInvite`
---
.../CollaboratorsInviteHandlerTests.coffee | 142 +++++++++++++++++-
1 file changed, 141 insertions(+), 1 deletion(-)
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index 61513ec8fa..b4adff9a33 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -242,7 +242,7 @@ describe "CollaboratorsInviteHandler", ->
@ContactManager.addContact.calledWith(@sendingUserId, @userId).should.equal true
done()
- it 'should have called Project.update, adding the user to callaberator_refs', (done) ->
+ it 'should have called Project.update, adding the user to collaberator_refs', (done) ->
@call (err) =>
@Project.update.callCount.should.equal 1
@Project.update.calledWith({_id: @projectId}, {$addToSet: {"collaberator_refs": @userId}}).should.equal true
@@ -259,3 +259,143 @@ describe "CollaboratorsInviteHandler", ->
@ProjectInvite.remove.callCount.should.equal 1
@ProjectInvite.remove.calledWith({_id: @inviteId}).should.equal true
done()
+
+ describe 'when the invite is for readOnly access', ->
+
+ beforeEach ->
+ @fakeInvite.privileges = 'readOnly'
+ @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
+
+ it 'should not produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.not.be.instanceof Error
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should have called Project.update, adding the user to readOnly_refs', (done) ->
+ @call (err) =>
+ @Project.update.callCount.should.equal 1
+ @Project.update.calledWith({_id: @projectId}, {$addToSet: {"readOnly_refs": @userId}}).should.equal true
+ done()
+
+ describe 'when the invite is for an unknown access level', ->
+
+ beforeEach ->
+ @fakeInvite.privileges = 'some_crazy_permission'
+ @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ done()
+
+ it 'should not have called Project.update', (done) ->
+ @call (err) =>
+ @Project.update.callCount.should.equal 0
+ done()
+
+ it 'should not have called ProjectInvite.remove', (done) ->
+ @call (err) =>
+ @ProjectInvite.remove.callCount.should.equal 0
+ done()
+
+ describe 'when ProjectInvite.findOne does not find an invite', ->
+
+ beforeEach ->
+ @ProjectInvite.findOne.callsArgWith(1, null, null)
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ expect(err.name).to.equal "NotFoundError"
+ done()
+
+ it 'should not have called Project.update', (done) ->
+ @call (err) =>
+ @Project.update.callCount.should.equal 0
+ done()
+
+ it 'should not have called ProjectInvite.remove', (done) ->
+ @call (err) =>
+ @ProjectInvite.remove.callCount.should.equal 0
+ done()
+
+ describe 'when Project.findOne produces an error', ->
+
+ beforeEach ->
+ @Project.findOne.callsArgWith(1, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ done()
+
+ it 'should not have called Project.update', (done) ->
+ @call (err) =>
+ @Project.update.callCount.should.equal 0
+ done()
+
+ it 'should not have called ProjectInvite.remove', (done) ->
+ @call (err) =>
+ @ProjectInvite.remove.callCount.should.equal 0
+ done()
+
+ describe 'when ProjectInvite.findOne produces an error', ->
+
+ beforeEach ->
+ @ProjectInvite.findOne.callsArgWith(1, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ done()
+
+ it 'should not have called Project.update', (done) ->
+ @call (err) =>
+ @Project.update.callCount.should.equal 0
+ done()
+
+ it 'should not have called ProjectInvite.remove', (done) ->
+ @call (err) =>
+ @ProjectInvite.remove.callCount.should.equal 0
+ done()
+
+ describe 'when Project.update produces an error', ->
+
+ beforeEach ->
+ @Project.update.callsArgWith(2, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ done()
+
+ it 'should have called Project.update', (done) ->
+ @call (err) =>
+ @Project.update.callCount.should.equal 1
+ done()
+
+ it 'should not have called ProjectInvite.remove', (done) ->
+ @call (err) =>
+ @ProjectInvite.remove.callCount.should.equal 0
+ done()
+
+ describe 'when ProjectInvite.remove produces an error', ->
+
+ beforeEach ->
+ @ProjectInvite.remove.callsArgWith(1, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ done()
+
+ it 'should have called Project.update', (done) ->
+ @call (err) =>
+ @Project.update.callCount.should.equal 1
+ done()
+
+ it 'should have called ProjectInvite.remove', (done) ->
+ @call (err) =>
+ @ProjectInvite.remove.callCount.should.equal 1
+ done()
From 8dea179b016e9c0483b85c5bb69be43646afbb16 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 25 Jul 2016 10:33:43 +0100
Subject: [PATCH 025/378] Whitespace
---
.../ShareProjectModalController.coffee | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index 13d5faea9f..ea58bf9ebb 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -38,7 +38,7 @@ define [
else
# Must be a group
contact.display = contact.name
-
+
getCurrentMemberEmails = () ->
$scope.project.members.map (u) -> u.email
@@ -60,26 +60,26 @@ define [
$scope.inputs.contacts = []
$scope.state.error = null
$scope.state.inflight = true
-
+
currentMemberEmails = getCurrentMemberEmails()
do addNextMember = () ->
if members.length == 0 or !$scope.canAddCollaborators
$scope.state.inflight = false
$scope.$apply()
return
-
+
member = members.shift()
if !member.type? and member.display in currentMemberEmails
# Skip this existing member
return addNextMember()
-
+
if member.type == "user"
request = projectMembers.addMember(member.email, $scope.inputs.privileges)
else if member.type == "group"
request = projectMembers.addGroup(member.id, $scope.inputs.privileges)
else # Not an auto-complete object, so email == display
request = projectMembers.addMember(member.display, $scope.inputs.privileges)
-
+
request
.success (data) ->
if data.users?
@@ -88,7 +88,7 @@ define [
users = [data.user]
else
users = []
-
+
$scope.project.members.push users...
setTimeout () ->
# Give $scope a chance to update $scope.canAddCollaborators
@@ -98,8 +98,7 @@ define [
.error () ->
$scope.state.inflight = false
$scope.state.error = true
-
-
+
$timeout addMembers, 50 # Give email list a chance to update
$scope.removeMember = (member) ->
@@ -158,4 +157,4 @@ define [
$scope.cancel = () ->
$modalInstance.dismiss()
- ]
\ No newline at end of file
+ ]
From 73fed8b0bfedd7af66b6a0d5efb384cb8e8c166f Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 25 Jul 2016 11:17:47 +0100
Subject: [PATCH 026/378] Add a `getAllInvites` api endpoint
---
.../CollaboratorsInviteController.coffee | 9 ++++
.../CollaboratorsInviteHandler.coffee | 8 ++++
.../Collaborators/CollaboratorsRouter.coffee | 6 +++
.../CollaboratorsInviteControllerTests.coffee | 42 ++++++++++++++++++
.../CollaboratorsInviteHandlerTests.coffee | 44 +++++++++++++++++++
5 files changed, 109 insertions(+)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index c3c73a2165..ef23263805 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -7,6 +7,15 @@ logger = require('logger-sharelatex')
module.exports = CollaboratorsInviteController =
+ getAllInvites: (req, res, next) ->
+ projectId = req.params.Project_id
+ logger.log {projectId}, "getting all active invites for project"
+ CollaboratorsInviteHandler.getAllInvites projectId, (err, invites) ->
+ if err?
+ logger.err {projectId}, "error getting invites for project"
+ return next(err)
+ res.json({invites: invites})
+
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
email = req.body.email
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 597fe8c082..3ba77019b7 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -11,6 +11,14 @@ Crypto = require 'crypto'
module.exports = CollaboratorsInviteHandler =
+ getAllInvites: (projectId, callback=(err, invites)->) ->
+ logger.log {projectId}, "fetching invites from mongo"
+ ProjectInvite.find {projectId: projectId}, (err, invites) ->
+ if err?
+ logger.err {err, projectId}, "error getting invites from mongo"
+ return callback(err)
+ callback(null, invites)
+
inviteToProject: (projectId, sendingUserId, email, privileges, callback=(err,invite)->) ->
logger.log {projectId, sendingUserId, email, privileges}, "adding invite"
Crypto.randomBytes 24, (err, buffer) ->
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
index 9412f794ba..f67bb3ee6c 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
@@ -17,6 +17,12 @@ module.exports =
CollaboratorsInviteController.inviteToProject
)
+ webRouter.get(
+ '/project/:Project_id/invite',
+ AuthorizationMiddlewear.ensureUserCanAdminProject,
+ CollaboratorsInviteController.getAllInvites
+ )
+
webRouter.delete(
'/project/:Project_id/invite/:invite_id',
AuthorizationMiddlewear.ensureUserCanAdminProject,
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 6e1cb4f11a..08fcd2d54c 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -25,6 +25,48 @@ describe "CollaboratorsInviteController", ->
@project_id = "project-id-123"
@callback = sinon.stub()
+
+ describe 'getAllInvites', ->
+
+ beforeEach ->
+ @fakeInvites = [
+ {_id: ObjectId(), one: 1},
+ {_id: ObjectId(), two: 2}
+ ]
+ @req.params =
+ Project_id: @project_id
+ @res.json = sinon.stub()
+ @next = sinon.stub()
+
+ describe 'when all goes well', ->
+
+ beforeEach ->
+ @CollaboratorsInviteHandler.getAllInvites = sinon.stub().callsArgWith(1, null, @fakeInvites)
+ @CollaboratorsInviteController.getAllInvites @req, @res, @next
+
+ it 'should not produce an error', ->
+ @next.callCount.should.equal 0
+
+ it 'should produce a list of invite objects', ->
+ @res.json.callCount.should.equal 1
+ @res.json.calledWith({invites: @fakeInvites}).should.equal true
+
+ it 'should have called CollaboratorsInviteHandler.getAllInvites', ->
+ @CollaboratorsInviteHandler.getAllInvites.callCount.should.equal 1
+ @CollaboratorsInviteHandler.getAllInvites.calledWith(@project_id).should.equal true
+
+ describe 'when CollaboratorsInviteHandler.getAllInvites produces an error', ->
+
+ beforeEach ->
+ @CollaboratorsInviteHandler.getAllInvites = sinon.stub().callsArgWith(1, new Error('woops'))
+ @CollaboratorsInviteController.getAllInvites @req, @res, @next
+
+ it 'should produce an error', ->
+ @next.callCount.should.equal 1
+ @next.firstCall.args[0].should.be.instanceof Error
+
+ # # # #
+
describe 'inviteToProject', ->
beforeEach ->
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index b4adff9a33..1af35edc90 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -18,6 +18,7 @@ describe "CollaboratorsInviteHandler", ->
this
save: sinon.stub()
@findOne: sinon.stub()
+ @find: sinon.stub()
@remove: sinon.stub()
@Project = class Project
constructor: () ->
@@ -54,6 +55,49 @@ describe "CollaboratorsInviteHandler", ->
privileges: @privileges
createdAt: new Date()
+ describe 'getAllInvites', ->
+
+ beforeEach ->
+ @fakeInvites = [
+ {_id: ObjectId(), one: 1},
+ {_id: ObjectId(), two: 2}
+ ]
+ @ProjectInvite.find.callsArgWith(1, null, @fakeInvites)
+ @call = (callback) =>
+ @CollaboratorsInviteHandler.getAllInvites @projectId, callback
+
+ describe 'when all goes well', ->
+
+ beforeEach ->
+
+ it 'should not produce an error', (done) ->
+ @call (err, invites) =>
+ expect(err).to.not.be.instanceof Error
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should produce a list of invite objects', (done) ->
+ @call (err, invites) =>
+ expect(invites).to.not.be.oneOf [null, undefined]
+ expect(invites).to.deep.equal @fakeInvites
+ done()
+
+ it 'should have called ProjectInvite.find', (done) ->
+ @call (err, invites) =>
+ @ProjectInvite.find.callCount.should.equal 1
+ @ProjectInvite.find.calledWith({projectId: @projectId}).should.equal true
+ done()
+
+ describe 'when ProjectInvite.find produces an error', ->
+
+ beforeEach ->
+ @ProjectInvite.find.callsArgWith(1, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err, invites) =>
+ expect(err).to.be.instanceof Error
+ done()
+
describe 'inviteToProject', ->
beforeEach ->
From b3add65de1d25ad05308000a99c700fec5e2604d Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 25 Jul 2016 14:27:02 +0100
Subject: [PATCH 027/378] Start integrating invites into share frontend
---
.../web/app/views/project/editor/share.jade | 13 ++++++++
.../ShareProjectModalController.coffee | 31 +++++++++++++++++--
.../web/public/coffee/ide/share/index.coffee | 3 +-
.../ide/share/services/projectInvites.coffee | 30 ++++++++++++++++++
.../public/stylesheets/app/editor/share.less | 4 +--
5 files changed, 76 insertions(+), 5 deletions(-)
create mode 100644 services/web/public/coffee/ide/share/services/projectInvites.coffee
diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade
index ad3b2bd9ba..4d6bee100b 100644
--- a/services/web/app/views/project/editor/share.jade
+++ b/services/web/app/views/project/editor/share.jade
@@ -43,6 +43,19 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
ng-click="removeMember(member)"
)
i.fa.fa-times
+ .row.project-invite(ng-repeat="invite in state.invites")
+ .col-xs-8 {{ invite.email }} (invite)
+ .col-xs-3.text-right
+ span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")}
+ span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")}
+ .col-xs-1
+ a(
+ href
+ tooltip="#{translate('revoke_invite')}"
+ tooltip-placement="bottom"
+ ng-click="revokeInvite(invite)"
+ )
+ i.fa.fa-times
.row.invite-controls
form(ng-show="canAddCollaborators")
.small #{translate("share_with_your_collabs")}
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index ea58bf9ebb..05376b01b8 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -1,7 +1,7 @@
define [
"base"
], (App) ->
- App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, $modal, $http) ->
+ App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, projectInvites, $modal, $http) ->
$scope.inputs = {
privileges: "readAndWrite"
contacts: []
@@ -10,9 +10,11 @@ define [
error: null
inflight: false
startedFreeTrial: false
+ invites: []
}
$modalInstance.opened.then () ->
+ getOutstandingInvites()
$timeout () ->
$scope.$broadcast "open"
, 200
@@ -42,6 +44,16 @@ define [
getCurrentMemberEmails = () ->
$scope.project.members.map (u) -> u.email
+ getOutstandingInvites = (callback) ->
+ projectInvites.getInvites().then(
+ (response) ->
+ $scope.state.invites = response?.data?.invites
+ $scope.state.invites = [{_id: "wat", email: "user@example.com"}]
+ , (response) ->
+ console.error response
+ )
+ window._x = getOutstandingInvites
+
$scope.filterAutocompleteUsers = ($query) ->
currentMemberEmails = getCurrentMemberEmails()
return $scope.autocompleteContacts.filter (contact) ->
@@ -73,12 +85,13 @@ define [
# Skip this existing member
return addNextMember()
+ # TODO: double-check if member.type == 'user' needs to be an invite
if member.type == "user"
request = projectMembers.addMember(member.email, $scope.inputs.privileges)
else if member.type == "group"
request = projectMembers.addGroup(member.id, $scope.inputs.privileges)
else # Not an auto-complete object, so email == display
- request = projectMembers.addMember(member.display, $scope.inputs.privileges)
+ request = projectInvites.sendInvite(member.display, $scope.inputs.privileges)
request
.success (data) ->
@@ -115,6 +128,20 @@ define [
$scope.state.inflight = false
$scope.state.error = "Sorry, something went wrong :("
+ $scope.revokeInvite = (invite) ->
+ $scope.state.error = null
+ $scope.state.inflight = true
+ projectInvites
+ .revokeInvite(invite._id)
+ .success () ->
+ $scope.state.inflight = false
+ index = $scope.state.invites.indexOf(invite)
+ return if index == -1
+ $scope.state.invites.splice(index, 1)
+ .error () ->
+ $scope.state.inflight = false
+ $scope.state.error = "Sorry, something went wrong :("
+
$scope.openMakePublicModal = () ->
$modal.open {
templateUrl: "makePublicModalTemplate"
diff --git a/services/web/public/coffee/ide/share/index.coffee b/services/web/public/coffee/ide/share/index.coffee
index 545a145be3..13b2bdbcfd 100644
--- a/services/web/public/coffee/ide/share/index.coffee
+++ b/services/web/public/coffee/ide/share/index.coffee
@@ -2,4 +2,5 @@ define [
"ide/share/controllers/ShareController"
"ide/share/controllers/ShareProjectModalController"
"ide/share/services/projectMembers"
-], () ->
\ No newline at end of file
+ "ide/share/services/projectInvites"
+], () ->
diff --git a/services/web/public/coffee/ide/share/services/projectInvites.coffee b/services/web/public/coffee/ide/share/services/projectInvites.coffee
new file mode 100644
index 0000000000..f453792574
--- /dev/null
+++ b/services/web/public/coffee/ide/share/services/projectInvites.coffee
@@ -0,0 +1,30 @@
+define [
+ "base"
+], (App) ->
+ App.factory "projectInvites", ["ide", "$http", (ide, $http) ->
+ return {
+
+ sendInvite: (email, privileges) ->
+ $http.post("/project/#{ide.project_id}/invite", {
+ email: email
+ privileges: privileges
+ _csrf: window.csrfToken
+ })
+
+ revokeInvite: (inviteId) ->
+ $http({
+ url: "/project/#{ide.project_id}/invite/#{inviteId}"
+ method: "DELETE"
+ headers:
+ "X-Csrf-Token": window.csrfToken
+ })
+
+ getInvites: () ->
+ $http.get("/project/#{ide.project_id}/invite", {
+ json: true
+ headers:
+ "X-Csrf-Token": window.csrfToken
+ })
+
+ }
+ ]
diff --git a/services/web/public/stylesheets/app/editor/share.less b/services/web/public/stylesheets/app/editor/share.less
index 77ed2e0df6..cd06a15313 100644
--- a/services/web/public/stylesheets/app/editor/share.less
+++ b/services/web/public/stylesheets/app/editor/share.less
@@ -6,7 +6,7 @@
font-size: 1rem;
}
- .project-member, .public-access-level {
+ .project-member, .project-invite, .public-access-level {
padding: (@line-height-computed / 2) 0;
border-bottom: 1px solid @gray-lighter;
font-size: 14px;
@@ -19,7 +19,7 @@
padding-bottom: @line-height-computed;
}
- .project-member {
+ .project-member, .project-invite {
&:hover {
background-color: @gray-lightest;
}
From 16dcbe2cd434200a80f885fb886051e02881e894 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 25 Jul 2016 15:07:14 +0100
Subject: [PATCH 028/378] WIP: wire up share-modal frontend to invite system
---
.../CollaboratorsInviteController.coffee | 4 ++--
.../app/coffee/models/ProjectInvite.coffee | 20 ++++++++++++-------
.../ShareProjectModalController.coffee | 15 +++++---------
.../ide/share/services/projectMembers.coffee | 5 ++---
.../CollaboratorsInviteControllerTests.coffee | 8 +++-----
5 files changed, 25 insertions(+), 27 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index ef23263805..2b64f70887 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -25,7 +25,7 @@ module.exports = CollaboratorsInviteController =
return next(error) if error?
if !allowed
logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
- return res.json {inviteId: null}
+ return res.json {invite: null}
{email, privileges} = req.body
email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
if !email? or email == ""
@@ -36,7 +36,7 @@ module.exports = CollaboratorsInviteController =
logger.err {projectId, email, sendingUserId}, "error creating project invite"
return next(err)
logger.log {projectId, email, sendingUserId}, "invite created"
- return res.json {inviteId: invite._id}
+ return res.json {invite: invite}
revokeInvite: (req, res, next) ->
projectId = req.params.Project_id
diff --git a/services/web/app/coffee/models/ProjectInvite.coffee b/services/web/app/coffee/models/ProjectInvite.coffee
index decea99f46..5e46408176 100644
--- a/services/web/app/coffee/models/ProjectInvite.coffee
+++ b/services/web/app/coffee/models/ProjectInvite.coffee
@@ -9,13 +9,19 @@ ObjectId = Schema.ObjectId
THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30
-ProjectInviteSchema = new Schema
- email: String
- token: String
- sendingUserId: ObjectId
- projectId: ObjectId
- privileges: String
- createdAt: {type: Date, default: Date.now, index: {expiresAfterSeconds: THIRTY_DAYS_IN_SECONDS}}
+ProjectInviteSchema = new Schema(
+ {
+ email: String
+ token: String
+ sendingUserId: ObjectId
+ projectId: ObjectId
+ privileges: String
+ createdAt: {type: Date, default: Date.now, index: {expiresAfterSeconds: THIRTY_DAYS_IN_SECONDS}}
+ },
+ {
+ collection: 'projectInvites'
+ }
+)
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10)
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index 05376b01b8..ac2bc431ba 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -48,7 +48,6 @@ define [
projectInvites.getInvites().then(
(response) ->
$scope.state.invites = response?.data?.invites
- $scope.state.invites = [{_id: "wat", email: "user@example.com"}]
, (response) ->
console.error response
)
@@ -86,8 +85,9 @@ define [
return addNextMember()
# TODO: double-check if member.type == 'user' needs to be an invite
+ console.log ">> inviting", member
if member.type == "user"
- request = projectMembers.addMember(member.email, $scope.inputs.privileges)
+ request = projectInvites.sendInvite(member.email, $scope.inputs.privileges)
else if member.type == "group"
request = projectMembers.addGroup(member.id, $scope.inputs.privileges)
else # Not an auto-complete object, so email == display
@@ -95,14 +95,9 @@ define [
request
.success (data) ->
- if data.users?
- users = data.users
- else if data.user?
- users = [data.user]
- else
- users = []
-
- $scope.project.members.push users...
+ if data.invite
+ invite = data.invite
+ $scope.state.invites.push invite
setTimeout () ->
# Give $scope a chance to update $scope.canAddCollaborators
# with new collaborator information.
diff --git a/services/web/public/coffee/ide/share/services/projectMembers.coffee b/services/web/public/coffee/ide/share/services/projectMembers.coffee
index a51ea63e99..4bc69e814f 100644
--- a/services/web/public/coffee/ide/share/services/projectMembers.coffee
+++ b/services/web/public/coffee/ide/share/services/projectMembers.coffee
@@ -17,13 +17,12 @@ define [
privileges: privileges
_csrf: window.csrfToken
})
-
+
addGroup: (group_id, privileges) ->
$http.post("/project/#{ide.project_id}/group", {
group_id: group_id
privileges: privileges
_csrf: window.csrfToken
})
-
}
- ]
\ No newline at end of file
+ ]
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 08fcd2d54c..432ace7052 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -65,8 +65,6 @@ describe "CollaboratorsInviteController", ->
@next.callCount.should.equal 1
@next.firstCall.args[0].should.be.instanceof Error
- # # # #
-
describe 'inviteToProject', ->
beforeEach ->
@@ -101,7 +99,7 @@ describe "CollaboratorsInviteController", ->
it 'should produce json response', ->
@res.json.callCount.should.equal 1
- ({inviteId: @invite._id}).should.deep.equal(@res.json.firstCall.args[0])
+ ({invite: @invite}).should.deep.equal(@res.json.firstCall.args[0])
it 'should have called canAddXCollaborators', ->
@LimitationsManager.canAddXCollaborators.callCount.should.equal 1
@@ -117,9 +115,9 @@ describe "CollaboratorsInviteController", ->
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false)
@CollaboratorsInviteController.inviteToProject @req, @res, @next
- it 'should produce json response without inviteId', ->
+ it 'should produce json response without an invite', ->
@res.json.callCount.should.equal 1
- ({inviteId: null}).should.deep.equal(@res.json.firstCall.args[0])
+ ({invite: null}).should.deep.equal(@res.json.firstCall.args[0])
it 'should not have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0
From b898c62e919c82b34c5f6cfb8d5d02312dc6df68 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 25 Jul 2016 16:14:41 +0100
Subject: [PATCH 029/378] Add appropriate query strings to the end of invite
link
---
.../Collaborators/CollaboratorsEmailHandler.coffee | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
index f5d08054d4..dcaaf5fcde 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
@@ -36,6 +36,11 @@ module.exports =
replyTo: project.owner_ref.email
project:
name: project.name
- inviteUrl: "#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}"
+ inviteUrl: "#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [
+ "project_name=#{encodeURIComponent(project.name)}"
+ "user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
+ "r=#{project.owner_ref.referal_id}" # Referal
+ "rs=ci" # referral source = collaborator invite
+ ].join("&")
owner: project.owner_ref
EmailHandler.sendEmail "projectInvite", emailOptions, callback
From 41755212f0265c42efc74e372cba0bd55b8a9ab1 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 26 Jul 2016 09:06:21 +0100
Subject: [PATCH 030/378] Update the register page with new message
---
services/web/app/views/user/register.jade | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/services/web/app/views/user/register.jade b/services/web/app/views/user/register.jade
index 96db403f1c..2a5ab707a6 100644
--- a/services/web/app/views/user/register.jade
+++ b/services/web/app/views/user/register.jade
@@ -6,8 +6,12 @@ block content
.row
.registration_message
if sharedProjectData.user_first_name !== undefined
- h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:sharedProjectData.project_name})}
- div #{translate("join_sl_to_view_project")}.
+ h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:""})}
+ em #{sharedProjectData.project_name}
+ div
+ | #{translate("join_sl_to_view_project")}.
+ div
+ | #{translate("if_you_are_registered")},
a(href="/login") #{translate("login_here")}
else if newTemplateData.templateName !== undefined
h1 #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})}
From 2dede5f7934b31a63fac4a0fba9e7e379c3a5941 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 26 Jul 2016 11:46:41 +0100
Subject: [PATCH 031/378] WIP: Working "accept invite" page
---
.../CollaboratorsInviteController.coffee | 16 ++++++++--
.../CollaboratorsInviteHandler.coffee | 2 +-
.../web/app/views/project/invite/show.jade | 31 ++++++++++++++++++-
.../web/public/stylesheets/app/invite.less | 5 +++
services/web/public/stylesheets/style.less | 2 +-
5 files changed, 50 insertions(+), 6 deletions(-)
create mode 100644 services/web/public/stylesheets/app/invite.less
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 2b64f70887..a1bb0bdaf5 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -1,6 +1,8 @@
ProjectGetter = require "../Project/ProjectGetter"
LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
+Project = require("../../models/Project").Project
+User = require("../../models/User").User
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
mimelib = require("mimelib")
logger = require('logger-sharelatex')
@@ -60,11 +62,19 @@ module.exports = CollaboratorsInviteController =
if !invite
logger.log {projectId, token}, "no invite found for token"
return res.render "project/invite/not-valid"
- res.render "project/invite/show", {invite}
+ Project.findOne {_id: projectId}, {owner_ref: 1, name: 1}, (err, project) ->
+ if err?
+ logger.err {err, projectId}, "error getting project"
+ return callback(err)
+ User.findOne {_id: project.owner_ref}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
+ if err?
+ logger.err {err, projectId}, "error getting project owner"
+ return callback(err)
+ res.render "project/invite/show", {invite, project, owner}
acceptInvite: (req, res, next) ->
projectId = req.params.Project_id
- inviteId = req.params.inviteId
+ inviteId = req.params.invite_id
{token} = req.body
currentUser = req.session.user
logger.log {projectId, inviteId}, "accepting invite"
@@ -72,4 +82,4 @@ module.exports = CollaboratorsInviteController =
if err?
logger.err {projectId, inviteId}, "error accepting invite by token"
return next(err)
- res.sendStatus(201)
+ res.redirect "/project/#{projectId}"
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 3ba77019b7..161ddf0454 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -75,7 +75,7 @@ module.exports = CollaboratorsInviteHandler =
return callback(err)
if !invite
err = new Errors.NotFoundError("no matching invite found")
- logger.log {err, projectId, inviteId}, "no matching invite found"
+ logger.log {err, projectId, inviteId, tokenString}, "no matching invite found"
return callback(err)
# do the thing
diff --git a/services/web/app/views/project/invite/show.jade b/services/web/app/views/project/invite/show.jade
index 705cea4fe8..012af3f85e 100644
--- a/services/web/app/views/project/invite/show.jade
+++ b/services/web/app/views/project/invite/show.jade
@@ -1 +1,30 @@
-h1 Invite TEST
\ No newline at end of file
+extends ../../layout
+
+block content
+ .content.content-alt
+ .container
+ .row
+ .col-md-8.col-md-offset-2
+ .card.project-invite-accept
+ .page-header.text-centered
+ h1 #{translate("user_wants_you_to_see_project", {username:owner.first_name, projectname:""})}
+ em #{project.name}
+ .row.text-center
+ .col-md-12
+ p
+ | You are accepting this invite as
+ em #{user.email}
+ .row
+ .col-md-12
+ form.form(
+ name="acceptForm",
+ method="POST",
+ action="/project/#{invite.projectId}/invite/#{invite._id}/accept"
+ )
+ input(name='_csrf', type='hidden', value=csrfToken)
+ input(name='token', type='hidden', value="#{invite.token}")
+ .form-group.text-center
+ button.btn.btn-lg.btn-primary(type="submit")
+ | Accept Invite
+ .form-group.text-center
+
\ No newline at end of file
diff --git a/services/web/public/stylesheets/app/invite.less b/services/web/public/stylesheets/app/invite.less
new file mode 100644
index 0000000000..4354aa79c4
--- /dev/null
+++ b/services/web/public/stylesheets/app/invite.less
@@ -0,0 +1,5 @@
+.project-invite-accept {
+ form {
+ padding-top: 15px;
+ }
+}
\ No newline at end of file
diff --git a/services/web/public/stylesheets/style.less b/services/web/public/stylesheets/style.less
index b02891425c..0cf0939cbf 100755
--- a/services/web/public/stylesheets/style.less
+++ b/services/web/public/stylesheets/style.less
@@ -76,8 +76,8 @@
@import "app/translations.less";
@import "app/contact-us.less";
@import "app/sprites.less";
+@import "app/invite.less";
@import "../js/libs/pdfListView/TextLayer.css";
@import "../js/libs/pdfListView/AnnotationsLayer.css";
@import "../js/libs/pdfListView/HighlightsLayer.css";
-
From 367b138cae89fea90a50692f61d05f381b7a18d3 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 26 Jul 2016 12:09:58 +0100
Subject: [PATCH 032/378] fix failing tests
---
.../CollaboratorsInviteControllerTests.coffee | 34 +++++++++++++++----
1 file changed, 27 insertions(+), 7 deletions(-)
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 432ace7052..7e351fa30c 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -11,6 +11,13 @@ ObjectId = require("mongojs").ObjectId
describe "CollaboratorsInviteController", ->
beforeEach ->
+ @User = class User
+ constructor: (options={}) -> this
+ @findOne: sinon.stub()
+ @Project = class Project
+ constructor: () ->
+ this
+ @findOne: sinon.stub()
@CollaboratorsInviteController = SandboxedModule.require modulePath, requires:
"../Project/ProjectGetter": @ProjectGetter = {}
"./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
@@ -18,6 +25,8 @@ describe "CollaboratorsInviteController", ->
'../Subscription/LimitationsManager' : @LimitationsManager = {}
'../Project/ProjectEditorHandler' : @ProjectEditorHandler = {}
'../User/UserGetter': @UserGetter = {}
+ '../../models/Project': {Project: @Project}
+ '../../models/User': {User; @User}
'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
@res = new MockResponse()
@req = new MockRequest()
@@ -169,10 +178,21 @@ describe "CollaboratorsInviteController", ->
_id: ObjectId(),
token: "htnseuthaouse",
sendingUserId: ObjectId(),
- projectId: @projectId,
+ projectId: @project_id,
targetEmail: 'user@example.com'
createdAt: new Date(),
}
+ @fakeProject =
+ _id: @project_id
+ name: "some project"
+ owner_ref: @invite.sendingUserId
+ @owner =
+ _id: @fakeProject.owner_ref
+ first_name: "John"
+ last_name: "Doe"
+ email: "john@example.com"
+ @Project.findOne.callsArgWith(2, null, @fakeProject)
+ @User.findOne.callsArgWith(2, null, @owner)
@CollaboratorsInviteHandler.getInviteByToken = sinon.stub().callsArgWith(2, null, @invite)
@callback = sinon.stub()
@next = sinon.stub()
@@ -277,7 +297,7 @@ describe "CollaboratorsInviteController", ->
@req.body =
token: "thsueothaueotauahsuet"
@res.render = sinon.stub()
- @res.sendStatus = sinon.stub()
+ @res.redirect = sinon.stub()
@CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, null)
@callback = sinon.stub()
@next = sinon.stub()
@@ -287,9 +307,9 @@ describe "CollaboratorsInviteController", ->
beforeEach ->
@CollaboratorsInviteController.acceptInvite @req, @res, @next
- it 'should produce a 201 response', ->
- @res.sendStatus.callCount.should.equal 1
- @res.sendStatus.calledWith(201).should.equal true
+ it 'should redirect to project page', () ->
+ @res.redirect.callCount.should.equal 1
+ @res.redirect.calledWith("/project/#{@project_id}").should.equal true
it 'should have called acceptInvite', ->
@CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1
@@ -301,8 +321,8 @@ describe "CollaboratorsInviteController", ->
@CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, @err)
@CollaboratorsInviteController.acceptInvite @req, @res, @next
- it 'should not produce a 201 response', ->
- @res.sendStatus.callCount.should.equal 0
+ it 'should not redirect to project page', ->
+ @res.redirect.callCount.should.equal 0
it 'should call next with the error', ->
@next.callCount.should.equal 1
From 855cc28483e11e7dc3da0bdfd9ece10ab7e56f11 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 26 Jul 2016 14:14:14 +0100
Subject: [PATCH 033/378] Finish adding project and owner details to the
accept-invite page
---
.../CollaboratorsInviteController.coffee | 15 ++-
.../CollaboratorsInviteControllerTests.coffee | 100 ++++++++++++++++++
2 files changed, 111 insertions(+), 4 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index a1bb0bdaf5..4dcf5134f5 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -58,18 +58,25 @@ module.exports = CollaboratorsInviteController =
if err?
logger.err {projectId, token}, "error getting invite by token"
return next(err)
- # TODO: render a not-valid view instead
+ _renderInvalidPage = () ->
+ res.render "project/invite/not-valid", {invite}
if !invite
logger.log {projectId, token}, "no invite found for token"
- return res.render "project/invite/not-valid"
+ return _renderInvalidPage()
Project.findOne {_id: projectId}, {owner_ref: 1, name: 1}, (err, project) ->
if err?
logger.err {err, projectId}, "error getting project"
- return callback(err)
+ return next(err)
+ if !project
+ logger.log {projectId}, "no project found"
+ return _renderInvalidPage()
User.findOne {_id: project.owner_ref}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
if err?
logger.err {err, projectId}, "error getting project owner"
- return callback(err)
+ return next(err)
+ if !owner
+ logger.log {projectId}, "no project owner found"
+ return _renderInvalidPage()
res.render "project/invite/show", {invite, project, owner}
acceptInvite: (req, res, next) ->
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 7e351fa30c..af10cb48de 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -213,6 +213,14 @@ describe "CollaboratorsInviteController", ->
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+ it 'should call Project.findOne', ->
+ @Project.findOne.callCount.should.equal 1
+ @Project.findOne.calledWith({_id: @project_id}).should.equal true
+
+ it 'should call User.findOne', ->
+ @User.findOne.callCount.should.equal 1
+ @User.findOne.calledWith({_id: @fakeProject.owner_ref}).should.equal true
+
describe 'when the getInviteByToken produces an error', ->
beforeEach ->
@@ -227,6 +235,12 @@ describe "CollaboratorsInviteController", ->
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+ it 'should not call Project.findOne', ->
+ @Project.findOne.callCount.should.equal 0
+
+ it 'should not call User.findOne', ->
+ @User.findOne.callCount.should.equal 0
+
describe 'when the getInviteByToken does not produce an invite', ->
beforeEach ->
@@ -243,6 +257,92 @@ describe "CollaboratorsInviteController", ->
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+ it 'should not call Project.findOne', ->
+ @Project.findOne.callCount.should.equal 0
+
+ it 'should not call User.findOne', ->
+ @User.findOne.callCount.should.equal 0
+
+ describe 'when Project.findOne produces an error', ->
+
+ beforeEach ->
+ @Project.findOne.callsArgWith(2, new Error('woops'))
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
+
+ it 'should produce an error', ->
+ @next.callCount.should.equal 1
+ expect(@next.firstCall.args[0]).to.be.instanceof Error
+
+ it 'should call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+
+ it 'should call Project.findOne', ->
+ @Project.findOne.callCount.should.equal 1
+ @Project.findOne.calledWith({_id: @project_id}).should.equal true
+
+ it 'should not call User.findOne', ->
+ @User.findOne.callCount.should.equal 0
+
+ describe 'when Project.findOne does not find a project', ->
+
+ beforeEach ->
+ @Project.findOne.callsArgWith(2, null, null)
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
+
+ it 'should render the not-valid view template', ->
+ @res.render.callCount.should.equal 1
+ @res.render.calledWith('project/invite/not-valid').should.equal true
+
+ it 'should not call next', ->
+ @next.callCount.should.equal 0
+
+ it 'should call Project.findOne', ->
+ @Project.findOne.callCount.should.equal 1
+ @Project.findOne.calledWith({_id: @project_id}).should.equal true
+
+ it 'should not call User.findOne', ->
+ @User.findOne.callCount.should.equal 0
+
+ describe 'when User.findOne produces an error', ->
+
+ beforeEach ->
+ @User.findOne.callsArgWith(2, new Error('woops'))
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
+
+ it 'should produce an error', ->
+ @next.callCount.should.equal 1
+ expect(@next.firstCall.args[0]).to.be.instanceof Error
+
+ it 'should call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+
+ it 'should call Project.findOne', ->
+ @Project.findOne.callCount.should.equal 1
+ @Project.findOne.calledWith({_id: @project_id}).should.equal true
+
+ it 'should call User.findOne', ->
+ @User.findOne.callCount.should.equal 1
+
+ describe 'when User.findOne does not find a user', ->
+
+ beforeEach ->
+ @User.findOne.callsArgWith(2, null, null)
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
+
+ it 'should render the not-valid view template', ->
+ @res.render.callCount.should.equal 1
+ @res.render.calledWith('project/invite/not-valid').should.equal true
+
+ it 'should not call next', ->
+ @next.callCount.should.equal 0
+
+ it 'should call Project.findOne', ->
+ @Project.findOne.callCount.should.equal 1
+ @Project.findOne.calledWith({_id: @project_id}).should.equal true
+
+ it 'should call User.findOne', ->
+ @User.findOne.callCount.should.equal 1
+
describe "revokeInvite", ->
beforeEach ->
From 827629a74a52bfaf006637bb5f8c6ff2a411dcff Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 27 Jul 2016 10:10:44 +0100
Subject: [PATCH 034/378] Invalid-invite page, and re-jigg the share modal
---
.../CollaboratorsInviteController.coffee | 2 +-
.../web/app/views/project/editor/share.jade | 14 ++++++++------
.../app/views/project/invite/not-valid.jade | 19 ++++++++++++++++++-
.../web/public/stylesheets/app/invite.less | 8 ++++++++
4 files changed, 35 insertions(+), 8 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 4dcf5134f5..aa886fdfff 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -59,7 +59,7 @@ module.exports = CollaboratorsInviteController =
logger.err {projectId, token}, "error getting invite by token"
return next(err)
_renderInvalidPage = () ->
- res.render "project/invite/not-valid", {invite}
+ res.render "project/invite/not-valid"
if !invite
logger.log {projectId, token}, "no invite found for token"
return _renderInvalidPage()
diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade
index 4d6bee100b..cf35918a98 100644
--- a/services/web/app/views/project/editor/share.jade
+++ b/services/web/app/views/project/editor/share.jade
@@ -27,12 +27,12 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
) #{translate("make_private")}
.row.project-member
.col-xs-8 {{ project.owner.email }}
- .text-right(
+ .text-left(
ng-class="{'col-xs-3': project.members.length > 0, 'col-xs-4': project.members.length == 0}"
) #{translate("owner")}
.row.project-member(ng-repeat="member in project.members")
.col-xs-8 {{ member.email }}
- .col-xs-3.text-right
+ .col-xs-3.text-left
span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")}
span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")}
.col-xs-1
@@ -44,10 +44,12 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
)
i.fa.fa-times
.row.project-invite(ng-repeat="invite in state.invites")
- .col-xs-8 {{ invite.email }} (invite)
- .col-xs-3.text-right
- span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")}
- span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")}
+ .col-xs-8 {{ invite.email }}
+ span.label.label-primary pending
+ .col-xs-3.text-left
+ // todo: get invite privileges
+ span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")}
+ span(ng-show="invite.privileges == 'readOnly'") #{translate("read_only")}
.col-xs-1
a(
href
diff --git a/services/web/app/views/project/invite/not-valid.jade b/services/web/app/views/project/invite/not-valid.jade
index 6eb6306b16..d01cb82c81 100644
--- a/services/web/app/views/project/invite/not-valid.jade
+++ b/services/web/app/views/project/invite/not-valid.jade
@@ -1 +1,18 @@
-h1 Invite Not Valid TEST
\ No newline at end of file
+extends ../../layout
+
+block content
+ .content.content-alt
+ .container
+ .row
+ .col-md-8.col-md-offset-2
+ .card.project-invite-invalid
+ .page-header.text-centered
+ h1 This is not a valid project invite
+ .row.text-center
+ .col-md-12
+ p
+ | The invite may have expired. Please contact the project owner.
+ .row.text-center.actions
+ .col-md-12
+ a.btn.btn-info(href="/project") #{translate("back_to_your_projects")}
+
\ No newline at end of file
diff --git a/services/web/public/stylesheets/app/invite.less b/services/web/public/stylesheets/app/invite.less
index 4354aa79c4..aaa6f08ab4 100644
--- a/services/web/public/stylesheets/app/invite.less
+++ b/services/web/public/stylesheets/app/invite.less
@@ -2,4 +2,12 @@
form {
padding-top: 15px;
}
+ margin-bottom: 30px;
+}
+
+.project-invite-invalid {
+ .actions {
+ padding-top: 15px;
+ }
+ margin-bottom: 30px;
}
\ No newline at end of file
From 78948251a11249d7e75d1a6f0a24a53e0854f682 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 27 Jul 2016 10:28:01 +0100
Subject: [PATCH 035/378] Change the Close button color
---
services/web/app/views/project/editor/share.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade
index cf35918a98..7002266b70 100644
--- a/services/web/app/views/project/editor/share.jade
+++ b/services/web/app/views/project/editor/share.jade
@@ -138,7 +138,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
.modal-footer-left
i.fa.fa-refresh.fa-spin(ng-show="state.inflight")
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
- button.btn.btn-primary(
+ button.btn.btn-default(
ng-click="done()"
) #{translate("close")}
From 6f39813a56d7be23d0d778668276b7d68e77c799 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 27 Jul 2016 10:56:22 +0100
Subject: [PATCH 036/378] Add translations
---
services/web/app/views/project/editor/share.jade | 2 +-
services/web/app/views/project/invite/not-valid.jade | 4 ++--
services/web/app/views/project/invite/show.jade | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade
index 7002266b70..8de9af28d2 100644
--- a/services/web/app/views/project/editor/share.jade
+++ b/services/web/app/views/project/editor/share.jade
@@ -45,7 +45,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
i.fa.fa-times
.row.project-invite(ng-repeat="invite in state.invites")
.col-xs-8 {{ invite.email }}
- span.label.label-primary pending
+ span.label.label-primary #{translate("pending")}
.col-xs-3.text-left
// todo: get invite privileges
span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")}
diff --git a/services/web/app/views/project/invite/not-valid.jade b/services/web/app/views/project/invite/not-valid.jade
index d01cb82c81..6ce66e69f4 100644
--- a/services/web/app/views/project/invite/not-valid.jade
+++ b/services/web/app/views/project/invite/not-valid.jade
@@ -7,11 +7,11 @@ block content
.col-md-8.col-md-offset-2
.card.project-invite-invalid
.page-header.text-centered
- h1 This is not a valid project invite
+ h1 #{translate("invite_not_valid")}
.row.text-center
.col-md-12
p
- | The invite may have expired. Please contact the project owner.
+ | #{translate("invite_not_valid_description")}
.row.text-center.actions
.col-md-12
a.btn.btn-info(href="/project") #{translate("back_to_your_projects")}
diff --git a/services/web/app/views/project/invite/show.jade b/services/web/app/views/project/invite/show.jade
index 012af3f85e..158e5309f2 100644
--- a/services/web/app/views/project/invite/show.jade
+++ b/services/web/app/views/project/invite/show.jade
@@ -12,7 +12,7 @@ block content
.row.text-center
.col-md-12
p
- | You are accepting this invite as
+ | #{translate("accepting_invite_as")}
em #{user.email}
.row
.col-md-12
@@ -25,6 +25,6 @@ block content
input(name='token', type='hidden', value="#{invite.token}")
.form-group.text-center
button.btn.btn-lg.btn-primary(type="submit")
- | Accept Invite
+ | #{translate("accept_invite")}
.form-group.text-center
\ No newline at end of file
From e1af171534ba74e61ce7efe7ba84fd35e6d70509 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 27 Jul 2016 11:07:26 +0100
Subject: [PATCH 037/378] Add a dot to end of sentence.
---
services/web/app/views/project/invite/not-valid.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/project/invite/not-valid.jade b/services/web/app/views/project/invite/not-valid.jade
index 6ce66e69f4..4d6b7f869e 100644
--- a/services/web/app/views/project/invite/not-valid.jade
+++ b/services/web/app/views/project/invite/not-valid.jade
@@ -11,7 +11,7 @@ block content
.row.text-center
.col-md-12
p
- | #{translate("invite_not_valid_description")}
+ | #{translate("invite_not_valid_description")}.
.row.text-center.actions
.col-md-12
a.btn.btn-info(href="/project") #{translate("back_to_your_projects")}
From 46ec17f2c41659e4c84ff40cdad18f50e3ae08cd Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 27 Jul 2016 13:51:52 +0100
Subject: [PATCH 038/378] Add redir query string to login link
---
services/web/app/views/user/register.jade | 2 +-
.../ide/share/controllers/ShareProjectModalController.coffee | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/services/web/app/views/user/register.jade b/services/web/app/views/user/register.jade
index 2a5ab707a6..5a1196b6a6 100644
--- a/services/web/app/views/user/register.jade
+++ b/services/web/app/views/user/register.jade
@@ -12,7 +12,7 @@ block content
| #{translate("join_sl_to_view_project")}.
div
| #{translate("if_you_are_registered")},
- a(href="/login") #{translate("login_here")}
+ a(href="/login?redir=#{getReqQueryParam('redir')}") #{translate("login_here")}
else if newTemplateData.templateName !== undefined
h1 #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})}
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index ac2bc431ba..b9567f46dd 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -85,7 +85,6 @@ define [
return addNextMember()
# TODO: double-check if member.type == 'user' needs to be an invite
- console.log ">> inviting", member
if member.type == "user"
request = projectInvites.sendInvite(member.email, $scope.inputs.privileges)
else if member.type == "group"
From 62d544ccfc77827085f12188f2fc13eb81817507 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 27 Jul 2016 15:28:22 +0100
Subject: [PATCH 039/378] Redirect to project if user is already member.
If invite is missing, and current user is already a member
of the project, then just redirect to the project page
---
.../CollaboratorsInviteController.coffee | 35 +++--
.../CollaboratorsInviteControllerTests.coffee | 128 +++++++++++-------
2 files changed, 103 insertions(+), 60 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index aa886fdfff..5618da7db7 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -54,29 +54,40 @@ module.exports = CollaboratorsInviteController =
projectId = req.params.Project_id
token = req.params.token
currentUser = req.session.user
- CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) ->
+ _renderInvalidPage = () ->
+ res.render "project/invite/not-valid"
+ # get the target project
+ Project.findOne {_id: projectId}, {owner_ref: 1, name: 1, collaberator_refs: 1, readOnly_refs: 1}, (err, project) ->
if err?
- logger.err {projectId, token}, "error getting invite by token"
+ logger.err {err, projectId}, "error getting project"
return next(err)
- _renderInvalidPage = () ->
- res.render "project/invite/not-valid"
- if !invite
- logger.log {projectId, token}, "no invite found for token"
+ if !project
+ logger.log {projectId}, "no project found"
return _renderInvalidPage()
- Project.findOne {_id: projectId}, {owner_ref: 1, name: 1}, (err, project) ->
+ # get the invite
+ CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) ->
if err?
- logger.err {err, projectId}, "error getting project"
+ logger.err {projectId, token}, "error getting invite by token"
return next(err)
- if !project
- logger.log {projectId}, "no project found"
- return _renderInvalidPage()
- User.findOne {_id: project.owner_ref}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
+ # check if invite is gone
+ if !invite
+ logger.log {projectId, token}, "no invite found for this token"
+ # check if user is already a member of the project, redirect to project if so
+ allMembers = (project.collaberator_refs || []).concat(project.readOnly_refs || []).map((oid) -> oid.toString())
+ if currentUser._id in allMembers
+ logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting"
+ return res.redirect "/project/#{projectId}"
+ else
+ return _renderInvalidPage()
+ # check the user who sent the invite exists
+ User.findOne {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
if err?
logger.err {err, projectId}, "error getting project owner"
return next(err)
if !owner
logger.log {projectId}, "no project owner found"
return _renderInvalidPage()
+ # finally render the invite
res.render "project/invite/show", {invite, project, owner}
acceptInvite: (req, res, next) ->
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index af10cb48de..3de79d9fad 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -173,6 +173,7 @@ describe "CollaboratorsInviteController", ->
@req.session =
user: _id: @current_user_id = "current-user-id"
@res.render = sinon.stub()
+ @res.redirect = sinon.stub()
@res.sendStatus = sinon.stub()
@invite = {
_id: ObjectId(),
@@ -186,6 +187,8 @@ describe "CollaboratorsInviteController", ->
_id: @project_id
name: "some project"
owner_ref: @invite.sendingUserId
+ collaberator_refs: []
+ readOnly_refs: []
@owner =
_id: @fakeProject.owner_ref
first_name: "John"
@@ -210,59 +213,17 @@ describe "CollaboratorsInviteController", ->
it 'should not call next', ->
@next.callCount.should.equal 0
- it 'should call getInviteByToken', ->
- @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
-
it 'should call Project.findOne', ->
@Project.findOne.callCount.should.equal 1
@Project.findOne.calledWith({_id: @project_id}).should.equal true
+ it 'should call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+
it 'should call User.findOne', ->
@User.findOne.callCount.should.equal 1
@User.findOne.calledWith({_id: @fakeProject.owner_ref}).should.equal true
- describe 'when the getInviteByToken produces an error', ->
-
- beforeEach ->
- @err = new Error('woops')
- @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, @err)
- @CollaboratorsInviteController.viewInvite @req, @res, @next
-
- it 'should call next with the error', ->
- @next.callCount.should.equal 1
- @next.calledWith(@err).should.equal true
-
- it 'should call getInviteByToken', ->
- @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
-
- it 'should not call Project.findOne', ->
- @Project.findOne.callCount.should.equal 0
-
- it 'should not call User.findOne', ->
- @User.findOne.callCount.should.equal 0
-
- describe 'when the getInviteByToken does not produce an invite', ->
-
- beforeEach ->
- @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null)
- @CollaboratorsInviteController.viewInvite @req, @res, @next
-
- it 'should render the not-valid view template', ->
- @res.render.callCount.should.equal 1
- @res.render.calledWith('project/invite/not-valid').should.equal true
-
- it 'should not call next', ->
- @next.callCount.should.equal 0
-
- it 'should call getInviteByToken', ->
- @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
-
- it 'should not call Project.findOne', ->
- @Project.findOne.callCount.should.equal 0
-
- it 'should not call User.findOne', ->
- @User.findOne.callCount.should.equal 0
-
describe 'when Project.findOne produces an error', ->
beforeEach ->
@@ -273,13 +234,13 @@ describe "CollaboratorsInviteController", ->
@next.callCount.should.equal 1
expect(@next.firstCall.args[0]).to.be.instanceof Error
- it 'should call getInviteByToken', ->
- @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
-
it 'should call Project.findOne', ->
@Project.findOne.callCount.should.equal 1
@Project.findOne.calledWith({_id: @project_id}).should.equal true
+ it 'should not call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0
+
it 'should not call User.findOne', ->
@User.findOne.callCount.should.equal 0
@@ -300,9 +261,80 @@ describe "CollaboratorsInviteController", ->
@Project.findOne.callCount.should.equal 1
@Project.findOne.calledWith({_id: @project_id}).should.equal true
+ it 'should not call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0
+
it 'should not call User.findOne', ->
@User.findOne.callCount.should.equal 0
+ describe 'when the getInviteByToken produces an error', ->
+
+ beforeEach ->
+ @err = new Error('woops')
+ @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, @err)
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
+
+ it 'should call next with the error', ->
+ @next.callCount.should.equal 1
+ @next.calledWith(@err).should.equal true
+
+ it 'should call Project.findOne', ->
+ @Project.findOne.callCount.should.equal 1
+
+ it 'should call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+
+ it 'should not call User.findOne', ->
+ @User.findOne.callCount.should.equal 0
+
+ describe 'when the getInviteByToken does not produce an invite', ->
+
+ describe 'when the user is not already a member of this project', ->
+
+ beforeEach ->
+ @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null)
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
+
+ it 'should render the not-valid view template', ->
+ @res.render.callCount.should.equal 1
+ @res.render.calledWith('project/invite/not-valid').should.equal true
+
+ it 'should not call next', ->
+ @next.callCount.should.equal 0
+
+ it 'should call Project.findOne', ->
+ @Project.findOne.callCount.should.equal 1
+
+ it 'should call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+
+ it 'should not call User.findOne', ->
+ @User.findOne.callCount.should.equal 0
+
+ describe 'when the user is already a member of the project', ->
+
+ beforeEach ->
+ @fakeProject.collaberator_refs = [ObjectId(), @current_user_id, ObjectId()]
+ @Project.findOne.callsArgWith(2, null, @fakeProject)
+ @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null)
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
+
+ it 'should redirect to the project page', ->
+ @res.redirect.callCount.should.equal 1
+ @res.redirect.calledWith("/project/#{@project_id}").should.equal true
+
+ it 'should not call next', ->
+ @next.callCount.should.equal 0
+
+ it 'should call Project.findOne', ->
+ @Project.findOne.callCount.should.equal 1
+
+ it 'should call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+
+ it 'should not call User.findOne', ->
+ @User.findOne.callCount.should.equal 0
+
describe 'when User.findOne produces an error', ->
beforeEach ->
From e70f12146187ee4cbe8e4918075930cbf2c54bfe Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 27 Jul 2016 15:55:31 +0100
Subject: [PATCH 040/378] Correct name of expireAfterSeconds index
---
services/web/app/coffee/models/ProjectInvite.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/coffee/models/ProjectInvite.coffee b/services/web/app/coffee/models/ProjectInvite.coffee
index 5e46408176..2e3309397b 100644
--- a/services/web/app/coffee/models/ProjectInvite.coffee
+++ b/services/web/app/coffee/models/ProjectInvite.coffee
@@ -16,7 +16,7 @@ ProjectInviteSchema = new Schema(
sendingUserId: ObjectId
projectId: ObjectId
privileges: String
- createdAt: {type: Date, default: Date.now, index: {expiresAfterSeconds: THIRTY_DAYS_IN_SECONDS}}
+ createdAt: {type: Date, default: Date.now, index: {expireAfterSeconds: THIRTY_DAYS_IN_SECONDS}}
},
{
collection: 'projectInvites'
From 1cb9c3582dd331cb54edbbabe82e799360172d48 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 28 Jul 2016 09:47:07 +0100
Subject: [PATCH 041/378] Don't return early if user is already member.
---
.../CollaboratorsInviteHandler.coffee | 20 +++++++++----------
1 file changed, 9 insertions(+), 11 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 161ddf0454..0950c7efaa 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -60,6 +60,7 @@ module.exports = CollaboratorsInviteHandler =
callback(null, invite)
acceptInvite: (projectId, inviteId, tokenString, user, callback=(err)->) ->
+ # fetch the target project
Project.findOne {_id: projectId}, (err, project) ->
if err?
logger.err {err, projectId}, "error finding project"
@@ -68,7 +69,7 @@ module.exports = CollaboratorsInviteHandler =
err = new Errors.NotFoundError("no project found for invite")
logger.log {err, projectId, inviteId}, "no project found"
return callback(err)
- # TODO: check if we need to cast the ids to ObjectId
+ # fetch the invite
ProjectInvite.findOne {_id: inviteId, projectId: projectId, token: tokenString}, (err, invite) ->
if err?
logger.err {err, projectId, inviteId}, "error finding invite"
@@ -78,26 +79,23 @@ module.exports = CollaboratorsInviteHandler =
logger.log {err, projectId, inviteId, tokenString}, "no matching invite found"
return callback(err)
- # do the thing
- existing_users = (project.collaberator_refs or [])
- existing_users = existing_users.concat(project.readOnly_refs or [])
- existing_users = existing_users.map (u) -> u.toString()
- if existing_users.indexOf(user._id.toString()) > -1
- return callback null # User already in Project
-
+ # build an update to be applied with $addToSet, user is added to either
+ # `collaberator_refs` or `readOnly_refs`
privilegeLevel = invite.privileges
-
if privilegeLevel == PrivilegeLevels.READ_AND_WRITE
level = {"collaberator_refs": user._id}
- logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user"
+ logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user with read-write access"
else if privilegeLevel == PrivilegeLevels.READ_ONLY
level = {"readOnly_refs": user._id}
- logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user"
+ logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user with read-only access"
else
return callback(new Error("unknown privilegeLevel: #{privilegeLevel}"))
ContactManager.addContact invite.sendingUserId, user._id
+ # Update the project, adding the new member. We don't check if the user is already a member of the project,
+ # because even if they are we still want to have them 'accept' the invite and go through the usual process,
+ # despite the $addToSet operation having no meaningful effect
Project.update { _id: project._id }, { $addToSet: level }, (error) ->
return callback(error) if error?
# Flush to TPDS in background to add files to collaborator's Dropbox
From ed65e16e54ef17ba0219aea2d82c26ac06e420a1 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 28 Jul 2016 11:15:11 +0100
Subject: [PATCH 042/378] If user is member of project, redirect to project.
Leave invite in place to expire naturally.
---
.../CollaboratorsInviteController.coffee | 16 ++++-----
.../CollaboratorsInviteHandler.coffee | 13 +++++--
.../CollaboratorsInviteControllerTests.coffee | 4 +--
.../CollaboratorsInviteHandlerTests.coffee | 35 +++++++++++++++++++
4 files changed, 55 insertions(+), 13 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 5618da7db7..26da21d9db 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -55,6 +55,7 @@ module.exports = CollaboratorsInviteController =
token = req.params.token
currentUser = req.session.user
_renderInvalidPage = () ->
+ logger.log {projectId, token}, "invite not valid, rendering not-valid page"
res.render "project/invite/not-valid"
# get the target project
Project.findOne {_id: projectId}, {owner_ref: 1, name: 1, collaberator_refs: 1, readOnly_refs: 1}, (err, project) ->
@@ -64,21 +65,20 @@ module.exports = CollaboratorsInviteController =
if !project
logger.log {projectId}, "no project found"
return _renderInvalidPage()
+ # check if user is already a member of the project, redirect to project if so
+ allMembers = (project.collaberator_refs || []).concat(project.readOnly_refs || []).map((oid) -> oid.toString())
+ if currentUser._id in allMembers
+ logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting"
+ return res.redirect "/project/#{projectId}"
# get the invite
CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) ->
if err?
logger.err {projectId, token}, "error getting invite by token"
return next(err)
- # check if invite is gone
+ # check if invite is gone, or otherwise non-existent
if !invite
logger.log {projectId, token}, "no invite found for this token"
- # check if user is already a member of the project, redirect to project if so
- allMembers = (project.collaberator_refs || []).concat(project.readOnly_refs || []).map((oid) -> oid.toString())
- if currentUser._id in allMembers
- logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting"
- return res.redirect "/project/#{projectId}"
- else
- return _renderInvalidPage()
+ return _renderInvalidPage()
# check the user who sent the invite exists
User.findOne {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
if err?
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 0950c7efaa..8118f7b135 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -79,6 +79,15 @@ module.exports = CollaboratorsInviteHandler =
logger.log {err, projectId, inviteId, tokenString}, "no matching invite found"
return callback(err)
+ # check if user is already a member of this project,
+ # return early if so
+ existing_users = (project.collaberator_refs or [])
+ existing_users = existing_users.concat(project.readOnly_refs or [])
+ existing_users = existing_users.map (u) -> u.toString()
+ if existing_users.indexOf(user._id.toString()) > -1
+ logger.log {projectId, userId: user._id}, "user already member of project, returning"
+ return callback null
+
# build an update to be applied with $addToSet, user is added to either
# `collaberator_refs` or `readOnly_refs`
privilegeLevel = invite.privileges
@@ -93,9 +102,7 @@ module.exports = CollaboratorsInviteHandler =
ContactManager.addContact invite.sendingUserId, user._id
- # Update the project, adding the new member. We don't check if the user is already a member of the project,
- # because even if they are we still want to have them 'accept' the invite and go through the usual process,
- # despite the $addToSet operation having no meaningful effect
+ # Update the project, adding the new member.
Project.update { _id: project._id }, { $addToSet: level }, (error) ->
return callback(error) if error?
# Flush to TPDS in background to add files to collaborator's Dropbox
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 3de79d9fad..9cf4db291e 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -329,8 +329,8 @@ describe "CollaboratorsInviteController", ->
it 'should call Project.findOne', ->
@Project.findOne.callCount.should.equal 1
- it 'should call getInviteByToken', ->
- @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+ it 'should not call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0
it 'should not call User.findOne', ->
@User.findOne.callCount.should.equal 0
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index 1af35edc90..d144a3dcb9 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -343,6 +343,41 @@ describe "CollaboratorsInviteHandler", ->
@ProjectInvite.remove.callCount.should.equal 0
done()
+ describe 'when user is already a member of project', ->
+
+ beforeEach ->
+ @fakeProject.collaberator_refs = [ObjectId(), @user._id, ObjectId(), ObjectId()]
+ @Project.findOne.callsArgWith(1, null, @fakeProject)
+
+ it 'should not produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.not.be.instanceof Error
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should have called Project.findOne', (done) ->
+ @call (err) =>
+ @Project.findOne.callCount.should.equal 1
+ @Project.findOne.calledWith({_id: @projectId}).should.equal true
+ done()
+
+ it 'should have called ProjectInvite.findOne', (done) ->
+ @call (err) =>
+ @ProjectInvite.findOne.callCount.should.equal 1
+ @ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId, token: @token}).should.equal true
+ done()
+
+ it 'should have returned early, not have called Project.update', (done) ->
+ @call (err) =>
+ @Project.update.callCount.should.equal 0
+ done()
+
+ it 'should not have called ProjectInvite.remove', (done) ->
+ @call (err) =>
+ @ProjectInvite.remove.callCount.should.equal 0
+ done()
+
+
describe 'when ProjectInvite.findOne does not find an invite', ->
beforeEach ->
From 254705c3f17e2939f381bb378de20f845587c12e Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 28 Jul 2016 13:47:19 +0100
Subject: [PATCH 043/378] Tidy up, and fall back to handling data.users.
---
.../controllers/ShareProjectModalController.coffee | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index b9567f46dd..4f3adbdcfd 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -51,7 +51,6 @@ define [
, (response) ->
console.error response
)
- window._x = getOutstandingInvites
$scope.filterAutocompleteUsers = ($query) ->
currentMemberEmails = getCurrentMemberEmails()
@@ -84,7 +83,7 @@ define [
# Skip this existing member
return addNextMember()
- # TODO: double-check if member.type == 'user' needs to be an invite
+ # NOTE: groups aren't really a thing in ShareLaTeX, partially inherited from DJ
if member.type == "user"
request = projectInvites.sendInvite(member.email, $scope.inputs.privileges)
else if member.type == "group"
@@ -97,6 +96,15 @@ define [
if data.invite
invite = data.invite
$scope.state.invites.push invite
+ else
+ if data.users?
+ users = data.users
+ else if data.user?
+ users = [data.user]
+ else
+ users = []
+ $scope.project.members.push users...
+
setTimeout () ->
# Give $scope a chance to update $scope.canAddCollaborators
# with new collaborator information.
From 29d333ae514cb5c0a616c982ef3e1e958885b84a Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 28 Jul 2016 14:50:08 +0100
Subject: [PATCH 044/378] Increase timeout on acceptance tests
---
services/web/Gruntfile.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee
index 0e3329c41f..7c966260e8 100644
--- a/services/web/Gruntfile.coffee
+++ b/services/web/Gruntfile.coffee
@@ -199,7 +199,7 @@ module.exports = (grunt) ->
acceptance:
src: ["test/acceptance/js/#{grunt.option('feature') or '**'}/*.js"]
options:
- timeout: 10000
+ timeout: 40000
reporter: grunt.option('reporter') or 'spec'
grep: grunt.option("grep")
From 748851b51ebafbe8de56f5ca76a13971b7d4419c Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 28 Jul 2016 14:53:22 +0100
Subject: [PATCH 045/378] start ProjectInvite acceptance test module
---
.../coffee/ProjectInviteTests.coffee | 50 +++++++++++++++++++
1 file changed, 50 insertions(+)
create mode 100644 services/web/test/acceptance/coffee/ProjectInviteTests.coffee
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
new file mode 100644
index 0000000000..7a460fac0d
--- /dev/null
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -0,0 +1,50 @@
+expect = require("chai").expect
+Async = require("async")
+User = require "./helpers/User"
+request = require "./helpers/request"
+settings = require "settings-sharelatex"
+
+describe "ProjectInviteTests", ->
+ before (done) ->
+ @timeout(20000)
+ @sendingUser = new User()
+ @user = new User()
+ @site_admin = new User({email: "admin@example.com"})
+ @projectId = null
+ Async.series [
+ (cb) => @user.login cb
+ (cb) => @user.logout cb
+ (cb) => @sendingUser.login cb
+ (cb) => @sendingUser.createProject('sharing test', (err, projectId) =>
+ throw err if err
+ @projectId = projectId
+ cb()
+ )
+ (cb) => @sendingUser.logout cb
+ ], done
+
+ describe "user is logged in", ->
+
+ beforeEach (done) ->
+ @user.login (err) =>
+ if err
+ throw err
+ done()
+
+ describe 'user is already a member of the project', ->
+
+ beforeEach ->
+
+ it 'should redirect to the project page', (done) ->
+ Async.series(
+ [
+ (cb) =>
+ cb()
+
+
+
+ ], (err, result) =>
+ if err
+ throw err
+ done()
+ )
From 9c6195fbec25c2a45ead074ba200a096451d0062 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 28 Jul 2016 15:59:59 +0100
Subject: [PATCH 046/378] Factor out link builder
---
.../CollaboratorsEmailHandler.coffee | 18 +++++++++++-------
1 file changed, 11 insertions(+), 7 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
index dcaaf5fcde..a6ea518815 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
@@ -2,7 +2,16 @@ Project = require("../../models/Project").Project
EmailHandler = require("../Email/EmailHandler")
Settings = require "settings-sharelatex"
-module.exports =
+
+module.exports = CollaboratorsEmailHandler =
+
+ _buildInviteUrl: (project, invite) ->
+ "#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [
+ "project_name=#{encodeURIComponent(project.name)}"
+ "user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
+ "r=#{project.owner_ref.referal_id}" # Referal
+ "rs=ci" # referral source = collaborator invite
+ ].join("&")
notifyUserOfProjectShare: (project_id, email, callback)->
Project
@@ -36,11 +45,6 @@ module.exports =
replyTo: project.owner_ref.email
project:
name: project.name
- inviteUrl: "#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [
- "project_name=#{encodeURIComponent(project.name)}"
- "user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
- "r=#{project.owner_ref.referal_id}" # Referal
- "rs=ci" # referral source = collaborator invite
- ].join("&")
+ inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
owner: project.owner_ref
EmailHandler.sendEmail "projectInvite", emailOptions, callback
From 23c94c9599a3e5a0be0b093ee9cde802ef291b5b Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 28 Jul 2016 16:00:18 +0100
Subject: [PATCH 047/378] get invite and link for test
---
.../coffee/ProjectInviteTests.coffee | 41 ++++++++++++++++---
.../acceptance/coffee/helpers/User.coffee | 15 +++++--
2 files changed, 46 insertions(+), 10 deletions(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 7a460fac0d..aa29db344d 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -3,6 +3,19 @@ Async = require("async")
User = require "./helpers/User"
request = require "./helpers/request"
settings = require "settings-sharelatex"
+CollaboratorsEmailHandler = require "../../../app/js/Features/Collaborators/CollaboratorsEmailHandler"
+
+
+_createInvite = (projectId, user, email, callback=(err, invite)->) ->
+ user.getCsrfToken (err) ->
+ return callback(err) if err
+ user.request.post {
+ url: "/project/#{projectId}/invite",
+ json:
+ email: email
+ }, (err, response, body) ->
+ return callback(err) if err
+ callback(err, body.invite)
describe "ProjectInviteTests", ->
before (done) ->
@@ -10,19 +23,30 @@ describe "ProjectInviteTests", ->
@sendingUser = new User()
@user = new User()
@site_admin = new User({email: "admin@example.com"})
+ @email = 'user@example.com'
+ @projectName = 'sharing test'
@projectId = null
+ @fakeProject = null
Async.series [
(cb) => @user.login cb
- (cb) => @user.logout cb
(cb) => @sendingUser.login cb
- (cb) => @sendingUser.createProject('sharing test', (err, projectId) =>
+ (cb) => @sendingUser.createProject(@projectName, (err, projectId, project) =>
throw err if err
@projectId = projectId
+ @fakeProject = {
+ _id: projectId,
+ name: @projectName,
+ owner_ref: @sendingUser
+ }
cb()
)
- (cb) => @sendingUser.logout cb
], done
+ after (done) ->
+ Async.series [
+ (cb) => @sendingUser.deleteProject(@projectId, cb)
+ ], done
+
describe "user is logged in", ->
beforeEach (done) ->
@@ -33,16 +57,21 @@ describe "ProjectInviteTests", ->
describe 'user is already a member of the project', ->
- beforeEach ->
+ beforeEach (done) ->
+ @invite = null
+ @link = null
+ _createInvite @projectId, @sendingUser, @email, (err, invite) =>
+ @invite = invite
+ @link = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, @invite)
+ done()
it 'should redirect to the project page', (done) ->
Async.series(
[
(cb) =>
+ console.log ">> yes"
cb()
-
-
], (err, result) =>
if err
throw err
diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee
index 888473578e..6ce3072529 100644
--- a/services/web/test/acceptance/coffee/helpers/User.coffee
+++ b/services/web/test/acceptance/coffee/helpers/User.coffee
@@ -13,7 +13,7 @@ class User
@request = request.defaults({
jar: @jar
})
-
+
login: (callback = (error) ->) ->
@getCsrfToken (error) =>
return callback(error) if error?
@@ -58,8 +58,15 @@ class User
return callback(error) if error?
if !body?.project_id?
console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body
- callback(null, body.project_id)
-
+ callback(null, body.project_id, body)
+
+ deleteProject: (project_id, callback=(error)) ->
+ @request.delete {
+ url: "/project/#{project_id}"
+ }, (error, response, body) ->
+ return callback(error) if error?
+ callback(null)
+
addUserToProject: (project_id, email, privileges, callback = (error, user) ->) ->
@request.post {
url: "/project/#{project_id}/users",
@@ -67,7 +74,7 @@ class User
}, (error, response, body) ->
return callback(error) if error?
callback(null, body.user)
-
+
makePublic: (project_id, level, callback = (error) ->) ->
@request.post {
url: "/project/#{project_id}/settings/admin",
From 563247044bc453c5c70da4b4e765374bac38174e Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 29 Jul 2016 09:52:55 +0100
Subject: [PATCH 048/378] Start testing the invite page
---
.../CollaboratorsInviteController.coffee | 2 +-
.../coffee/ProjectInviteTests.coffee | 22 ++++++++++++-------
.../acceptance/coffee/helpers/User.coffee | 2 ++
3 files changed, 17 insertions(+), 9 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 26da21d9db..c30acdb4cb 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -88,7 +88,7 @@ module.exports = CollaboratorsInviteController =
logger.log {projectId}, "no project owner found"
return _renderInvalidPage()
# finally render the invite
- res.render "project/invite/show", {invite, project, owner}
+ res.render "project/invite/show", {invite, project, owner, title: "Project Invite"}
acceptInvite: (req, res, next) ->
projectId = req.params.Project_id
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index aa29db344d..2e21d92a0b 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -6,10 +6,10 @@ settings = require "settings-sharelatex"
CollaboratorsEmailHandler = require "../../../app/js/Features/Collaborators/CollaboratorsEmailHandler"
-_createInvite = (projectId, user, email, callback=(err, invite)->) ->
- user.getCsrfToken (err) ->
+_createInvite = (projectId, sendingUser, email, callback=(err, invite)->) ->
+ sendingUser.getCsrfToken (err) ->
return callback(err) if err
- user.request.post {
+ sendingUser.request.post {
url: "/project/#{projectId}/invite",
json:
email: email
@@ -23,7 +23,7 @@ describe "ProjectInviteTests", ->
@sendingUser = new User()
@user = new User()
@site_admin = new User({email: "admin@example.com"})
- @email = 'user@example.com'
+ @email = 'smoketestuser@example.com'
@projectName = 'sharing test'
@projectId = null
@fakeProject = null
@@ -55,7 +55,7 @@ describe "ProjectInviteTests", ->
throw err
done()
- describe 'user is already a member of the project', ->
+ describe 'user is not a member of the project', ->
beforeEach (done) ->
@invite = null
@@ -65,12 +65,18 @@ describe "ProjectInviteTests", ->
@link = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, @invite)
done()
- it 'should redirect to the project page', (done) ->
+ it 'should render the invite page', (done) ->
Async.series(
[
(cb) =>
- console.log ">> yes"
- cb()
+ @user.request.get {
+ uri: @link
+ baseUrl: null
+ }, (err, response, body) =>
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 200
+ expect(body).to.match new RegExp("Project Invite - .*")
+ cb()
], (err, result) =>
if err
diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee
index 6ce3072529..a96ab42648 100644
--- a/services/web/test/acceptance/coffee/helpers/User.coffee
+++ b/services/web/test/acceptance/coffee/helpers/User.coffee
@@ -28,6 +28,8 @@ class User
return callback(error) if error?
@id = user?._id?.toString()
@_id = user?._id?.toString()
+ @first_name = user?.first_name
+ @referal_id = user?.referal_id
callback()
logout: (callback = (error) ->) ->
From f33d01f37511807cbedee47d8e12475e6be4461f Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 29 Jul 2016 11:04:07 +0100
Subject: [PATCH 049/378] Test acceptance of invite
---
.../coffee/ProjectInviteTests.coffee | 46 ++++++++++++++-----
.../acceptance/coffee/helpers/User.coffee | 7 +++
2 files changed, 42 insertions(+), 11 deletions(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 2e21d92a0b..83eb5ee81f 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -6,17 +6,32 @@ settings = require "settings-sharelatex"
CollaboratorsEmailHandler = require "../../../app/js/Features/Collaborators/CollaboratorsEmailHandler"
-_createInvite = (projectId, sendingUser, email, callback=(err, invite)->) ->
+createInvite = (projectId, sendingUser, email, callback=(err, invite)->) ->
sendingUser.getCsrfToken (err) ->
return callback(err) if err
sendingUser.request.post {
url: "/project/#{projectId}/invite",
json:
email: email
+ privileges: 'readAndWrite'
}, (err, response, body) ->
return callback(err) if err
callback(err, body.invite)
+followInviteLink = (user, link, callback=(err, response, body)->) ->
+ user.request.get {
+ uri: link
+ baseUrl: null
+ }, callback
+
+acceptInvite = (user, invite, callback=(err, response, body)->) ->
+ user.request.post {
+ uri: "/project/#{invite.projectId}/invite/#{invite._id}/accept"
+ json:
+ token: invite.token
+ }, callback
+
+
describe "ProjectInviteTests", ->
before (done) ->
@timeout(20000)
@@ -60,26 +75,35 @@ describe "ProjectInviteTests", ->
beforeEach (done) ->
@invite = null
@link = null
- _createInvite @projectId, @sendingUser, @email, (err, invite) =>
+ createInvite @projectId, @sendingUser, @email, (err, invite) =>
@invite = invite
@link = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, @invite)
done()
- it 'should render the invite page', (done) ->
+ it 'should allow the user to accept the invite and access the project', (done) ->
Async.series(
[
+ # go to the invite page
(cb) =>
- @user.request.get {
- uri: @link
- baseUrl: null
- }, (err, response, body) =>
+ followInviteLink @user, @link, (err, response, body) =>
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 200
expect(body).to.match new RegExp("Project Invite - .*")
cb()
- ], (err, result) =>
- if err
- throw err
- done()
+ # accept the invite
+ (cb) =>
+ acceptInvite @user, @invite, (err, response, body) =>
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 302
+ expect(response.headers.location).to.equal "/project/#{@invite.projectId}"
+ cb()
+
+ # access the project page
+ (cb) =>
+ @user.openProject @invite.projectId, (err) =>
+ expect(err).to.be.oneOf [null, undefined]
+ cb()
+
+ ], done
)
diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee
index a96ab42648..3db946bea3 100644
--- a/services/web/test/acceptance/coffee/helpers/User.coffee
+++ b/services/web/test/acceptance/coffee/helpers/User.coffee
@@ -69,6 +69,13 @@ class User
return callback(error) if error?
callback(null)
+ openProject: (project_id, callback=(error)) ->
+ @request.get {
+ url: "/project/#{project_id}"
+ }, (error, response, body) ->
+ return callback(error) if error? or response.statusCode != 200
+ callback(null)
+
addUserToProject: (project_id, email, privileges, callback = (error, user) ->) ->
@request.post {
url: "/project/#{project_id}/users",
From b33d4e103de8027ef8becc5febba6ce539d9f4e1 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 29 Jul 2016 11:08:24 +0100
Subject: [PATCH 050/378] Test when the user does not accept the invite
---
.../coffee/ProjectInviteTests.coffee | 20 +++++++++++++++++++
.../acceptance/coffee/helpers/User.coffee | 5 ++++-
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 83eb5ee81f..c65e788398 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -80,6 +80,26 @@ describe "ProjectInviteTests", ->
@link = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, @invite)
done()
+ it 'should not grant access if the user does not accept the invite', (done) ->
+ Async.series(
+ [
+ # go to the invite page
+ (cb) =>
+ followInviteLink @user, @link, (err, response, body) =>
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 200
+ expect(body).to.match new RegExp("Project Invite - .*")
+ cb()
+
+ # access the project page
+ (cb) =>
+ @user.openProject @invite.projectId, (err) =>
+ expect(err).to.be.instanceof Error
+ cb()
+
+ ], done
+ )
+
it 'should allow the user to accept the invite and access the project', (done) ->
Async.series(
[
diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee
index 3db946bea3..1d39e3cf92 100644
--- a/services/web/test/acceptance/coffee/helpers/User.coffee
+++ b/services/web/test/acceptance/coffee/helpers/User.coffee
@@ -73,7 +73,10 @@ class User
@request.get {
url: "/project/#{project_id}"
}, (error, response, body) ->
- return callback(error) if error? or response.statusCode != 200
+ return callback(error) if error?
+ if response.statusCode != 200
+ err = new Error("Non-success response when opening project: #{response.statusCode}")
+ return callback(err)
callback(null)
addUserToProject: (project_id, email, privileges, callback = (error, user) ->) ->
From f3a1f32bb1263619aaca2f9f7962d0a7a35347f5 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 29 Jul 2016 11:54:08 +0100
Subject: [PATCH 051/378] Test the invalid-invite page
---
.../CollaboratorsInviteController.coffee | 2 +-
.../coffee/ProjectInviteTests.coffee | 23 ++++++++++++++++++-
2 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index c30acdb4cb..f86d46e151 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -56,7 +56,7 @@ module.exports = CollaboratorsInviteController =
currentUser = req.session.user
_renderInvalidPage = () ->
logger.log {projectId, token}, "invite not valid, rendering not-valid page"
- res.render "project/invite/not-valid"
+ res.render "project/invite/not-valid", {title: "Invalid Invite"}
# get the target project
Project.findOne {_id: projectId}, {owner_ref: 1, name: 1, collaberator_refs: 1, readOnly_refs: 1}, (err, project) ->
if err?
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index c65e788398..f7512f5a57 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -91,7 +91,28 @@ describe "ProjectInviteTests", ->
expect(body).to.match new RegExp("Project Invite - .*")
cb()
- # access the project page
+ # forbid access to the project page
+ (cb) =>
+ @user.openProject @invite.projectId, (err) =>
+ expect(err).to.be.instanceof Error
+ cb()
+
+ ], done
+ )
+
+ it 'should render the invalid-invite page if the token is invalid', (done) ->
+ Async.series(
+ [
+ # go to the invite page with an invalid token
+ (cb) =>
+ link = @link.replace(@invite.token, 'not_a_real_token')
+ followInviteLink @user, link, (err, response, body) =>
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 200
+ expect(body).to.match new RegExp("Invalid Invite - .*")
+ cb()
+
+ # forbid access to the project page
(cb) =>
@user.openProject @invite.projectId, (err) =>
expect(err).to.be.instanceof Error
From e7c1f7f0fc7bffc7bdd4c18edefa5bd02a90ed80 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 29 Jul 2016 13:39:18 +0100
Subject: [PATCH 052/378] Refactor, deduplicate tests
---
.../coffee/ProjectInviteTests.coffee | 94 ++++++++++---------
1 file changed, 49 insertions(+), 45 deletions(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index f7512f5a57..1a2603299e 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -18,13 +18,15 @@ createInvite = (projectId, sendingUser, email, callback=(err, invite)->) ->
return callback(err) if err
callback(err, body.invite)
-followInviteLink = (user, link, callback=(err, response, body)->) ->
+
+# Actions
+tryFollowInviteLink = (user, link, callback=(err, response, body)->) ->
user.request.get {
uri: link
baseUrl: null
}, callback
-acceptInvite = (user, invite, callback=(err, response, body)->) ->
+tryAcceptInvite = (user, invite, callback=(err, response, body)->) ->
user.request.post {
uri: "/project/#{invite.projectId}/invite/#{invite._id}/accept"
json:
@@ -32,6 +34,44 @@ acceptInvite = (user, invite, callback=(err, response, body)->) ->
}, callback
+# Expectations
+expectProjectAccess = (user, projectId, callback=()->) ->
+ # should have access to project
+ user.openProject projectId, (err) =>
+ expect(err).to.be.oneOf [null, undefined]
+ callback()
+
+expectNoProjectAccess = (user, projectId, callback=()->) ->
+ # should not have access to project page
+ user.openProject projectId, (err) =>
+ expect(err).to.be.instanceof Error
+ callback()
+
+expectInvitePage = (user, link, callback=()->) ->
+ # view invite
+ tryFollowInviteLink user, link, (err, response, body) ->
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 200
+ expect(body).to.match new RegExp("Project Invite - .*")
+ callback()
+
+expectInvalidInvitePage = (user, link, callback=()->) ->
+ # view invalid invite
+ tryFollowInviteLink user, link, (err, response, body) ->
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 200
+ expect(body).to.match new RegExp("Invalid Invite - .*")
+ callback()
+
+expectAcceptInviteAndRedirect = (user, invite, callback=()->) ->
+ # should accept the invite and redirect to project
+ tryAcceptInvite user, invite, (err, response, body) =>
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 302
+ expect(response.headers.location).to.equal "/project/#{invite.projectId}"
+ callback()
+
+
describe "ProjectInviteTests", ->
before (done) ->
@timeout(20000)
@@ -83,68 +123,32 @@ describe "ProjectInviteTests", ->
it 'should not grant access if the user does not accept the invite', (done) ->
Async.series(
[
- # go to the invite page
(cb) =>
- followInviteLink @user, @link, (err, response, body) =>
- expect(err).to.be.oneOf [null, undefined]
- expect(response.statusCode).to.equal 200
- expect(body).to.match new RegExp("Project Invite - .*")
- cb()
-
- # forbid access to the project page
+ expectInvitePage @user, @link, cb
(cb) =>
- @user.openProject @invite.projectId, (err) =>
- expect(err).to.be.instanceof Error
- cb()
-
+ expectNoProjectAccess @user, @invite.projectId, cb
], done
)
it 'should render the invalid-invite page if the token is invalid', (done) ->
Async.series(
[
- # go to the invite page with an invalid token
(cb) =>
link = @link.replace(@invite.token, 'not_a_real_token')
- followInviteLink @user, link, (err, response, body) =>
- expect(err).to.be.oneOf [null, undefined]
- expect(response.statusCode).to.equal 200
- expect(body).to.match new RegExp("Invalid Invite - .*")
- cb()
-
- # forbid access to the project page
+ expectInvalidInvitePage @user, link, cb
(cb) =>
- @user.openProject @invite.projectId, (err) =>
- expect(err).to.be.instanceof Error
- cb()
-
+ expectNoProjectAccess @user, @invite.projectId, cb
], done
)
it 'should allow the user to accept the invite and access the project', (done) ->
Async.series(
[
- # go to the invite page
(cb) =>
- followInviteLink @user, @link, (err, response, body) =>
- expect(err).to.be.oneOf [null, undefined]
- expect(response.statusCode).to.equal 200
- expect(body).to.match new RegExp("Project Invite - .*")
- cb()
-
- # accept the invite
+ expectInvitePage @user, @link, cb
(cb) =>
- acceptInvite @user, @invite, (err, response, body) =>
- expect(err).to.be.oneOf [null, undefined]
- expect(response.statusCode).to.equal 302
- expect(response.headers.location).to.equal "/project/#{@invite.projectId}"
- cb()
-
- # access the project page
+ expectAcceptInviteAndRedirect @user, @invite, cb
(cb) =>
- @user.openProject @invite.projectId, (err) =>
- expect(err).to.be.oneOf [null, undefined]
- cb()
-
+ expectProjectAccess @user, @invite.projectId, cb
], done
)
From 39fc6119647e2575855194db6418bf2161f65f94 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 29 Jul 2016 13:55:08 +0100
Subject: [PATCH 053/378] Revoke invite after each test
---
.../coffee/ProjectInviteTests.coffee | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 1a2603299e..e915a17d4c 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -6,7 +6,7 @@ settings = require "settings-sharelatex"
CollaboratorsEmailHandler = require "../../../app/js/Features/Collaborators/CollaboratorsEmailHandler"
-createInvite = (projectId, sendingUser, email, callback=(err, invite)->) ->
+createInvite = (sendingUser, projectId, email, callback=(err, invite)->) ->
sendingUser.getCsrfToken (err) ->
return callback(err) if err
sendingUser.request.post {
@@ -16,8 +16,16 @@ createInvite = (projectId, sendingUser, email, callback=(err, invite)->) ->
privileges: 'readAndWrite'
}, (err, response, body) ->
return callback(err) if err
- callback(err, body.invite)
+ callback(null, body.invite)
+revokeInvite = (sendingUser, projectId, inviteId, callback=(err)->) ->
+ sendingUser.getCsrfToken (err) ->
+ return callback(err) if err
+ sendingUser.request.delete {
+ url: "/project/#{projectId}/invite/#{inviteId}",
+ }, (err, response, body) ->
+ return callback(err) if err
+ callback(null)
# Actions
tryFollowInviteLink = (user, link, callback=(err, response, body)->) ->
@@ -115,11 +123,16 @@ describe "ProjectInviteTests", ->
beforeEach (done) ->
@invite = null
@link = null
- createInvite @projectId, @sendingUser, @email, (err, invite) =>
+ createInvite @sendingUser, @projectId, @email, (err, invite) =>
@invite = invite
@link = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, @invite)
done()
+ afterEach (done) ->
+ revokeInvite @sendingUser, @projectId, @invite._id, (err) =>
+ throw err if err
+ done()
+
it 'should not grant access if the user does not accept the invite', (done) ->
Async.series(
[
From e0d5075fdb349968269cb80a5af5f98dce95c739 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Tue, 26 Jul 2016 16:25:19 +0100
Subject: [PATCH 054/378] include check option when compiling
---
services/web/app/coffee/Features/Compile/ClsiManager.coffee | 1 +
.../web/app/coffee/Features/Compile/CompileController.coffee | 2 ++
.../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 1 +
3 files changed, 4 insertions(+)
diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee
index 568d806e99..08ef23bcd8 100755
--- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee
+++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee
@@ -147,6 +147,7 @@ module.exports = ClsiManager =
timeout: options.timeout
imageName: project.imageName
draft: !!options.draft
+ check: options.check
rootResourcePath: rootResourcePath
resources: resources
}
diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee
index f3680dc38e..e52693d4c4 100755
--- a/services/web/app/coffee/Features/Compile/CompileController.coffee
+++ b/services/web/app/coffee/Features/Compile/CompileController.coffee
@@ -29,6 +29,8 @@ module.exports = CompileController =
options.compiler = req.body.compiler
if req.body?.draft
options.draft = req.body.draft
+ if req.body?.check
+ options.check = if req.body.check is "error" then "error" else "warn"
logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
return next(error) if error?
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index 0f8134258c..5144e0e73e 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -86,6 +86,7 @@ define [
return $http.post url, {
rootDoc_id: options.rootDocOverride_id or null
draft: $scope.draft
+ check: if options.check then "error" else "warn"
_csrf: window.csrfToken
}, {params: params}
From ef85f1014c93e7b32856076e7755b1297f425d60 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Tue, 26 Jul 2016 16:26:27 +0100
Subject: [PATCH 055/378] add menu option for checking with chktex
---
.../web/app/views/project/editor/pdf.jade | 8 +-
.../ide/pdf/controllers/PdfController.coffee | 80 ++++++++++++++-----
2 files changed, 69 insertions(+), 19 deletions(-)
diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade
index 200c609d97..b2d821f753 100644
--- a/services/web/app/views/project/editor/pdf.jade
+++ b/services/web/app/views/project/editor/pdf.jade
@@ -36,6 +36,11 @@ div.full-size.pdf(ng-controller="PdfController")
i.fa.fa-fw(ng-class="{'fa-check': draft}")
| #{translate("fast")}
span.subdued [draft]
+ li.dropdown-header #{translate("file_checks")}
+ li
+ a(href, ng-click="recompile({check:true})")
+ i.fa.fa-fw()
+ | #{translate("syntax check")}
a(
href
ng-click="stop()"
@@ -115,7 +120,8 @@ div.full-size.pdf(ng-controller="PdfController")
|
span(ng-show="entry.file") {{ entry.file }}
span(ng-show="entry.line") , line {{ entry.line }}
- p.entry-message(ng-show="entry.message") {{ entry.message }}
+ p.entry-message(ng-show="entry.message")
+ {{ entry.type }} {{ entry.message }}
.card.card-hint(
ng-if="entry.humanReadableHint"
stop-propagation="click"
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index 5144e0e73e..5a0b232a4c 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -92,6 +92,9 @@ define [
parseCompileResponse = (response) ->
+ # keep last url
+ last_pdf_url = $scope.pdf.url
+
# Reset everything
$scope.pdf.error = false
$scope.pdf.timedout = false
@@ -121,11 +124,17 @@ define [
if response.status == "timedout"
$scope.pdf.view = 'errors'
$scope.pdf.timedout = true
- fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
+ fetchLogs(fileByPath)
else if response.status == "terminated"
$scope.pdf.view = 'errors'
$scope.pdf.compileTerminated = true
- fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
+ fetchLogs(fileByPath)
+ else if response.status == "exited"
+ $scope.pdf.view = 'pdf'
+ $scope.pdf.compileExited = true
+ $scope.pdf.url = last_pdf_url
+ $scope.shouldShowLogs = true
+ fetchLogs(fileByPath)
else if response.status == "autocompile-backoff"
$scope.pdf.view = 'uncompiled'
else if response.status == "project-too-large"
@@ -135,7 +144,7 @@ define [
$scope.pdf.view = 'errors'
$scope.pdf.failure = true
$scope.shouldShowLogs = true
- fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
+ fetchLogs(fileByPath)
else if response.status == 'clsi-maintenance'
$scope.pdf.view = 'errors'
$scope.pdf.clsiMaintenance = true
@@ -166,7 +175,7 @@ define [
qs.popupDownload = true
$scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs)
- fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
+ fetchLogs(fileByPath)
IGNORE_FILES = ["output.fls", "output.fdb_latexmk"]
$scope.pdf.outputFiles = []
@@ -187,7 +196,11 @@ define [
}
- fetchLogs = (logFile, blgFile) ->
+ fetchLogs = (fileByPath) ->
+
+ logFile = fileByPath['output.log'];
+ blgFile = fileByPath['output.blg'];
+ chktexFile = fileByPath['output.chktex'];
getFile = (name, file) ->
opts =
@@ -214,6 +227,8 @@ define [
accumulateResults = (newEntries) ->
for key in ['all', 'errors', 'warnings']
+ if newEntries.type?
+ entry.type = newEntries.type for entry in newEntries[key]
logEntries[key] = logEntries[key].concat newEntries[key]
# use the parsers for each file type
@@ -223,10 +238,23 @@ define [
all = [].concat errors, warnings, typesetting
accumulateResults {all, errors, warnings}
+ processChkTex = (log) ->
+ errors = []
+ warnings = []
+ for line in log.split("\n")
+ if m = line.match /^(\S+):(\d+):(\d+): (Error|Warning): (.*)/
+ result = { file:m[1], line:m[2], column:m[3], level:m[4].toLowerCase(), message: "#{m[4]}: #{m[5]}"}
+ if result.level is 'error'
+ errors.push result
+ else
+ warnings.push result
+ all = [].concat errors, warnings
+ accumulateResults {type: "ChkTeX", all, errors, warnings}
+
processBiber = (log) ->
{errors, warnings} = BibLogParser.parse(log, {})
all = [].concat errors, warnings
- accumulateResults {all, errors, warnings}
+ accumulateResults {type: "BibTeX", all, errors, warnings}
# output the results
handleError = () ->
@@ -249,19 +277,35 @@ define [
}
# retrieve the logfile and process it
- response = getFile('output.log', logFile)
- .success processLog
- .error handleError
+ if logFile?
+ response = getFile('output.log', logFile)
+ .then (response) -> processLog(response.data)
- if blgFile? # retrieve the blg file if present
- response.success () ->
- getFile('output.blg', blgFile)
- # ignore errors in biber file
- .success processBiber
- # display the combined result
- .then annotateFiles
- else # otherwise just display the result
- response.success annotateFiles
+ if blgFile? # retrieve the blg file if present
+ response = response.then () ->
+ getFile('output.blg', blgFile)
+ .then(
+ (response) -> processBiber(response.data),
+ () -> true # ignore errors in biber file
+ )
+
+ if response?
+ response.catch handleError
+ else
+ handleError()
+
+ if chktexFile?
+ getChkTex = () ->
+ getFile('output.chktex', chktexFile)
+ .then (response) -> processChkTex(response.data)
+ # always retrieve the chktex file if present
+ if response?
+ response = response.then getChkTex, getChkTex
+ else
+ response = getChkTex()
+
+ # display the combined result
+ response.finally annotateFiles
getRootDocOverride_id = () ->
doc = ide.editorManager.getCurrentDocValue()
From e99176c0d089b15e6d2ffda2745d9a29030b7194 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Wed, 27 Jul 2016 10:55:24 +0100
Subject: [PATCH 056/378] fix tests
---
.../web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee | 1 +
1 file changed, 1 insertion(+)
diff --git a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee
index 2fb980b461..a3eeebcaaa 100644
--- a/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee
@@ -174,6 +174,7 @@ describe "ClsiManager", ->
timeout : 100
imageName: @image
draft: false
+ check: undefined
rootResourcePath: "main.tex"
resources: [{
path: "main.tex"
From d4c5028350696289d2b391a4040d86ede4e1449b Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Wed, 27 Jul 2016 16:51:13 +0100
Subject: [PATCH 057/378] allow HumanReadableLogs to accept an already parsed
log
---
.../coffee/ide/human-readable-logs/HumanReadableLogs.coffee | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee
index 747ca1ad08..ae9c3c1e4f 100644
--- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee
+++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee
@@ -3,7 +3,10 @@ define [
"ide/human-readable-logs/HumanReadableLogsRules"
], (LogParser, ruleset) ->
parse : (rawLog, options) ->
- parsedLogEntries = LogParser.parse(rawLog, options)
+ if typeof rawLog is 'string'
+ parsedLogEntries = LogParser.parse(rawLog, options)
+ else
+ parsedLogEntries = rawLog
_getRule = (logMessage) ->
return rule for rule in ruleset when rule.regexToMatch.test logMessage
From 14a0499b56789f279baf1dda7b97091a46e1e4e4 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Wed, 27 Jul 2016 16:52:36 +0100
Subject: [PATCH 058/378] allow HumanReadableLogs to rewrite messages with
regex
also allow an explicit hintId for each rule
---
.../ide/human-readable-logs/HumanReadableLogs.coffee | 7 ++++++-
.../ide/human-readable-logs/HumanReadableLogsRules.coffee | 7 +++++++
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee
index ae9c3c1e4f..e59e35a40c 100644
--- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee
+++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee
@@ -15,7 +15,12 @@ define [
ruleDetails = _getRule entry.message
if (ruleDetails?)
- entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1) if ruleDetails.regexToMatch?
+ if ruleDetails.ruleId?
+ entry.ruleId = ruleDetails.ruleId
+ else if ruleDetails.regexToMatch?
+ entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1)
+ if ruleDetails.newMessage?
+ entry.message = entry.message.replace ruleDetails.regexToMatch, ruleDetails.newMessage
entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint?
entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL?
diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
index f123515deb..021ff5d284 100644
--- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
+++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
@@ -88,4 +88,11 @@ define -> [
humanReadableHint: """
You have used a font command which is only available in math mode. To use this command, you must be in maths mode (E.g. $ \u2026 $ or \\begin{math} \u2026 \\end{math}). If you want to use it outside of math mode, use the text version instead: \\textrm, \\textit, etc.
"""
+ ,
+ ruleId: "hint_mismatched_environment"
+ regexToMatch: /Error: `([^']{2,})' expected, found `([^']{2,})'.*/
+ newMessage: "Error: environment does not match \\begin{$1} ... \\end{$2}"
+ humanReadableHint: """
+ You have used \\begin{...} without a corresponding \\end{...}
+ """
]
From 5da1b9041897f4b61d492bb00eb9b8ad105d38ba Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Wed, 27 Jul 2016 16:53:22 +0100
Subject: [PATCH 059/378] use validate as keyword for syntax checks
---
.../coffee/Features/Compile/CompileController.coffee | 2 +-
.../coffee/ide/pdf/controllers/PdfController.coffee | 11 +++++++++--
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee
index e52693d4c4..000465a83c 100755
--- a/services/web/app/coffee/Features/Compile/CompileController.coffee
+++ b/services/web/app/coffee/Features/Compile/CompileController.coffee
@@ -30,7 +30,7 @@ module.exports = CompileController =
if req.body?.draft
options.draft = req.body.draft
if req.body?.check
- options.check = if req.body.check is "error" then "error" else "warn"
+ options.check = if req.body.check is "validate" then "validate" else undefined
logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
return next(error) if error?
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index 5a0b232a4c..968cd1b60f 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -86,7 +86,7 @@ define [
return $http.post url, {
rootDoc_id: options.rootDocOverride_id or null
draft: $scope.draft
- check: if options.check then "error" else "warn"
+ check: if options.check then "validate" else null
_csrf: window.csrfToken
}, {params: params}
@@ -129,6 +129,12 @@ define [
$scope.pdf.view = 'errors'
$scope.pdf.compileTerminated = true
fetchLogs(fileByPath)
+ else if response.status in ["validation-fail", "validation-pass"]
+ $scope.pdf.view = 'pdf'
+ $scope.pdf.compileExited = true
+ $scope.pdf.url = last_pdf_url
+ $scope.shouldShowLogs = true
+ fetchLogs(fileByPath)
else if response.status == "exited"
$scope.pdf.view = 'pdf'
$scope.pdf.compileExited = true
@@ -249,7 +255,8 @@ define [
else
warnings.push result
all = [].concat errors, warnings
- accumulateResults {type: "ChkTeX", all, errors, warnings}
+ logHints = HumanReadableLogs.parse {type: "Validation", all, errors, warnings}
+ accumulateResults logHints
processBiber = (log) ->
{errors, warnings} = BibLogParser.parse(log, {})
From a3c8202d0eba75b84a54c2c2ebded0d46a507f3b Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 28 Jul 2016 16:18:36 +0100
Subject: [PATCH 060/378] support jumping to position with line and column from
log entries
---
.../public/coffee/ide/editor/EditorManager.coffee | 2 +-
.../cursor-position/CursorPositionManager.coffee | 12 ++++++------
.../coffee/ide/pdf/controllers/PdfController.coffee | 4 +++-
3 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee
index 54d56cac19..64cbe1d884 100644
--- a/services/web/public/coffee/ide/editor/EditorManager.coffee
+++ b/services/web/public/coffee/ide/editor/EditorManager.coffee
@@ -46,7 +46,7 @@ define [
done = () =>
if options.gotoLine?
- @$scope.$broadcast "editor:gotoLine", options.gotoLine
+ @$scope.$broadcast "editor:gotoLine", options.gotoLine, options.gotoColumn
if doc.id == @$scope.editor.open_doc_id and !options.forceReopen
@$scope.$apply () =>
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 7019b213b5..6e238122e1 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
@@ -23,10 +23,10 @@ define [], () ->
@storeCursorPosition(@editor.getSession())
@storeScrollTopPosition(@editor.getSession())
- @$scope.$on "#{@$scope.name}:gotoLine", (editor, value) =>
- if value?
+ @$scope.$on "#{@$scope.name}:gotoLine", (editor, line, column) =>
+ if line?
setTimeout () =>
- @gotoLine(value)
+ @gotoLine(line, column)
, 10 # Hack: Must happen after @gotoStoredPosition
storeScrollTopPosition: (session) ->
@@ -53,6 +53,6 @@ define [], () ->
@editor.getSession().setScrollTop(pos.scrollTop or 0)
delete @ignoreCursorPositionChanges
- gotoLine: (line) ->
- @editor.gotoLine(line)
- @editor.focus()
\ No newline at end of file
+ gotoLine: (line, column) ->
+ @editor.gotoLine(line, column)
+ @editor.focus()
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index 968cd1b60f..5dff3acb04 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -556,7 +556,9 @@ define [
return if !entity? or entity.type != "doc"
if entry.line?
line = entry.line
- ide.editorManager.openDoc(entity, gotoLine: line)
+ if entry.column?
+ column = entry.column
+ ide.editorManager.openDoc(entity, gotoLine: line, gotoColumn: column)
]
App.controller 'ClearCacheModalController', ["$scope", "$modalInstance", ($scope, $modalInstance) ->
From 4d3b743e4de0000e9990029b1715600e277c7193 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 28 Jul 2016 16:19:10 +0100
Subject: [PATCH 061/378] display validation errors only when doing validation
---
.../coffee/ide/pdf/controllers/PdfController.coffee | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index 5dff3acb04..780fda5aa3 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -134,7 +134,7 @@ define [
$scope.pdf.compileExited = true
$scope.pdf.url = last_pdf_url
$scope.shouldShowLogs = true
- fetchLogs(fileByPath)
+ fetchLogs(fileByPath, { validation: true })
else if response.status == "exited"
$scope.pdf.view = 'pdf'
$scope.pdf.compileExited = true
@@ -202,11 +202,13 @@ define [
}
- fetchLogs = (fileByPath) ->
+ fetchLogs = (fileByPath, options) ->
- logFile = fileByPath['output.log'];
- blgFile = fileByPath['output.blg'];
- chktexFile = fileByPath['output.chktex'];
+ if options?.validation
+ chktexFile = fileByPath['output.chktex']
+ else
+ logFile = fileByPath['output.log']
+ blgFile = fileByPath['output.blg']
getFile = (name, file) ->
opts =
From 208798ebe3f6d86cb27ce256d9b341644dd94747 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 28 Jul 2016 16:19:35 +0100
Subject: [PATCH 062/378] add a log hint for mismatched brackets
---
.../human-readable-logs/HumanReadableLogsRules.coffee | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
index 021ff5d284..c61c61506b 100644
--- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
+++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
@@ -93,6 +93,14 @@ define -> [
regexToMatch: /Error: `([^']{2,})' expected, found `([^']{2,})'.*/
newMessage: "Error: environment does not match \\begin{$1} ... \\end{$2}"
humanReadableHint: """
- You have used \\begin{...} without a corresponding \\end{...}
+ You have used \\begin{...} without a corresponding \\end{...}.
"""
+ ,
+ ruleId: "hint_mismatched_brackets"
+ regexToMatch: /Error: `([^a-zA-Z0-9])' expected, found `([^a-zA-Z0-9])'.*/
+ newMessage: "Error: brackets do not match, found '$2' instead of '$1'"
+ humanReadableHint: """
+ You have used an open bracket without a corresponding close bracket.
+ """
+
]
From 0742db7732face70b61a2914081a247501fcbcc5 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 28 Jul 2016 16:40:40 +0100
Subject: [PATCH 063/378] enable validation option only for beta programme
users
---
services/web/app/views/project/editor/pdf.jade | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade
index b2d821f753..832134c0d5 100644
--- a/services/web/app/views/project/editor/pdf.jade
+++ b/services/web/app/views/project/editor/pdf.jade
@@ -36,11 +36,12 @@ div.full-size.pdf(ng-controller="PdfController")
i.fa.fa-fw(ng-class="{'fa-check': draft}")
| #{translate("fast")}
span.subdued [draft]
- li.dropdown-header #{translate("file_checks")}
- li
- a(href, ng-click="recompile({check:true})")
- i.fa.fa-fw()
- | #{translate("syntax check")}
+ if user.betaProgram
+ li.dropdown-header #{translate("file_checks")}
+ li
+ a(href, ng-click="recompile({check:true})")
+ i.fa.fa-fw()
+ | #{translate("syntax_check")}
a(
href
ng-click="stop()"
From e508f7b8a99e460e1e9afb64aaff100ee4e306f0 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 28 Jul 2016 16:41:02 +0100
Subject: [PATCH 064/378] don't try to display a log hint link if there isn't
one
---
services/web/app/views/project/editor/pdf.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade
index 832134c0d5..973562abc0 100644
--- a/services/web/app/views/project/editor/pdf.jade
+++ b/services/web/app/views/project/editor/pdf.jade
@@ -133,7 +133,7 @@ div.full-size.pdf(ng-controller="PdfController")
ng-show="entry.humanReadableHint",
ng-bind-html="wikiEnabled ? entry.humanReadableHint : stripHTMLFromString(entry.humanReadableHint)")
.card-hint-actions.clearfix
- .card-hint-ext-link(ng-if="wikiEnabled")
+ .card-hint-ext-link(ng-if="wikiEnabled && entry.extraInfoURL")
a(
ng-href="{{ entry.extraInfoURL }}",
ng-click="trackLogHintsLearnMore()"
From f012a6fe327252aa08d414aeccb8f81650cc2abe Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Fri, 29 Jul 2016 17:24:45 +0100
Subject: [PATCH 065/378] Scroll patcher for Safari.
---
.../coffee/ide/SafariScrollPatcher.coffee | 59 +++++++++++++++++++
1 file changed, 59 insertions(+)
create mode 100644 services/web/public/coffee/ide/SafariScrollPatcher.coffee
diff --git a/services/web/public/coffee/ide/SafariScrollPatcher.coffee b/services/web/public/coffee/ide/SafariScrollPatcher.coffee
new file mode 100644
index 0000000000..4c77802926
--- /dev/null
+++ b/services/web/public/coffee/ide/SafariScrollPatcher.coffee
@@ -0,0 +1,59 @@
+define [
+], () ->
+ class SafariScrollPatcher
+ constructor: (ide, $scope) ->
+ @isBootstrapped = false
+ @isOverAce = false
+ @pdfDiv = null
+ @aceDiv = null
+
+ $scope.$on "loaded", () =>
+ console.log this
+ if !@isBootstrapped
+ console.log "bootstrapping"
+
+ @isBootstrapped = true;
+ @pdfDiv = document.querySelector ".pdfjs-viewer" # Grab the PDF div.
+ @aceDiv = document.querySelector ".ace_content" # Also the editor.
+ @isOverAce = false # Flag to control if the pointer is over Ace.
+
+ # Start listening to PDF wheel events when the pointer leaves the PDF region.
+ # P.S. This is the problem in a nutshell: although the pointer is elsewhere,
+ # wheel events keep being dispatched to the PDF.
+ @pdfDiv.addEventListener "mouseleave", () =>
+ @pdfDiv.addEventListener "wheel", dispatchToAce
+
+ # Stop listening to wheel events when the pointer enters the PDF region. If
+ # the pointer is over the PDF, native behaviour is adequate.
+ @pdfDiv.addEventListener "mouseenter", () =>
+ @pdfDiv.removeEventListener "wheel", dispatchToAce
+
+ # Set the "pointer over Ace" flag as false, when the mouse leaves its area.
+ @aceDiv.addEventListener "mouseleave", () =>
+ @isOverAce = false
+
+ # Set the "pointer over Ace" flag as true, when the mouse enters its area.
+ @aceDiv.addEventListener "mouseenter", () =>
+ @isOverAce = true
+
+ # Handler for wheel events on the PDF.
+ # If the pointer is over Ace, grab the event, prevent default behaviour
+ # and dispatch it to Ace.
+ dispatchToAce = (e) =>
+ if @isOverAce
+ # If this is logged, the problem just happened: the event arrived
+ # here (the PDF wheel handler), but it should've gone to Ace.
+ console.log "Event was bound to the PDF, dispatching to Ace"
+
+ # Small timeout - if we dispatch immediately, an exception is thrown.
+ window.setTimeout(() =>
+ # Dispatch the exact same event to Ace (this will keep values
+ # values e.g. `wheelDelta` consistent with user interaction).
+ @aceDiv.dispatchEvent e
+ , 5)
+
+ # Avoid scrolling the PDF, as we assume this was intended to the
+ # editor.
+ e.preventDefault()
+
+
From 9b3a28048e62e8c560d6843950254ecebfef5785 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Fri, 29 Jul 2016 17:24:55 +0100
Subject: [PATCH 066/378] Integrate Safari scroll patcher.
---
services/web/public/coffee/ide.coffee | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee
index 4c75ab5d77..d22ec949fb 100644
--- a/services/web/public/coffee/ide.coffee
+++ b/services/web/public/coffee/ide.coffee
@@ -9,6 +9,7 @@ define [
"ide/pdf/PdfManager"
"ide/binary-files/BinaryFilesManager"
"ide/references/ReferencesManager"
+ "ide/SafariScrollPatcher"
"ide/settings/index"
"ide/share/index"
"ide/chat/index"
@@ -40,6 +41,7 @@ define [
PdfManager
BinaryFilesManager
ReferencesManager
+ SafariScrollPatcher
) ->
App.controller "IdeController", ($scope, $timeout, ide, localStorage, event_tracking) ->
@@ -135,6 +137,9 @@ define [
catch err
console.error err
+ if ide.browserIsSafari
+ ide.safariScrollPatcher = new SafariScrollPatcher(ide, $scope)
+
# User can append ?ft=somefeature to url to activate a feature toggle
ide.featureToggle = location?.search?.match(/^\?ft=(\w+)$/)?[1]
From 16e8cd7820108369d7544ea0bcf98792ce403fb6 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Fri, 29 Jul 2016 17:45:50 +0100
Subject: [PATCH 067/378] Refactor Safari scroll patcher; ensure it works after
PDF is reloaded.
---
.../coffee/ide/SafariScrollPatcher.coffee | 119 ++++++++++--------
1 file changed, 70 insertions(+), 49 deletions(-)
diff --git a/services/web/public/coffee/ide/SafariScrollPatcher.coffee b/services/web/public/coffee/ide/SafariScrollPatcher.coffee
index 4c77802926..c1535e3d27 100644
--- a/services/web/public/coffee/ide/SafariScrollPatcher.coffee
+++ b/services/web/public/coffee/ide/SafariScrollPatcher.coffee
@@ -2,58 +2,79 @@ define [
], () ->
class SafariScrollPatcher
constructor: (ide, $scope) ->
- @isBootstrapped = false
- @isOverAce = false
+ @isOverAce = false # Flag to control if the pointer is over Ace.
@pdfDiv = null
@aceDiv = null
+ # Start listening to PDF wheel events when the pointer leaves the PDF region.
+ # P.S. This is the problem in a nutshell: although the pointer is elsewhere,
+ # wheel events keep being dispatched to the PDF.
+ @handlePdfDivMouseLeave = () =>
+ console.log "dispatching to ace enabled"
+ @pdfDiv.addEventListener "wheel", @dispatchToAce
+
+ # Stop listening to wheel events when the pointer enters the PDF region. If
+ # the pointer is over the PDF, native behaviour is adequate.
+ @handlePdfDivMouseEnter = () =>
+ console.log "dispatching to ace disabled"
+ @pdfDiv.removeEventListener "wheel", @dispatchToAce
+
+ # Set the "pointer over Ace" flag as false, when the mouse leaves its area.
+ @handleAceDivMouseLeave = () =>
+ @isOverAce = false
+ console.log 'is over ace = ' + @isOverAce
+
+ # Set the "pointer over Ace" flag as true, when the mouse enters its area.
+ @handleAceDivMouseEnter = () =>
+ @isOverAce = true
+ console.log 'is over ace = ' + @isOverAce
+
+ # Grab the elements (pdfDiv, aceDiv) and set the "hover" event listeners.
+ # If elements are already defined, clear existing event listeners and do
+ # the process again (grab elements, set listeners).
+ @setListeners = () =>
+ @isOverAce = false
+
+ # If elements aren't null, remove existing listeners.
+ if @pdfDiv?
+ @pdfDiv.removeEventListener @handlePdfDivMouseLeave
+ @pdfDiv.removeEventListener @handlePdfDivMouseEnter
+
+ if @aceDiv?
+ @aceDiv.removeEventListener @handleAceDivMouseLeave
+ @aceDiv.removeEventListener @handleAceDivMouseEnter
+
+ # Grab elements.
+ @pdfDiv = document.querySelector ".pdfjs-viewer" # Grab the PDF div.
+ @aceDiv = document.querySelector ".ace_content" # Also the editor.
+
+ # Set hover-related listeners.
+ @pdfDiv.addEventListener "mouseleave", @handlePdfDivMouseLeave
+ @pdfDiv.addEventListener "mouseenter", @handlePdfDivMouseEnter
+ @aceDiv.addEventListener "mouseleave", @handleAceDivMouseLeave
+ @aceDiv.addEventListener "mouseenter", @handleAceDivMouseEnter
+
+ # Handler for wheel events on the PDF.
+ # If the pointer is over Ace, grab the event, prevent default behaviour
+ # and dispatch it to Ace.
+ @dispatchToAce = (e) =>
+ if @isOverAce
+ # If this is logged, the problem just happened: the event arrived
+ # here (the PDF wheel handler), but it should've gone to Ace.
+ console.log "Event was bound to the PDF, dispatching to Ace"
+
+ # Small timeout - if we dispatch immediately, an exception is thrown.
+ window.setTimeout(() =>
+ # Dispatch the exact same event to Ace (this will keep values
+ # values e.g. `wheelDelta` consistent with user interaction).
+ @aceDiv.dispatchEvent e
+ , 5)
+
+ # Avoid scrolling the PDF, as we assume this was intended to the
+ # editor.
+ e.preventDefault()
+
$scope.$on "loaded", () =>
- console.log this
- if !@isBootstrapped
- console.log "bootstrapping"
-
- @isBootstrapped = true;
- @pdfDiv = document.querySelector ".pdfjs-viewer" # Grab the PDF div.
- @aceDiv = document.querySelector ".ace_content" # Also the editor.
- @isOverAce = false # Flag to control if the pointer is over Ace.
-
- # Start listening to PDF wheel events when the pointer leaves the PDF region.
- # P.S. This is the problem in a nutshell: although the pointer is elsewhere,
- # wheel events keep being dispatched to the PDF.
- @pdfDiv.addEventListener "mouseleave", () =>
- @pdfDiv.addEventListener "wheel", dispatchToAce
-
- # Stop listening to wheel events when the pointer enters the PDF region. If
- # the pointer is over the PDF, native behaviour is adequate.
- @pdfDiv.addEventListener "mouseenter", () =>
- @pdfDiv.removeEventListener "wheel", dispatchToAce
-
- # Set the "pointer over Ace" flag as false, when the mouse leaves its area.
- @aceDiv.addEventListener "mouseleave", () =>
- @isOverAce = false
-
- # Set the "pointer over Ace" flag as true, when the mouse enters its area.
- @aceDiv.addEventListener "mouseenter", () =>
- @isOverAce = true
-
- # Handler for wheel events on the PDF.
- # If the pointer is over Ace, grab the event, prevent default behaviour
- # and dispatch it to Ace.
- dispatchToAce = (e) =>
- if @isOverAce
- # If this is logged, the problem just happened: the event arrived
- # here (the PDF wheel handler), but it should've gone to Ace.
- console.log "Event was bound to the PDF, dispatching to Ace"
-
- # Small timeout - if we dispatch immediately, an exception is thrown.
- window.setTimeout(() =>
- # Dispatch the exact same event to Ace (this will keep values
- # values e.g. `wheelDelta` consistent with user interaction).
- @aceDiv.dispatchEvent e
- , 5)
-
- # Avoid scrolling the PDF, as we assume this was intended to the
- # editor.
- e.preventDefault()
+ @setListeners()
From 7a8142a43c9d526629c0978edc31ac819fa3dc26 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 09:06:02 +0100
Subject: [PATCH 068/378] remove extraneous `body` parameter
---
services/web/test/acceptance/coffee/ProjectInviteTests.coffee | 2 +-
services/web/test/acceptance/coffee/helpers/User.coffee | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index e915a17d4c..7ea7a4fc9c 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -93,7 +93,7 @@ describe "ProjectInviteTests", ->
Async.series [
(cb) => @user.login cb
(cb) => @sendingUser.login cb
- (cb) => @sendingUser.createProject(@projectName, (err, projectId, project) =>
+ (cb) => @sendingUser.createProject(@projectName, (err, projectId) =>
throw err if err
@projectId = projectId
@fakeProject = {
diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee
index 1d39e3cf92..eecde65322 100644
--- a/services/web/test/acceptance/coffee/helpers/User.coffee
+++ b/services/web/test/acceptance/coffee/helpers/User.coffee
@@ -60,7 +60,7 @@ class User
return callback(error) if error?
if !body?.project_id?
console.error "SOMETHING WENT WRONG CREATING PROJECT", response.statusCode, response.headers["location"], body
- callback(null, body.project_id, body)
+ callback(null, body.project_id)
deleteProject: (project_id, callback=(error)) ->
@request.delete {
From 74c824eddefceb6e1fe36b7d4dbca1145804d7d2 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 09:59:30 +0100
Subject: [PATCH 069/378] Test redirect to /register when user not logged in
---
services/web/config/settings.defaults.coffee | 3 +-
.../coffee/ProjectInviteTests.coffee | 177 ++++++++++++------
2 files changed, 117 insertions(+), 63 deletions(-)
diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee
index f345d14129..fb9257cdef 100644
--- a/services/web/config/settings.defaults.coffee
+++ b/services/web/config/settings.defaults.coffee
@@ -266,7 +266,8 @@ module.exports = settings =
# Should we allow access to any page without logging in? This includes
# public projects, /learn, /templates, about pages, etc.
- allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false
+ # allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false
+ allowPublicAccess: true
# Use a single compile directory for all users in a project
# (otherwise each user has their own directory)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 7ea7a4fc9c..23dd68b698 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -18,6 +18,24 @@ createInvite = (sendingUser, projectId, email, callback=(err, invite)->) ->
return callback(err) if err
callback(null, body.invite)
+createProject = (owner, projectName, callback=(err, projectId, project)->) ->
+ owner.createProject projectName, (err, projectId) ->
+ throw err if err
+ fakeProject = {
+ _id: projectId,
+ name: projectName,
+ owner_ref: owner
+ }
+ callback(err, projectId, fakeProject)
+
+createProjectAndInvite = (owner, projectName, email, callback=(err, project, invite)->) ->
+ createProject owner, projectName, (err, projectId, project) ->
+ return callback(err) if err
+ createInvite owner, projectId, email, (err, invite) ->
+ return callback(err) if err
+ link = CollaboratorsEmailHandler._buildInviteUrl(project, invite)
+ callback(null, project, invite, link)
+
revokeInvite = (sendingUser, projectId, inviteId, callback=(err)->) ->
sendingUser.getCsrfToken (err) ->
return callback(err) if err
@@ -27,6 +45,7 @@ revokeInvite = (sendingUser, projectId, inviteId, callback=(err)->) ->
return callback(err) if err
callback(null)
+
# Actions
tryFollowInviteLink = (user, link, callback=(err, response, body)->) ->
user.request.get {
@@ -71,6 +90,13 @@ expectInvalidInvitePage = (user, link, callback=()->) ->
expect(body).to.match new RegExp("Invalid Invite - .*")
callback()
+expectInviteRedirectToRegister = (user, link, callback=()->) ->
+ tryFollowInviteLink user, link, (err, response, body) ->
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 302
+ expect(response.headers.location).to.match new RegExp("^/register\?.*redir=.*$")
+ callback()
+
expectAcceptInviteAndRedirect = (user, invite, callback=()->) ->
# should accept the invite and redirect to project
tryAcceptInvite user, invite, (err, response, body) =>
@@ -82,86 +108,113 @@ expectAcceptInviteAndRedirect = (user, invite, callback=()->) ->
describe "ProjectInviteTests", ->
before (done) ->
- @timeout(20000)
@sendingUser = new User()
@user = new User()
@site_admin = new User({email: "admin@example.com"})
@email = 'smoketestuser@example.com'
@projectName = 'sharing test'
- @projectId = null
- @fakeProject = null
Async.series [
(cb) => @user.login cb
+ (cb) => @user.logout cb
(cb) => @sendingUser.login cb
- (cb) => @sendingUser.createProject(@projectName, (err, projectId) =>
- throw err if err
- @projectId = projectId
- @fakeProject = {
- _id: projectId,
- name: @projectName,
- owner_ref: @sendingUser
- }
- cb()
- )
], done
- after (done) ->
- Async.series [
- (cb) => @sendingUser.deleteProject(@projectId, cb)
- ], done
-
- describe "user is logged in", ->
+ describe 'clicking the invite link', ->
beforeEach (done) ->
- @user.login (err) =>
- if err
- throw err
- done()
+ @projectId = null
+ @fakeProject = null
+ done()
- describe 'user is not a member of the project', ->
+
+ describe "user is logged in already", ->
beforeEach (done) ->
- @invite = null
- @link = null
- createInvite @sendingUser, @projectId, @email, (err, invite) =>
- @invite = invite
- @link = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, @invite)
- done()
+ Async.series [
+ (cb) =>
+ createProjectAndInvite @sendingUser, @projectName, @email, (err, project, invite, link) =>
+ @projectId = project._id
+ @fakeProject = project
+ @invite = invite
+ @link = link
+ cb()
+ (cb) =>
+ @user.login (err) =>
+ if err
+ throw err
+ cb()
+ ], done
afterEach (done) ->
- revokeInvite @sendingUser, @projectId, @invite._id, (err) =>
- throw err if err
- done()
+ Async.series [
+ (cb) => @sendingUser.deleteProject(@projectId, cb)
+ (cb) => @sendingUser.deleteProject(@projectId, cb)
+ (cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb)
+ ], done
- it 'should not grant access if the user does not accept the invite', (done) ->
- Async.series(
- [
- (cb) =>
- expectInvitePage @user, @link, cb
- (cb) =>
- expectNoProjectAccess @user, @invite.projectId, cb
- ], done
- )
+ # describe 'user is already a member of the project', ->
- it 'should render the invalid-invite page if the token is invalid', (done) ->
- Async.series(
- [
- (cb) =>
- link = @link.replace(@invite.token, 'not_a_real_token')
- expectInvalidInvitePage @user, link, cb
- (cb) =>
- expectNoProjectAccess @user, @invite.projectId, cb
- ], done
- )
+ describe 'user is not a member of the project', ->
- it 'should allow the user to accept the invite and access the project', (done) ->
- Async.series(
- [
- (cb) =>
- expectInvitePage @user, @link, cb
- (cb) =>
- expectAcceptInviteAndRedirect @user, @invite, cb
- (cb) =>
- expectProjectAccess @user, @invite.projectId, cb
+ it 'should not grant access if the user does not accept the invite', (done) ->
+ Async.series(
+ [
+ (cb) =>
+ expectInvitePage @user, @link, cb
+ (cb) =>
+ expectNoProjectAccess @user, @invite.projectId, cb
+ ], done
+ )
+
+ it 'should render the invalid-invite page if the token is invalid', (done) ->
+ Async.series(
+ [
+ (cb) =>
+ link = @link.replace(@invite.token, 'not_a_real_token')
+ expectInvalidInvitePage @user, link, cb
+ (cb) =>
+ expectNoProjectAccess @user, @invite.projectId, cb
+ ], done
+ )
+
+ it 'should allow the user to accept the invite and access the project', (done) ->
+ Async.series(
+ [
+ (cb) =>
+ expectInvitePage @user, @link, cb
+ (cb) =>
+ expectAcceptInviteAndRedirect @user, @invite, cb
+ (cb) =>
+ expectProjectAccess @user, @invite.projectId, cb
+ ], done
+ )
+
+ describe 'user is not logged in initially', ->
+
+ before (done) ->
+ @user.logout done
+
+ beforeEach (done) ->
+ Async.series [
+ (cb) =>
+ createProjectAndInvite @sendingUser, @projectName, @email, (err, project, invite, link) =>
+ @projectId = project._id
+ @fakeProject = project
+ @invite = invite
+ @link = link
+ cb()
+ ], done
+
+ afterEach (done) ->
+ Async.series [
+ (cb) => @sendingUser.deleteProject(@projectId, cb)
+ (cb) => @sendingUser.deleteProject(@projectId, cb)
+ (cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb)
+ ], done
+
+ describe 'user is not a member of the project', ->
+
+ it 'should redirect to the register page', (done) ->
+ Async.series [
+ (cb) => expectInviteRedirectToRegister(@user, @link, cb)
], done
- )
From 9c530e1bb60ac63e5f04c253576082e5cc5170be Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 10:04:42 +0100
Subject: [PATCH 070/378] rename test case
---
services/web/test/acceptance/coffee/ProjectInviteTests.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 23dd68b698..7824bb8fde 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -212,7 +212,7 @@ describe "ProjectInviteTests", ->
(cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb)
], done
- describe 'user is not a member of the project', ->
+ describe 'registration prompt workflow', ->
it 'should redirect to the register page', (done) ->
Async.series [
From 545ce79c71de504e7d5202af0997aeb6f203cfa2 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 10:14:08 +0100
Subject: [PATCH 071/378] Test clicking the invite after already accepting
---
.../coffee/ProjectInviteTests.coffee | 29 ++++++++++++++++++-
1 file changed, 28 insertions(+), 1 deletion(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 7824bb8fde..13afc57a2e 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -91,12 +91,21 @@ expectInvalidInvitePage = (user, link, callback=()->) ->
callback()
expectInviteRedirectToRegister = (user, link, callback=()->) ->
+ # view invite, redirect to `/register`
tryFollowInviteLink user, link, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 302
expect(response.headers.location).to.match new RegExp("^/register\?.*redir=.*$")
callback()
+expectInviteRedirectToProject = (user, link, invite, callback=()->) ->
+ # view invite, redirect straight to project
+ tryFollowInviteLink user, link, (err, response, body) ->
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 302
+ expect(response.headers.location).to.equal "/project/#{invite.projectId}"
+ callback()
+
expectAcceptInviteAndRedirect = (user, invite, callback=()->) ->
# should accept the invite and redirect to project
tryAcceptInvite user, invite, (err, response, body) =>
@@ -152,7 +161,25 @@ describe "ProjectInviteTests", ->
(cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb)
], done
- # describe 'user is already a member of the project', ->
+ describe 'user is already a member of the project', ->
+
+ beforeEach (done) ->
+ Async.series [
+ (cb) =>
+ expectInvitePage @user, @link, cb
+ (cb) =>
+ expectAcceptInviteAndRedirect @user, @invite, cb
+ ], done
+
+ describe 'when user clicks on the invite a second time', ->
+
+ it 'should just redirect to the project page', (done) ->
+ Async.series [
+ (cb) =>
+ expectProjectAccess @user, @invite.projectId, cb
+ (cb) =>
+ expectInviteRedirectToProject @user, @link, @invite, cb
+ ], done
describe 'user is not a member of the project', ->
From 5159cdd0e9aaeb89283a16874137e784921508c7 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 10:57:20 +0100
Subject: [PATCH 072/378] Test when the user recieves second invite to project
---
.../coffee/ProjectInviteTests.coffee | 22 +++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 13afc57a2e..5b8b1c3c5f 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -10,7 +10,7 @@ createInvite = (sendingUser, projectId, email, callback=(err, invite)->) ->
sendingUser.getCsrfToken (err) ->
return callback(err) if err
sendingUser.request.post {
- url: "/project/#{projectId}/invite",
+ uri: "/project/#{projectId}/invite",
json:
email: email
privileges: 'readAndWrite'
@@ -40,7 +40,7 @@ revokeInvite = (sendingUser, projectId, inviteId, callback=(err)->) ->
sendingUser.getCsrfToken (err) ->
return callback(err) if err
sendingUser.request.delete {
- url: "/project/#{projectId}/invite/#{inviteId}",
+ uri: "/project/#{projectId}/invite/#{inviteId}",
}, (err, response, body) ->
return callback(err) if err
callback(null)
@@ -181,6 +181,24 @@ describe "ProjectInviteTests", ->
expectInviteRedirectToProject @user, @link, @invite, cb
], done
+ describe 'when the user recieves another invite to the same project', ->
+
+ it 'should redirect to the project page', (done) ->
+ Async.series [
+ (cb) =>
+ createInvite @sendingUser, @projectId, @email, (err, invite) =>
+ if err
+ throw err
+ @secondInvite = invite
+ @secondLink = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, invite)
+ cb()
+ (cb) =>
+ expectInviteRedirectToProject @user, @secondLink, @secondInvite, cb
+ (cb) =>
+ revokeInvite @sendingUser, @projectId, @secondInvite._id, cb
+ ], done
+
+
describe 'user is not a member of the project', ->
it 'should not grant access if the user does not accept the invite', (done) ->
From 0d4e1e5495113a6eaf3c7f27459533ee17f10f22 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Mon, 1 Aug 2016 11:12:50 +0100
Subject: [PATCH 073/378] Try a smaller timeout + minor fixes.
---
services/web/public/coffee/ide.coffee | 2 +-
.../web/public/coffee/ide/SafariScrollPatcher.coffee | 11 +++--------
2 files changed, 4 insertions(+), 9 deletions(-)
diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee
index d22ec949fb..e62227c9be 100644
--- a/services/web/public/coffee/ide.coffee
+++ b/services/web/public/coffee/ide.coffee
@@ -138,7 +138,7 @@ define [
console.error err
if ide.browserIsSafari
- ide.safariScrollPatcher = new SafariScrollPatcher(ide, $scope)
+ ide.safariScrollPatcher = new SafariScrollPatcher($scope)
# User can append ?ft=somefeature to url to activate a feature toggle
ide.featureToggle = location?.search?.match(/^\?ft=(\w+)$/)?[1]
diff --git a/services/web/public/coffee/ide/SafariScrollPatcher.coffee b/services/web/public/coffee/ide/SafariScrollPatcher.coffee
index c1535e3d27..1859511c99 100644
--- a/services/web/public/coffee/ide/SafariScrollPatcher.coffee
+++ b/services/web/public/coffee/ide/SafariScrollPatcher.coffee
@@ -1,7 +1,7 @@
define [
], () ->
class SafariScrollPatcher
- constructor: (ide, $scope) ->
+ constructor: ($scope) ->
@isOverAce = false # Flag to control if the pointer is over Ace.
@pdfDiv = null
@aceDiv = null
@@ -10,24 +10,20 @@ define [
# P.S. This is the problem in a nutshell: although the pointer is elsewhere,
# wheel events keep being dispatched to the PDF.
@handlePdfDivMouseLeave = () =>
- console.log "dispatching to ace enabled"
@pdfDiv.addEventListener "wheel", @dispatchToAce
# Stop listening to wheel events when the pointer enters the PDF region. If
# the pointer is over the PDF, native behaviour is adequate.
@handlePdfDivMouseEnter = () =>
- console.log "dispatching to ace disabled"
@pdfDiv.removeEventListener "wheel", @dispatchToAce
# Set the "pointer over Ace" flag as false, when the mouse leaves its area.
@handleAceDivMouseLeave = () =>
@isOverAce = false
- console.log 'is over ace = ' + @isOverAce
# Set the "pointer over Ace" flag as true, when the mouse enters its area.
@handleAceDivMouseEnter = () =>
@isOverAce = true
- console.log 'is over ace = ' + @isOverAce
# Grab the elements (pdfDiv, aceDiv) and set the "hover" event listeners.
# If elements are already defined, clear existing event listeners and do
@@ -46,7 +42,7 @@ define [
# Grab elements.
@pdfDiv = document.querySelector ".pdfjs-viewer" # Grab the PDF div.
- @aceDiv = document.querySelector ".ace_content" # Also the editor.
+ @aceDiv = document.querySelector ".ace_content" # Also the editor.
# Set hover-related listeners.
@pdfDiv.addEventListener "mouseleave", @handlePdfDivMouseLeave
@@ -61,14 +57,13 @@ define [
if @isOverAce
# If this is logged, the problem just happened: the event arrived
# here (the PDF wheel handler), but it should've gone to Ace.
- console.log "Event was bound to the PDF, dispatching to Ace"
# Small timeout - if we dispatch immediately, an exception is thrown.
window.setTimeout(() =>
# Dispatch the exact same event to Ace (this will keep values
# values e.g. `wheelDelta` consistent with user interaction).
@aceDiv.dispatchEvent e
- , 5)
+ , 1)
# Avoid scrolling the PDF, as we assume this was intended to the
# editor.
From 69bd954001ed4094861f135a36327501c8c0999f Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 12:14:34 +0100
Subject: [PATCH 074/378] test the registration workflow
---
.../coffee/ProjectInviteTests.coffee | 53 ++++++++++++++++---
1 file changed, 45 insertions(+), 8 deletions(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 5b8b1c3c5f..8c1fbf4835 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -60,21 +60,32 @@ tryAcceptInvite = (user, invite, callback=(err, response, body)->) ->
token: invite.token
}, callback
+tryRegisterUser = (user, email, redir, callback=(err, response, body)->) ->
+ user.getCsrfToken (error) =>
+ return callback(error) if error?
+ user.request.post {
+ url: "/register"
+ json:
+ email: email
+ password: "some_weird_password"
+ redir: redir
+ }, callback
+
# Expectations
-expectProjectAccess = (user, projectId, callback=()->) ->
+expectProjectAccess = (user, projectId, callback=(err,result)->) ->
# should have access to project
user.openProject projectId, (err) =>
expect(err).to.be.oneOf [null, undefined]
callback()
-expectNoProjectAccess = (user, projectId, callback=()->) ->
+expectNoProjectAccess = (user, projectId, callback=(err,result)->) ->
# should not have access to project page
user.openProject projectId, (err) =>
expect(err).to.be.instanceof Error
callback()
-expectInvitePage = (user, link, callback=()->) ->
+expectInvitePage = (user, link, callback=(err,result)->) ->
# view invite
tryFollowInviteLink user, link, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
@@ -82,7 +93,7 @@ expectInvitePage = (user, link, callback=()->) ->
expect(body).to.match new RegExp("Project Invite - .*")
callback()
-expectInvalidInvitePage = (user, link, callback=()->) ->
+expectInvalidInvitePage = (user, link, callback=(err,result)->) ->
# view invalid invite
tryFollowInviteLink user, link, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
@@ -90,15 +101,27 @@ expectInvalidInvitePage = (user, link, callback=()->) ->
expect(body).to.match new RegExp("Invalid Invite - .*")
callback()
-expectInviteRedirectToRegister = (user, link, callback=()->) ->
+expectInviteRedirectToRegister = (user, link, callback=(err,result)->) ->
# view invite, redirect to `/register`
tryFollowInviteLink user, link, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
expect(response.statusCode).to.equal 302
expect(response.headers.location).to.match new RegExp("^/register\?.*redir=.*$")
- callback()
+ # follow redirect to register page and extract the redirectUrl from form
+ user.request.get response.headers.location, (err, response, body) ->
+ redirectUrl = body.match(/input name="redir" type="hidden" value="([^"]*)"/m)?[1]
+ expect(redirectUrl).to.not.be.oneOf [null, undefined]
+ callback(null, redirectUrl)
-expectInviteRedirectToProject = (user, link, invite, callback=()->) ->
+expectRegistrationRedirectToInvite = (user, email, redir, link, callback=(err, result)->) ->
+ tryRegisterUser user, email, redir, (err, response, body) ->
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 200
+ expect(link).to.match new RegExp("^.*#{body.redir}\?.*$")
+ callback(null, null)
+
+
+expectInviteRedirectToProject = (user, link, invite, callback=(err,result)->) ->
# view invite, redirect straight to project
tryFollowInviteLink user, link, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
@@ -106,7 +129,7 @@ expectInviteRedirectToProject = (user, link, invite, callback=()->) ->
expect(response.headers.location).to.equal "/project/#{invite.projectId}"
callback()
-expectAcceptInviteAndRedirect = (user, invite, callback=()->) ->
+expectAcceptInviteAndRedirect = (user, invite, callback=(err,result)->) ->
# should accept the invite and redirect to project
tryAcceptInvite user, invite, (err, response, body) =>
expect(err).to.be.oneOf [null, undefined]
@@ -263,3 +286,17 @@ describe "ProjectInviteTests", ->
Async.series [
(cb) => expectInviteRedirectToRegister(@user, @link, cb)
], done
+
+ it 'should allow user to accept the invite if the user registers a new account', (done) ->
+ Async.series [
+ (cb) =>
+ expectInviteRedirectToRegister @user, @link, (err, redirectUrl) =>
+ @_redir = redirectUrl
+ cb()
+ (cb) =>
+ expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, @link, cb
+ (cb) =>
+ expectInvitePage @user, @link, cb
+ (cb) =>
+ expectAcceptInviteAndRedirect @user, @invite, cb
+ ], done
From 5f1aa4cc582974728f8d01b507c20a566f07cfe8 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 13:30:43 +0100
Subject: [PATCH 075/378] test registration with invalid token
---
.../coffee/ProjectInviteTests.coffee | 27 ++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 8c1fbf4835..17fbea57c0 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -280,7 +280,7 @@ describe "ProjectInviteTests", ->
(cb) => revokeInvite(@sendingUser, @projectId, @invite._id, cb)
], done
- describe 'registration prompt workflow', ->
+ describe 'registration prompt workflow with valid token', ->
it 'should redirect to the register page', (done) ->
Async.series [
@@ -300,3 +300,28 @@ describe "ProjectInviteTests", ->
(cb) =>
expectAcceptInviteAndRedirect @user, @invite, cb
], done
+
+ describe 'registration prompt workflow with non-valid token', ->
+
+ before (done)->
+ @user.logout done
+
+ it 'should redirect to the register page', (done) ->
+ Async.series [
+ (cb) => expectInviteRedirectToRegister(@user, @link, cb)
+ ], done
+
+ it 'should display invalid-invite if the user registers a new account', (done) ->
+ badLink = @link.replace(@invite.token, 'not_a_real_token')
+ Async.series [
+ (cb) =>
+ expectInviteRedirectToRegister @user, badLink, (err, redirectUrl) =>
+ @_redir = redirectUrl
+ cb()
+ (cb) =>
+ expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, badLink, cb
+ (cb) =>
+ expectInvalidInvitePage @user, badLink, cb
+ (cb) =>
+ expectNoProjectAccess @user, @invite.projectId, cb
+ ], done
From 4b8ab2dbba64f950e28cbf4834189b20def391ac Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Mon, 1 Aug 2016 13:35:49 +0100
Subject: [PATCH 076/378] More comments.
---
services/web/public/coffee/ide/SafariScrollPatcher.coffee | 3 +++
1 file changed, 3 insertions(+)
diff --git a/services/web/public/coffee/ide/SafariScrollPatcher.coffee b/services/web/public/coffee/ide/SafariScrollPatcher.coffee
index 1859511c99..07ad2e7565 100644
--- a/services/web/public/coffee/ide/SafariScrollPatcher.coffee
+++ b/services/web/public/coffee/ide/SafariScrollPatcher.coffee
@@ -69,6 +69,9 @@ define [
# editor.
e.preventDefault()
+ # "loaded" event is emitted from the pdfViewer controller $scope. This means
+ # that the previous PDF DOM element was destroyed and a new one is available,
+ # so we need to grab the elements and set the listeners again.
$scope.$on "loaded", () =>
@setListeners()
From 263822d665738fa0a1d86e0f52f26a6b6cb86931 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 13:54:49 +0100
Subject: [PATCH 077/378] Also parse out login url
---
services/web/test/acceptance/coffee/ProjectInviteTests.coffee | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 17fbea57c0..b3bcb762e9 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -110,8 +110,10 @@ expectInviteRedirectToRegister = (user, link, callback=(err,result)->) ->
# follow redirect to register page and extract the redirectUrl from form
user.request.get response.headers.location, (err, response, body) ->
redirectUrl = body.match(/input name="redir" type="hidden" value="([^"]*)"/m)?[1]
+ loginUrl = body.match(/href="([^"]*)">\s*Login here/m)?[1]
expect(redirectUrl).to.not.be.oneOf [null, undefined]
- callback(null, redirectUrl)
+ expect(loginUrl).to.not.be.oneOf [null, undefined]
+ callback(null, redirectUrl, loginUrl)
expectRegistrationRedirectToInvite = (user, email, redir, link, callback=(err, result)->) ->
tryRegisterUser user, email, redir, (err, response, body) ->
From 8af1a7b17a4cbc1b7bf136690514ec0a04a4ee87 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 15:16:03 +0100
Subject: [PATCH 078/378] Test login workflow
---
.../coffee/ProjectInviteTests.coffee | 74 +++++++++++++++++--
1 file changed, 68 insertions(+), 6 deletions(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index b3bcb762e9..5592eb92e6 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -71,6 +71,22 @@ tryRegisterUser = (user, email, redir, callback=(err, response, body)->) ->
redir: redir
}, callback
+tryFollowLoginLink = (user, loginLink, callback=(err, response, body)->) ->
+ user.getCsrfToken (error) =>
+ return callback(error) if error?
+ user.request.get loginLink, callback
+
+tryLoginUser = (user, redir, callback=(err, response, body)->) ->
+ user.getCsrfToken (error) =>
+ return callback(error) if error?
+ user.request.post {
+ url: "/login"
+ json:
+ email: user.email
+ password: user.password
+ redir: redir
+ }, callback
+
# Expectations
expectProjectAccess = (user, projectId, callback=(err,result)->) ->
@@ -115,6 +131,21 @@ expectInviteRedirectToRegister = (user, link, callback=(err,result)->) ->
expect(loginUrl).to.not.be.oneOf [null, undefined]
callback(null, redirectUrl, loginUrl)
+expectLoginPage = (user, loginLink, callback=(err, result)->) ->
+ tryFollowLoginLink user, loginLink, (err, response, body) ->
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 200
+ expect(body).to.match new RegExp("Login - .*")
+ redirectUrl = body.match(/input name="redir" type="hidden" value="([^"]*)"/m)?[1]
+ callback(null, redirectUrl)
+
+expectLoginRedirectToInvite = (user, redir, link, callback=(err, result)->) ->
+ tryLoginUser user, redir, (err, response, body) ->
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 200
+ expect(link).to.match new RegExp("^.*#{body.redir}\?.*$")
+ callback(null, null)
+
expectRegistrationRedirectToInvite = (user, email, redir, link, callback=(err, result)->) ->
tryRegisterUser user, email, redir, (err, response, body) ->
expect(err).to.be.oneOf [null, undefined]
@@ -320,10 +351,41 @@ describe "ProjectInviteTests", ->
expectInviteRedirectToRegister @user, badLink, (err, redirectUrl) =>
@_redir = redirectUrl
cb()
- (cb) =>
- expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, badLink, cb
- (cb) =>
- expectInvalidInvitePage @user, badLink, cb
- (cb) =>
- expectNoProjectAccess @user, @invite.projectId, cb
+ (cb) => expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, badLink, cb
+ (cb) => expectInvalidInvitePage @user, badLink, cb
+ (cb) => expectNoProjectAccess @user, @invite.projectId, cb
+ ], done
+
+ describe 'login workflow with valid token', ->
+
+ before (done)->
+ @user.logout done
+
+ it 'should redirect to the register page', (done) ->
+ Async.series [
+ (cb) => expectInviteRedirectToRegister(@user, @link, cb)
+ ], done
+
+ it 'should allow the user to login to view the invite', (done) ->
+ Async.series [
+ (cb) =>
+ expectInviteRedirectToRegister @user, @link, (err, redirectUrl, loginUrl) =>
+ @_redir = redirectUrl
+ @_loginLink = loginUrl
+ cb()
+ (cb) =>
+ expectLoginPage @user, @_loginLink, (err, redirectUrl) =>
+ expect(@_redir).to.equal redirectUrl
+ cb()
+ (cb) => expectLoginRedirectToInvite @user, @_redir, @link, cb
+ (cb) => expectInvitePage @user, @link, cb
+ (cb) => expectNoProjectAccess @user, @invite.projectId, cb
+ ], done
+
+
+ it 'should allow user to accept the invite if the user registers a new account', (done) ->
+ Async.series [
+ (cb) => expectInvitePage @user, @link, cb
+ (cb) => expectAcceptInviteAndRedirect @user, @invite, cb
+ (cb) => expectProjectAccess @user, @invite.projectId, cb
], done
From 495bc1bcd3a13e6e20318be4e9118b03fd569a84 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 15:16:10 +0100
Subject: [PATCH 079/378] Refactor
---
.../coffee/ProjectInviteTests.coffee | 44 +++++++------------
1 file changed, 15 insertions(+), 29 deletions(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 5592eb92e6..2917fdff43 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -153,7 +153,6 @@ expectRegistrationRedirectToInvite = (user, email, redir, link, callback=(err, r
expect(link).to.match new RegExp("^.*#{body.redir}\?.*$")
callback(null, null)
-
expectInviteRedirectToProject = (user, link, invite, callback=(err,result)->) ->
# view invite, redirect straight to project
tryFollowInviteLink user, link, (err, response, body) ->
@@ -221,20 +220,16 @@ describe "ProjectInviteTests", ->
beforeEach (done) ->
Async.series [
- (cb) =>
- expectInvitePage @user, @link, cb
- (cb) =>
- expectAcceptInviteAndRedirect @user, @invite, cb
+ (cb) => expectInvitePage @user, @link, cb
+ (cb) => expectAcceptInviteAndRedirect @user, @invite, cb
], done
describe 'when user clicks on the invite a second time', ->
it 'should just redirect to the project page', (done) ->
Async.series [
- (cb) =>
- expectProjectAccess @user, @invite.projectId, cb
- (cb) =>
- expectInviteRedirectToProject @user, @link, @invite, cb
+ (cb) => expectProjectAccess @user, @invite.projectId, cb
+ (cb) => expectInviteRedirectToProject @user, @link, @invite, cb
], done
describe 'when the user recieves another invite to the same project', ->
@@ -248,10 +243,8 @@ describe "ProjectInviteTests", ->
@secondInvite = invite
@secondLink = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, invite)
cb()
- (cb) =>
- expectInviteRedirectToProject @user, @secondLink, @secondInvite, cb
- (cb) =>
- revokeInvite @sendingUser, @projectId, @secondInvite._id, cb
+ (cb) => expectInviteRedirectToProject @user, @secondLink, @secondInvite, cb
+ (cb) => revokeInvite @sendingUser, @projectId, @secondInvite._id, cb
], done
@@ -260,10 +253,8 @@ describe "ProjectInviteTests", ->
it 'should not grant access if the user does not accept the invite', (done) ->
Async.series(
[
- (cb) =>
- expectInvitePage @user, @link, cb
- (cb) =>
- expectNoProjectAccess @user, @invite.projectId, cb
+ (cb) => expectInvitePage @user, @link, cb
+ (cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done
)
@@ -281,12 +272,9 @@ describe "ProjectInviteTests", ->
it 'should allow the user to accept the invite and access the project', (done) ->
Async.series(
[
- (cb) =>
- expectInvitePage @user, @link, cb
- (cb) =>
- expectAcceptInviteAndRedirect @user, @invite, cb
- (cb) =>
- expectProjectAccess @user, @invite.projectId, cb
+ (cb) => expectInvitePage @user, @link, cb
+ (cb) => expectAcceptInviteAndRedirect @user, @invite, cb
+ (cb) => expectProjectAccess @user, @invite.projectId, cb
], done
)
@@ -326,12 +314,10 @@ describe "ProjectInviteTests", ->
expectInviteRedirectToRegister @user, @link, (err, redirectUrl) =>
@_redir = redirectUrl
cb()
- (cb) =>
- expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, @link, cb
- (cb) =>
- expectInvitePage @user, @link, cb
- (cb) =>
- expectAcceptInviteAndRedirect @user, @invite, cb
+ (cb) => expectRegistrationRedirectToInvite @user, "some_email@example.com", @_redir, @link, cb
+ (cb) => expectInvitePage @user, @link, cb
+ (cb) => expectAcceptInviteAndRedirect @user, @invite, cb
+ (cb) => expectProjectAccess @user, @invite.projectId, cb
], done
describe 'registration prompt workflow with non-valid token', ->
From 9e0ff3f6282dce7c3665ea6558d6318e28d103c6 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 15:21:06 +0100
Subject: [PATCH 080/378] test when the token is invalid
---
.../coffee/ProjectInviteTests.coffee | 28 ++++++++++++++++++-
1 file changed, 27 insertions(+), 1 deletion(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 2917fdff43..85e9c2ced3 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -368,10 +368,36 @@ describe "ProjectInviteTests", ->
(cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done
-
it 'should allow user to accept the invite if the user registers a new account', (done) ->
Async.series [
(cb) => expectInvitePage @user, @link, cb
(cb) => expectAcceptInviteAndRedirect @user, @invite, cb
(cb) => expectProjectAccess @user, @invite.projectId, cb
], done
+
+ describe 'login workflow with non-valid token', ->
+
+ before (done)->
+ @user.logout done
+
+ it 'should redirect to the register page', (done) ->
+ Async.series [
+ (cb) => expectInviteRedirectToRegister(@user, @link, cb)
+ ], done
+
+ it 'should show the invalid-invite page once the user has logged in', (done) ->
+ badLink = @link.replace(@invite.token, 'not_a_real_token')
+ Async.series [
+ (cb) =>
+ expectInviteRedirectToRegister @user, badLink, (err, redirectUrl, loginUrl) =>
+ @_redir = redirectUrl
+ @_loginLink = loginUrl
+ cb()
+ (cb) =>
+ expectLoginPage @user, @_loginLink, (err, redirectUrl) =>
+ expect(@_redir).to.equal redirectUrl
+ cb()
+ (cb) => expectLoginRedirectToInvite @user, @_redir, badLink, cb
+ (cb) => expectInvalidInvitePage @user, badLink, cb
+ (cb) => expectNoProjectAccess @user, @invite.projectId, cb
+ ], done
From 9787edd716ecc2e80c63dd3d8f5d18d22335429e Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 15:55:56 +0100
Subject: [PATCH 081/378] Add more assertions about project access
---
.../test/acceptance/coffee/ProjectInviteTests.coffee | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 85e9c2ced3..74bced1c5a 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -222,6 +222,7 @@ describe "ProjectInviteTests", ->
Async.series [
(cb) => expectInvitePage @user, @link, cb
(cb) => expectAcceptInviteAndRedirect @user, @invite, cb
+ (cb) => expectProjectAccess @user, @invite.projectId, cb
], done
describe 'when user clicks on the invite a second time', ->
@@ -230,6 +231,7 @@ describe "ProjectInviteTests", ->
Async.series [
(cb) => expectProjectAccess @user, @invite.projectId, cb
(cb) => expectInviteRedirectToProject @user, @link, @invite, cb
+ (cb) => expectProjectAccess @user, @invite.projectId, cb
], done
describe 'when the user recieves another invite to the same project', ->
@@ -244,6 +246,7 @@ describe "ProjectInviteTests", ->
@secondLink = CollaboratorsEmailHandler._buildInviteUrl(@fakeProject, invite)
cb()
(cb) => expectInviteRedirectToProject @user, @secondLink, @secondInvite, cb
+ (cb) => expectProjectAccess @user, @invite.projectId, cb
(cb) => revokeInvite @sendingUser, @projectId, @secondInvite._id, cb
], done
@@ -264,8 +267,8 @@ describe "ProjectInviteTests", ->
(cb) =>
link = @link.replace(@invite.token, 'not_a_real_token')
expectInvalidInvitePage @user, link, cb
- (cb) =>
- expectNoProjectAccess @user, @invite.projectId, cb
+ (cb) => expectNoProjectAccess @user, @invite.projectId, cb
+ (cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done
)
@@ -328,6 +331,7 @@ describe "ProjectInviteTests", ->
it 'should redirect to the register page', (done) ->
Async.series [
(cb) => expectInviteRedirectToRegister(@user, @link, cb)
+ (cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done
it 'should display invalid-invite if the user registers a new account', (done) ->
@@ -350,6 +354,7 @@ describe "ProjectInviteTests", ->
it 'should redirect to the register page', (done) ->
Async.series [
(cb) => expectInviteRedirectToRegister(@user, @link, cb)
+ (cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done
it 'should allow the user to login to view the invite', (done) ->
@@ -383,6 +388,7 @@ describe "ProjectInviteTests", ->
it 'should redirect to the register page', (done) ->
Async.series [
(cb) => expectInviteRedirectToRegister(@user, @link, cb)
+ (cb) => expectNoProjectAccess @user, @invite.projectId, cb
], done
it 'should show the invalid-invite page once the user has logged in', (done) ->
From 291a26595cf4ae7ea06becfa68e845ecb660be85 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 15:56:07 +0100
Subject: [PATCH 082/378] Remove referal id from invite email link
---
.../Features/Collaborators/CollaboratorsEmailHandler.coffee | 2 --
1 file changed, 2 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
index a6ea518815..f669d85de4 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
@@ -9,8 +9,6 @@ module.exports = CollaboratorsEmailHandler =
"#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [
"project_name=#{encodeURIComponent(project.name)}"
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
- "r=#{project.owner_ref.referal_id}" # Referal
- "rs=ci" # referral source = collaborator invite
].join("&")
notifyUserOfProjectShare: (project_id, email, callback)->
From a6b8bf6ece9decf82a2ccf49edd322e50d5fe2c0 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 16:06:56 +0100
Subject: [PATCH 083/378] Undo debug change
---
services/web/config/settings.defaults.coffee | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee
index fb9257cdef..f345d14129 100644
--- a/services/web/config/settings.defaults.coffee
+++ b/services/web/config/settings.defaults.coffee
@@ -266,8 +266,7 @@ module.exports = settings =
# Should we allow access to any page without logging in? This includes
# public projects, /learn, /templates, about pages, etc.
- # allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false
- allowPublicAccess: true
+ allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false
# Use a single compile directory for all users in a project
# (otherwise each user has their own directory)
From c6334ffab719d7120e175c4bb3a8341a203a95f4 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Mon, 1 Aug 2016 16:35:28 +0100
Subject: [PATCH 084/378] Add Angular filter for wrapping words larger than N
characters.
---
.../public/coffee/filters/wrapLongWords.coffee | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
create mode 100644 services/web/public/coffee/filters/wrapLongWords.coffee
diff --git a/services/web/public/coffee/filters/wrapLongWords.coffee b/services/web/public/coffee/filters/wrapLongWords.coffee
new file mode 100644
index 0000000000..b639de763b
--- /dev/null
+++ b/services/web/public/coffee/filters/wrapLongWords.coffee
@@ -0,0 +1,17 @@
+define [
+ "base"
+], (App) ->
+ DEF_MIN_LENGTH = 20
+
+ _getWrappedWordsString = (baseStr, wrapperElName, minLength) ->
+ minLength = minLength || DEF_MIN_LENGTH
+
+ findWordsRegEx = new RegExp "\\w{#{minLength},}", "g"
+ wrappingTemplate = "<#{wrapperElName} style='word-break: break-all;'>$&#{wrapperElName}>"
+
+ baseStr.replace findWordsRegEx, wrappingTemplate
+
+
+ App.filter "wrapLongWords", () ->
+ (input, minLength) ->
+ _getWrappedWordsString input, "span", minLength
\ No newline at end of file
From ff62e505301b75e3f85108381b4f5cead9508539 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Mon, 1 Aug 2016 16:42:54 +0100
Subject: [PATCH 085/378] Integrate word wrapping filter in the chat component.
---
services/web/app/views/project/editor/chat.jade | 2 +-
services/web/public/coffee/filters/wrapLongWords.coffee | 2 +-
services/web/public/coffee/ide/chat/index.coffee | 1 +
services/web/public/stylesheets/app/editor/chat.less | 5 +++++
4 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/services/web/app/views/project/editor/chat.jade b/services/web/app/views/project/editor/chat.jade
index 538a5b9d37..86842c36de 100644
--- a/services/web/app/views/project/editor/chat.jade
+++ b/services/web/app/views/project/editor/chat.jade
@@ -45,7 +45,7 @@ aside.chat(
mathjax,
ng-repeat="content in message.contents track by $index"
)
- span(ng-bind-html="content | linky:'_blank'")
+ span(ng-bind-html="content | linky:'_blank' | wrapLongWords")
.new-message
textarea(
diff --git a/services/web/public/coffee/filters/wrapLongWords.coffee b/services/web/public/coffee/filters/wrapLongWords.coffee
index b639de763b..ea97a58fec 100644
--- a/services/web/public/coffee/filters/wrapLongWords.coffee
+++ b/services/web/public/coffee/filters/wrapLongWords.coffee
@@ -7,7 +7,7 @@ define [
minLength = minLength || DEF_MIN_LENGTH
findWordsRegEx = new RegExp "\\w{#{minLength},}", "g"
- wrappingTemplate = "<#{wrapperElName} style='word-break: break-all;'>$&#{wrapperElName}>"
+ wrappingTemplate = "<#{wrapperElName} class=\"break-word\">$&#{wrapperElName}>"
baseStr.replace findWordsRegEx, wrappingTemplate
diff --git a/services/web/public/coffee/ide/chat/index.coffee b/services/web/public/coffee/ide/chat/index.coffee
index d5a537903b..de9c46d62d 100644
--- a/services/web/public/coffee/ide/chat/index.coffee
+++ b/services/web/public/coffee/ide/chat/index.coffee
@@ -3,4 +3,5 @@ define [
"ide/chat/controllers/ChatController"
"ide/chat/controllers/ChatMessageController"
"ide/chat/directives/mathjax"
+ "filters/wrapLongWords"
], () ->
\ No newline at end of file
diff --git a/services/web/public/stylesheets/app/editor/chat.less b/services/web/public/stylesheets/app/editor/chat.less
index 495ac6dce0..7e1ad95b8d 100644
--- a/services/web/public/stylesheets/app/editor/chat.less
+++ b/services/web/public/stylesheets/app/editor/chat.less
@@ -129,6 +129,11 @@
}
}
+.break-word {
+ word-break: break-all;
+ color: red;
+}
+
.editor-dark {
.chat {
.new-message {
From dca1c9be5dc707d4484dddb614440d0e7a27b20e Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 1 Aug 2016 17:05:37 +0100
Subject: [PATCH 086/378] Load invites on project load, rather than
asynchronously.
---
.../CollaboratorsInviteController.coffee | 2 +-
.../CollaboratorsInviteHandler.coffee | 2 +-
.../Editor/EditorHttpController.coffee | 19 ++++++++++---------
.../Project/ProjectEditorHandler.coffee | 5 +++--
.../web/app/views/project/editor/share.jade | 2 +-
.../ShareProjectModalController.coffee | 15 +++------------
.../ide/share/services/projectMembers.coffee | 7 -------
.../Editor/EditorHttpControllerTests.coffee | 13 ++++++++++++-
.../Project/ProjectEditorHandlerTests.coffee | 10 +++++++++-
9 files changed, 40 insertions(+), 35 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index f86d46e151..93c84f4614 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -95,7 +95,7 @@ module.exports = CollaboratorsInviteController =
inviteId = req.params.invite_id
{token} = req.body
currentUser = req.session.user
- logger.log {projectId, inviteId}, "accepting invite"
+ logger.log {projectId, inviteId, userId: currentUser._id}, "accepting invite"
CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) ->
if err?
logger.err {projectId, inviteId}, "error accepting invite by token"
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 8118f7b135..c83433e880 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -54,7 +54,7 @@ module.exports = CollaboratorsInviteHandler =
if err?
logger.err {err, projectId}, "error fetching invite"
return callback(err)
- if !invite
+ if !invite?
logger.err {err, projectId, token: tokenString}, "no invite found"
return callback(null, null)
callback(null, invite)
diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
index 379f20fe5b..f467d7a9bf 100644
--- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
+++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
@@ -9,6 +9,7 @@ AuthorizationManager = require("../Authorization/AuthorizationManager")
ProjectEditorHandler = require('../Project/ProjectEditorHandler')
Metrics = require('../../infrastructure/Metrics')
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
+CollaboratorsInviteHandler = require("../Collaborators/CollaboratorsInviteHandler")
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
module.exports = EditorHttpController =
@@ -39,13 +40,15 @@ module.exports = EditorHttpController =
return callback(error) if error?
AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) ->
return callback(error) if error?
- if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
- callback null, null, false
- else
- callback(null,
- ProjectEditorHandler.buildProjectModelView(project, members),
- privilegeLevel
- )
+ CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) ->
+ return callback(error) if error?
+ if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
+ callback null, null, false
+ else
+ callback(null,
+ ProjectEditorHandler.buildProjectModelView(project, members, invites),
+ privilegeLevel
+ )
restoreDoc: (req, res, next) ->
project_id = req.params.Project_id
@@ -135,5 +138,3 @@ module.exports = EditorHttpController =
EditorController.deleteEntity project_id, entity_id, entity_type, "editor", (error) ->
return next(error) if error?
res.sendStatus 204
-
-
diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
index 4e4991a855..28d637eab6 100644
--- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
@@ -1,7 +1,7 @@
_ = require("underscore")
module.exports = ProjectEditorHandler =
- buildProjectModelView: (project, members) ->
+ buildProjectModelView: (project, members, invites) ->
result =
_id : project._id
name : project.name
@@ -15,7 +15,8 @@ module.exports = ProjectEditorHandler =
deletedByExternalDataSource : project.deletedByExternalDataSource || false
deletedDocs: project.deletedDocs
members: []
-
+ invites: invites || []
+
owner = null
for member in members
if member.privilegeLevel == "owner"
diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade
index 8de9af28d2..a39f66055f 100644
--- a/services/web/app/views/project/editor/share.jade
+++ b/services/web/app/views/project/editor/share.jade
@@ -43,7 +43,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
ng-click="removeMember(member)"
)
i.fa.fa-times
- .row.project-invite(ng-repeat="invite in state.invites")
+ .row.project-invite(ng-repeat="invite in project.invites")
.col-xs-8 {{ invite.email }}
span.label.label-primary #{translate("pending")}
.col-xs-3.text-left
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index 4f3adbdcfd..dc7e8a630e 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -14,7 +14,6 @@ define [
}
$modalInstance.opened.then () ->
- getOutstandingInvites()
$timeout () ->
$scope.$broadcast "open"
, 200
@@ -44,14 +43,6 @@ define [
getCurrentMemberEmails = () ->
$scope.project.members.map (u) -> u.email
- getOutstandingInvites = (callback) ->
- projectInvites.getInvites().then(
- (response) ->
- $scope.state.invites = response?.data?.invites
- , (response) ->
- console.error response
- )
-
$scope.filterAutocompleteUsers = ($query) ->
currentMemberEmails = getCurrentMemberEmails()
return $scope.autocompleteContacts.filter (contact) ->
@@ -95,7 +86,7 @@ define [
.success (data) ->
if data.invite
invite = data.invite
- $scope.state.invites.push invite
+ $scope.project.invites.push invite
else
if data.users?
users = data.users
@@ -137,9 +128,9 @@ define [
.revokeInvite(invite._id)
.success () ->
$scope.state.inflight = false
- index = $scope.state.invites.indexOf(invite)
+ index = $scope.project.invites.indexOf(invite)
return if index == -1
- $scope.state.invites.splice(index, 1)
+ $scope.project.invites.splice(index, 1)
.error () ->
$scope.state.inflight = false
$scope.state.error = "Sorry, something went wrong :("
diff --git a/services/web/public/coffee/ide/share/services/projectMembers.coffee b/services/web/public/coffee/ide/share/services/projectMembers.coffee
index 4bc69e814f..3f86b45cd4 100644
--- a/services/web/public/coffee/ide/share/services/projectMembers.coffee
+++ b/services/web/public/coffee/ide/share/services/projectMembers.coffee
@@ -11,13 +11,6 @@ define [
"X-Csrf-Token": window.csrfToken
})
- addMember: (email, privileges) ->
- $http.post("/project/#{ide.project_id}/users", {
- email: email
- privileges: privileges
- _csrf: window.csrfToken
- })
-
addGroup: (group_id, privileges) ->
$http.post("/project/#{ide.project_id}/group", {
group_id: group_id
diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee
index a98c34af28..3134cea34b 100644
--- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee
@@ -17,7 +17,8 @@ describe "EditorHttpController", ->
"./EditorController": @EditorController = {}
'../../infrastructure/Metrics': @Metrics = {inc: sinon.stub()}
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {}
-
+ "../Collaborators/CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
+
@project_id = "mock-project-id"
@doc_id = "mock-doc-id"
@user_id = "mock-user-id"
@@ -102,9 +103,14 @@ describe "EditorHttpController", ->
_id: @project_id
owner:{_id:"something"}
view: true
+ @invites = [
+ {_id: "invite_one", email: "user-one@example.com", privileges: "readOnly", projectId: @project._id}
+ {_id: "invite_two", email: "user-two@example.com", privileges: "readOnly", projectId: @project._id}
+ ]
@ProjectEditorHandler.buildProjectModelView = sinon.stub().returns(@projectModelView)
@ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project)
@CollaboratorsHandler.getMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, null, @members)
+ @CollaboratorsInviteHandler.getAllInvites = sinon.stub().callsArgWith(1, null, @invites)
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
describe "when authorized", ->
@@ -133,6 +139,11 @@ describe "EditorHttpController", ->
.calledWith(@user_id, @project_id)
.should.equal true
+ it 'should include the invites', ->
+ @CollaboratorsInviteHandler.getAllInvites
+ .calledWith(@project._id)
+ .should.equal true
+
it "should return the project model view, privilege level and protocol version", ->
@callback.calledWith(null, @projectModelView, "owner").should.equal true
diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee
index eede50bfd3..671ae4a65b 100644
--- a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee
@@ -67,12 +67,16 @@ describe "ProjectEditorHandler", ->
},
privilegeLevel: "readAndWrite"
}]
+ @invites = [
+ {_id: "invite_one", email: "user-one@example.com", privileges: "readOnly", projectId: @project._id}
+ {_id: "invite_two", email: "user-two@example.com", privileges: "readOnly", projectId: @project._id}
+ ]
@handler = SandboxedModule.require modulePath
describe "buildProjectModelView", ->
describe "with owner and members included", ->
beforeEach ->
- @result = @handler.buildProjectModelView @project, @members
+ @result = @handler.buildProjectModelView @project, @members, @invites
it "should include the id", ->
should.exist @result._id
@@ -144,6 +148,10 @@ describe "ProjectEditorHandler", ->
@result.rootFolder[0].folders[0].docs[0].name.should.equal "main.tex"
should.not.exist @result.rootFolder[0].folders[0].docs[0].lines
+ it 'should include invites', ->
+ should.exist @result.invites
+ @result.invites.should.deep.equal @invites
+
describe "deletedByExternalDataSource", ->
it "should set the deletedByExternalDataSource flag to false when it is not there", ->
From 63f8fe453a109e32b7671171b53a0f401c29a0bc Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 2 Aug 2016 09:48:09 +0100
Subject: [PATCH 087/378] Use UserGetter rather than User model
---
.../CollaboratorsInviteController.coffee | 3 +-
.../CollaboratorsInviteControllerTests.coffee | 50 +++++++++----------
2 files changed, 24 insertions(+), 29 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 93c84f4614..7f5adcc76f 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -2,7 +2,6 @@ ProjectGetter = require "../Project/ProjectGetter"
LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
Project = require("../../models/Project").Project
-User = require("../../models/User").User
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
mimelib = require("mimelib")
logger = require('logger-sharelatex')
@@ -80,7 +79,7 @@ module.exports = CollaboratorsInviteController =
logger.log {projectId, token}, "no invite found for this token"
return _renderInvalidPage()
# check the user who sent the invite exists
- User.findOne {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
+ UserGetter.getUser {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
if err?
logger.err {err, projectId}, "error getting project owner"
return next(err)
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 9cf4db291e..ee36ac3da6 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -11,9 +11,6 @@ ObjectId = require("mongojs").ObjectId
describe "CollaboratorsInviteController", ->
beforeEach ->
- @User = class User
- constructor: (options={}) -> this
- @findOne: sinon.stub()
@Project = class Project
constructor: () ->
this
@@ -24,9 +21,8 @@ describe "CollaboratorsInviteController", ->
"../Editor/EditorRealTimeController": @EditorRealTimeController = {}
'../Subscription/LimitationsManager' : @LimitationsManager = {}
'../Project/ProjectEditorHandler' : @ProjectEditorHandler = {}
- '../User/UserGetter': @UserGetter = {}
+ '../User/UserGetter': @UserGetter = {getUser: sinon.stub()}
'../../models/Project': {Project: @Project}
- '../../models/User': {User; @User}
'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
@res = new MockResponse()
@req = new MockRequest()
@@ -195,7 +191,7 @@ describe "CollaboratorsInviteController", ->
last_name: "Doe"
email: "john@example.com"
@Project.findOne.callsArgWith(2, null, @fakeProject)
- @User.findOne.callsArgWith(2, null, @owner)
+ @UserGetter.getUser.callsArgWith(2, null, @owner)
@CollaboratorsInviteHandler.getInviteByToken = sinon.stub().callsArgWith(2, null, @invite)
@callback = sinon.stub()
@next = sinon.stub()
@@ -220,9 +216,9 @@ describe "CollaboratorsInviteController", ->
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
- it 'should call User.findOne', ->
- @User.findOne.callCount.should.equal 1
- @User.findOne.calledWith({_id: @fakeProject.owner_ref}).should.equal true
+ it 'should call User.getUser', ->
+ @UserGetter.getUser.callCount.should.equal 1
+ @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true
describe 'when Project.findOne produces an error', ->
@@ -241,8 +237,8 @@ describe "CollaboratorsInviteController", ->
it 'should not call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0
- it 'should not call User.findOne', ->
- @User.findOne.callCount.should.equal 0
+ it 'should not call User.getUser', ->
+ @UserGetter.getUser.callCount.should.equal 0
describe 'when Project.findOne does not find a project', ->
@@ -264,8 +260,8 @@ describe "CollaboratorsInviteController", ->
it 'should not call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0
- it 'should not call User.findOne', ->
- @User.findOne.callCount.should.equal 0
+ it 'should not call User.getUser', ->
+ @UserGetter.getUser.callCount.should.equal 0
describe 'when the getInviteByToken produces an error', ->
@@ -284,8 +280,8 @@ describe "CollaboratorsInviteController", ->
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
- it 'should not call User.findOne', ->
- @User.findOne.callCount.should.equal 0
+ it 'should not call User.getUser', ->
+ @UserGetter.getUser.callCount.should.equal 0
describe 'when the getInviteByToken does not produce an invite', ->
@@ -308,8 +304,8 @@ describe "CollaboratorsInviteController", ->
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
- it 'should not call User.findOne', ->
- @User.findOne.callCount.should.equal 0
+ it 'should not call User.getUser', ->
+ @UserGetter.getUser.callCount.should.equal 0
describe 'when the user is already a member of the project', ->
@@ -332,13 +328,13 @@ describe "CollaboratorsInviteController", ->
it 'should not call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0
- it 'should not call User.findOne', ->
- @User.findOne.callCount.should.equal 0
+ it 'should not call User.getUser', ->
+ @UserGetter.getUser.callCount.should.equal 0
- describe 'when User.findOne produces an error', ->
+ describe 'when User.getUser produces an error', ->
beforeEach ->
- @User.findOne.callsArgWith(2, new Error('woops'))
+ @UserGetter.getUser.callsArgWith(2, new Error('woops'))
@CollaboratorsInviteController.viewInvite @req, @res, @next
it 'should produce an error', ->
@@ -352,13 +348,13 @@ describe "CollaboratorsInviteController", ->
@Project.findOne.callCount.should.equal 1
@Project.findOne.calledWith({_id: @project_id}).should.equal true
- it 'should call User.findOne', ->
- @User.findOne.callCount.should.equal 1
+ it 'should call User.getUser', ->
+ @UserGetter.getUser.callCount.should.equal 1
- describe 'when User.findOne does not find a user', ->
+ describe 'when User.getUser does not find a user', ->
beforeEach ->
- @User.findOne.callsArgWith(2, null, null)
+ @UserGetter.getUser.callsArgWith(2, null, null)
@CollaboratorsInviteController.viewInvite @req, @res, @next
it 'should render the not-valid view template', ->
@@ -372,8 +368,8 @@ describe "CollaboratorsInviteController", ->
@Project.findOne.callCount.should.equal 1
@Project.findOne.calledWith({_id: @project_id}).should.equal true
- it 'should call User.findOne', ->
- @User.findOne.callCount.should.equal 1
+ it 'should call User.getUser', ->
+ @UserGetter.getUser.callCount.should.equal 1
describe "revokeInvite", ->
From 01637386bd142560a1551e0d250f61f7948c68a1 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Tue, 2 Aug 2016 11:59:43 +0100
Subject: [PATCH 088/378] Use string split instead of regex.
---
.../web/public/coffee/filters/wrapLongWords.coffee | 14 +++++++++++---
.../web/public/stylesheets/app/editor/chat.less | 1 -
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/services/web/public/coffee/filters/wrapLongWords.coffee b/services/web/public/coffee/filters/wrapLongWords.coffee
index ea97a58fec..db502e848a 100644
--- a/services/web/public/coffee/filters/wrapLongWords.coffee
+++ b/services/web/public/coffee/filters/wrapLongWords.coffee
@@ -3,13 +3,21 @@ define [
], (App) ->
DEF_MIN_LENGTH = 20
+ _decodeHTMLEntities = (str) ->
+ str.replace /(\d+);/g, (match, dec) ->
+ String.fromCharCode dec;
+
_getWrappedWordsString = (baseStr, wrapperElName, minLength) ->
minLength = minLength || DEF_MIN_LENGTH
+ words = baseStr.split ' '
- findWordsRegEx = new RegExp "\\w{#{minLength},}", "g"
- wrappingTemplate = "<#{wrapperElName} class=\"break-word\">$&#{wrapperElName}>"
+ wordsWrapped = for word in words
+ if _decodeHTMLEntities(word).length >= minLength
+ "<#{wrapperElName} class=\"break-word\">#{word}#{wrapperElName}>"
+ else
+ word
- baseStr.replace findWordsRegEx, wrappingTemplate
+ outputStr = wordsWrapped.join ' '
App.filter "wrapLongWords", () ->
diff --git a/services/web/public/stylesheets/app/editor/chat.less b/services/web/public/stylesheets/app/editor/chat.less
index 7e1ad95b8d..592d39ecf4 100644
--- a/services/web/public/stylesheets/app/editor/chat.less
+++ b/services/web/public/stylesheets/app/editor/chat.less
@@ -131,7 +131,6 @@
.break-word {
word-break: break-all;
- color: red;
}
.editor-dark {
From abbd059eaeb3ac2ef538b1c490bf3d3695bf65f8 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 2 Aug 2016 13:51:00 +0100
Subject: [PATCH 089/378] Refactor to existing `addUserIdToProject` function
---
.../CollaboratorsInviteHandler.coffee | 70 ++-----
.../CollaboratorsInviteHandlerTests.coffee | 182 ++++++------------
2 files changed, 71 insertions(+), 181 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index c83433e880..8d71e019fe 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -1,9 +1,8 @@
-Project = require("../../models/Project").Project
ProjectInvite = require("../../models/ProjectInvite").ProjectInvite
mimelib = require("mimelib")
logger = require('logger-sharelatex')
-ContactManager = require "../Contacts/ContactManager"
CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
+CollaboratorsHandler = require "./CollaboratorsHandler"
Async = require "async"
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
Errors = require "../Errors/Errors"
@@ -60,59 +59,24 @@ module.exports = CollaboratorsInviteHandler =
callback(null, invite)
acceptInvite: (projectId, inviteId, tokenString, user, callback=(err)->) ->
- # fetch the target project
- Project.findOne {_id: projectId}, (err, project) ->
+ logger.log {projectId, inviteId, userId: user._id}, "accepting invite"
+ CollaboratorsInviteHandler.getInviteByToken projectId, tokenString, (err, invite) ->
if err?
- logger.err {err, projectId}, "error finding project"
+ logger.err {err, projectId, inviteId}, "error finding invite"
return callback(err)
- if !project
- err = new Errors.NotFoundError("no project found for invite")
- logger.log {err, projectId, inviteId}, "no project found"
+ if !invite
+ err = new Errors.NotFoundError("no matching invite found")
+ logger.log {err, projectId, inviteId, tokenString}, "no matching invite found"
return callback(err)
- # fetch the invite
- ProjectInvite.findOne {_id: inviteId, projectId: projectId, token: tokenString}, (err, invite) ->
+ inviteId = invite._id
+ CollaboratorsHandler.addUserIdToProject projectId, invite.sendingUserId, user._id, invite.privileges, (err) ->
if err?
- logger.err {err, projectId, inviteId}, "error finding invite"
+ logger.err {err, projectId, inviteId, userId: user._id}, "error adding user to project"
return callback(err)
- if !invite
- err = new Errors.NotFoundError("no matching invite found")
- logger.log {err, projectId, inviteId, tokenString}, "no matching invite found"
- return callback(err)
-
- # check if user is already a member of this project,
- # return early if so
- existing_users = (project.collaberator_refs or [])
- existing_users = existing_users.concat(project.readOnly_refs or [])
- existing_users = existing_users.map (u) -> u.toString()
- if existing_users.indexOf(user._id.toString()) > -1
- logger.log {projectId, userId: user._id}, "user already member of project, returning"
- return callback null
-
- # build an update to be applied with $addToSet, user is added to either
- # `collaberator_refs` or `readOnly_refs`
- privilegeLevel = invite.privileges
- if privilegeLevel == PrivilegeLevels.READ_AND_WRITE
- level = {"collaberator_refs": user._id}
- logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user with read-write access"
- else if privilegeLevel == PrivilegeLevels.READ_ONLY
- level = {"readOnly_refs": user._id}
- logger.log {privileges: privilegeLevel, user_id: user._id, projectId}, "adding user with read-only access"
- else
- return callback(new Error("unknown privilegeLevel: #{privilegeLevel}"))
-
- ContactManager.addContact invite.sendingUserId, user._id
-
- # Update the project, adding the new member.
- Project.update { _id: project._id }, { $addToSet: level }, (error) ->
- return callback(error) if error?
- # Flush to TPDS in background to add files to collaborator's Dropbox
- ProjectEntityHandler = require("../Project/ProjectEntityHandler")
- ProjectEntityHandler.flushProjectToThirdPartyDataStore project._id, (error) ->
- if error?
- logger.error {err: error, project_id: project._id, user_id}, "error flushing to TPDS after adding collaborator"
- # Remove invite
- ProjectInvite.remove {_id: inviteId}, (err) ->
- if err?
- logger.err {err, projectId, inviteId}, "error removing invite"
- return callback(err)
- callback()
+ # Remove invite
+ logger.log {projectId, inviteId}, "removing invite"
+ ProjectInvite.remove {_id: inviteId}, (err) ->
+ if err?
+ logger.err {err, projectId, inviteId}, "error removing invite"
+ return callback(err)
+ callback()
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index d144a3dcb9..e89488598f 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -20,20 +20,13 @@ describe "CollaboratorsInviteHandler", ->
@findOne: sinon.stub()
@find: sinon.stub()
@remove: sinon.stub()
- @Project = class Project
- constructor: () ->
- this
- @findOne: sinon.stub()
- @update: sinon.stub()
@Crypto = Crypto
@CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires:
'settings-sharelatex': @settings = {}
'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
'./CollaboratorsEmailHandler': @CollaboratorsEmailHandler = {}
- '../Contacts/ContactManager': @ContactManager = {}
- '../../models/Project': {Project: @Project}
+ "./CollaboratorsHandler": @CollaboratorsHandler = {addUserIdToProject: sinon.stub()}
'../../models/ProjectInvite': {ProjectInvite: @ProjectInvite}
- "../Project/ProjectEntityHandler": @ProjectEntityHandler = {}
'crypto': @Crypto
@projectId = ObjectId()
@@ -249,15 +242,16 @@ describe "CollaboratorsInviteHandler", ->
_id: @projectId
collaberator_refs: []
readOnly_refs: []
- @Project.findOne.callsArgWith(1, null, @fakeProject)
- @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
- @ContactManager.addContact = sinon.stub()
- @Project.update.callsArgWith(2, null)
- @ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon.stub().callsArgWith(1, null)
+ @CollaboratorsHandler.addUserIdToProject.callsArgWith(4, null)
+ @_getInviteByToken = sinon.stub(@CollaboratorsInviteHandler, 'getInviteByToken')
+ @_getInviteByToken.callsArgWith(2, null, @fakeInvite)
@ProjectInvite.remove.callsArgWith(1, null)
@call = (callback) =>
@CollaboratorsInviteHandler.acceptInvite @projectId, @inviteId, @token, @user, callback
+ afterEach ->
+ @_getInviteByToken.restore()
+
describe 'when all goes well', ->
beforeEach ->
@@ -268,34 +262,16 @@ describe "CollaboratorsInviteHandler", ->
expect(err).to.be.oneOf [null, undefined]
done()
- it 'should have called Project.findOne', (done) ->
+ it 'should have called getInviteByToken', (done) ->
@call (err) =>
- @Project.findOne.callCount.should.equal 1
- @Project.findOne.calledWith({_id: @projectId}).should.equal true
+ @_getInviteByToken.callCount.should.equal 1
+ @_getInviteByToken.calledWith(@projectId, @token).should.equal true
done()
- it 'should have called ProjectInvite.findOne', (done) ->
+ it 'should have called CollaboratorsHandler.addUserIdToProject', (done) ->
@call (err) =>
- @ProjectInvite.findOne.callCount.should.equal 1
- @ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId, token: @token}).should.equal true
- done()
-
- it 'should have called ContactManager.addContact', (done) ->
- @call (err) =>
- @ContactManager.addContact.callCount.should.equal 1
- @ContactManager.addContact.calledWith(@sendingUserId, @userId).should.equal true
- done()
-
- it 'should have called Project.update, adding the user to collaberator_refs', (done) ->
- @call (err) =>
- @Project.update.callCount.should.equal 1
- @Project.update.calledWith({_id: @projectId}, {$addToSet: {"collaberator_refs": @userId}}).should.equal true
- done()
-
- it 'should have called ProjectEntityHandler.flushProjectToThirdPartyDataStore', (done) ->
- @call (err) =>
- @ProjectEntityHandler.flushProjectToThirdPartyDataStore.callCount.should.equal 1
- @ProjectEntityHandler.flushProjectToThirdPartyDataStore.calledWith(@projectId).should.equal true
+ @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1
+ @CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true
done()
it 'should have called ProjectInvite.remove', (done) ->
@@ -308,7 +284,7 @@ describe "CollaboratorsInviteHandler", ->
beforeEach ->
@fakeInvite.privileges = 'readOnly'
- @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
+ @_getInviteByToken.callsArgWith(2, null, @fakeInvite)
it 'should not produce an error', (done) ->
@call (err) =>
@@ -316,72 +292,16 @@ describe "CollaboratorsInviteHandler", ->
expect(err).to.be.oneOf [null, undefined]
done()
- it 'should have called Project.update, adding the user to readOnly_refs', (done) ->
+ it 'should have called CollaboratorsHandler.addUserIdToProject', (done) ->
@call (err) =>
- @Project.update.callCount.should.equal 1
- @Project.update.calledWith({_id: @projectId}, {$addToSet: {"readOnly_refs": @userId}}).should.equal true
+ @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1
+ @CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true
done()
- describe 'when the invite is for an unknown access level', ->
+ describe 'when getInviteByToken does not find an invite', ->
beforeEach ->
- @fakeInvite.privileges = 'some_crazy_permission'
- @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
-
- it 'should produce an error', (done) ->
- @call (err) =>
- expect(err).to.be.instanceof Error
- done()
-
- it 'should not have called Project.update', (done) ->
- @call (err) =>
- @Project.update.callCount.should.equal 0
- done()
-
- it 'should not have called ProjectInvite.remove', (done) ->
- @call (err) =>
- @ProjectInvite.remove.callCount.should.equal 0
- done()
-
- describe 'when user is already a member of project', ->
-
- beforeEach ->
- @fakeProject.collaberator_refs = [ObjectId(), @user._id, ObjectId(), ObjectId()]
- @Project.findOne.callsArgWith(1, null, @fakeProject)
-
- it 'should not produce an error', (done) ->
- @call (err) =>
- expect(err).to.not.be.instanceof Error
- expect(err).to.be.oneOf [null, undefined]
- done()
-
- it 'should have called Project.findOne', (done) ->
- @call (err) =>
- @Project.findOne.callCount.should.equal 1
- @Project.findOne.calledWith({_id: @projectId}).should.equal true
- done()
-
- it 'should have called ProjectInvite.findOne', (done) ->
- @call (err) =>
- @ProjectInvite.findOne.callCount.should.equal 1
- @ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId, token: @token}).should.equal true
- done()
-
- it 'should have returned early, not have called Project.update', (done) ->
- @call (err) =>
- @Project.update.callCount.should.equal 0
- done()
-
- it 'should not have called ProjectInvite.remove', (done) ->
- @call (err) =>
- @ProjectInvite.remove.callCount.should.equal 0
- done()
-
-
- describe 'when ProjectInvite.findOne does not find an invite', ->
-
- beforeEach ->
- @ProjectInvite.findOne.callsArgWith(1, null, null)
+ @_getInviteByToken.callsArgWith(2, null, null)
it 'should produce an error', (done) ->
@call (err) =>
@@ -389,9 +309,15 @@ describe "CollaboratorsInviteHandler", ->
expect(err.name).to.equal "NotFoundError"
done()
- it 'should not have called Project.update', (done) ->
+ it 'should have called getInviteByToken', (done) ->
@call (err) =>
- @Project.update.callCount.should.equal 0
+ @_getInviteByToken.callCount.should.equal 1
+ @_getInviteByToken.calledWith(@projectId, @token).should.equal true
+ done()
+
+ it 'should not have called CollaboratorsHandler.addUserIdToProject', (done) ->
+ @call (err) =>
+ @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 0
done()
it 'should not have called ProjectInvite.remove', (done) ->
@@ -399,19 +325,25 @@ describe "CollaboratorsInviteHandler", ->
@ProjectInvite.remove.callCount.should.equal 0
done()
- describe 'when Project.findOne produces an error', ->
+ describe 'when getInviteByToken produces an error', ->
beforeEach ->
- @Project.findOne.callsArgWith(1, new Error('woops'))
+ @_getInviteByToken.callsArgWith(2, new Error('woops'))
it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.be.instanceof Error
done()
- it 'should not have called Project.update', (done) ->
+ it 'should have called getInviteByToken', (done) ->
@call (err) =>
- @Project.update.callCount.should.equal 0
+ @_getInviteByToken.callCount.should.equal 1
+ @_getInviteByToken.calledWith(@projectId, @token).should.equal true
+ done()
+
+ it 'should not have called CollaboratorsHandler.addUserIdToProject', (done) ->
+ @call (err) =>
+ @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 0
done()
it 'should not have called ProjectInvite.remove', (done) ->
@@ -419,39 +351,26 @@ describe "CollaboratorsInviteHandler", ->
@ProjectInvite.remove.callCount.should.equal 0
done()
- describe 'when ProjectInvite.findOne produces an error', ->
+ describe 'when addUserIdToProject produces an error', ->
beforeEach ->
- @ProjectInvite.findOne.callsArgWith(1, new Error('woops'))
+ @CollaboratorsHandler.addUserIdToProject.callsArgWith(4, new Error('woops'))
it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.be.instanceof Error
done()
- it 'should not have called Project.update', (done) ->
+ it 'should have called getInviteByToken', (done) ->
@call (err) =>
- @Project.update.callCount.should.equal 0
+ @_getInviteByToken.callCount.should.equal 1
+ @_getInviteByToken.calledWith(@projectId, @token).should.equal true
done()
- it 'should not have called ProjectInvite.remove', (done) ->
+ it 'should have called CollaboratorsHandler.addUserIdToProject', (done) ->
@call (err) =>
- @ProjectInvite.remove.callCount.should.equal 0
- done()
-
- describe 'when Project.update produces an error', ->
-
- beforeEach ->
- @Project.update.callsArgWith(2, new Error('woops'))
-
- it 'should produce an error', (done) ->
- @call (err) =>
- expect(err).to.be.instanceof Error
- done()
-
- it 'should have called Project.update', (done) ->
- @call (err) =>
- @Project.update.callCount.should.equal 1
+ @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1
+ @CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true
done()
it 'should not have called ProjectInvite.remove', (done) ->
@@ -469,9 +388,16 @@ describe "CollaboratorsInviteHandler", ->
expect(err).to.be.instanceof Error
done()
- it 'should have called Project.update', (done) ->
+ it 'should have called getInviteByToken', (done) ->
@call (err) =>
- @Project.update.callCount.should.equal 1
+ @_getInviteByToken.callCount.should.equal 1
+ @_getInviteByToken.calledWith(@projectId, @token).should.equal true
+ done()
+
+ it 'should have called CollaboratorsHandler.addUserIdToProject', (done) ->
+ @call (err) =>
+ @CollaboratorsHandler.addUserIdToProject.callCount.should.equal 1
+ @CollaboratorsHandler.addUserIdToProject.calledWith(@projectId, @sendingUserId, @userId, @fakeInvite.privileges).should.equal true
done()
it 'should have called ProjectInvite.remove', (done) ->
From 13fe000176aa7e3970a554f791229a343579fe39 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 2 Aug 2016 14:30:42 +0100
Subject: [PATCH 090/378] Move email parsing code to Helpers/EmailHelpers
---
.../CollaboratorsController.coffee | 12 ++++-----
.../Collaborators/CollaboratorsHandler.coffee | 27 ++++++++++---------
.../CollaboratorsInviteController.coffee | 5 ++--
.../CollaboratorsInviteHandler.coffee | 1 -
.../Features/Helpers/EmailHelpers.coffee | 11 ++++++++
5 files changed, 34 insertions(+), 22 deletions(-)
create mode 100644 services/web/app/coffee/Features/Helpers/EmailHelpers.coffee
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
index 1905d0d0a6..662e3109e3 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
@@ -4,7 +4,8 @@ ProjectEditorHandler = require "../Project/ProjectEditorHandler"
EditorRealTimeController = require "../Editor/EditorRealTimeController"
LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
-mimelib = require("mimelib")
+EmailHelpers = require "../Helpers/EmailHelpers"
+
module.exports = CollaboratorsController =
addUserToProject: (req, res, next) ->
@@ -16,11 +17,11 @@ module.exports = CollaboratorsController =
return res.json { user: false }
else
{email, privileges} = req.body
-
- email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
+
+ email = EmailHelpers.parseEmail(email)
if !email? or email == ""
return res.status(400).send("invalid email address")
-
+
adding_user_id = req.session?.user?._id
CollaboratorsHandler.addEmailToProject project_id, adding_user_id, email, privileges, (error, user_id) =>
return next(error) if error?
@@ -36,7 +37,7 @@ module.exports = CollaboratorsController =
CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) ->
return next(error) if error?
res.sendStatus 204
-
+
removeSelfFromProject: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
user_id = req.session?.user?._id
@@ -49,4 +50,3 @@ module.exports = CollaboratorsController =
return callback(error) if error?
EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id)
callback()
-
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
index 81557fea42..4b48f97304 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
@@ -1,6 +1,5 @@
UserCreator = require('../User/UserCreator')
Project = require("../../models/Project").Project
-mimelib = require("mimelib")
logger = require('logger-sharelatex')
UserGetter = require "../User/UserGetter"
ContactManager = require "../Contacts/ContactManager"
@@ -8,8 +7,11 @@ CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
async = require "async"
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
Errors = require "../Errors/Errors"
+EmailHelpers = require "../Helpers/EmailHelpers"
+
module.exports = CollaboratorsHandler =
+
getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) ->
return callback(error) if error?
@@ -21,12 +23,12 @@ module.exports = CollaboratorsHandler =
for member_id in project.collaberator_refs or []
members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_AND_WRITE }
return callback null, members
-
+
getMemberIds: (project_id, callback = (error, member_ids) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
return callback(error) if error?
return callback null, members.map (m) -> m.id
-
+
getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
return callback(error) if error?
@@ -41,7 +43,7 @@ module.exports = CollaboratorsHandler =
(error) ->
return callback(error) if error?
callback null, result
-
+
getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) ->
# In future if the schema changes and getting all member ids is more expensive (multiple documents)
# then optimise this.
@@ -51,12 +53,12 @@ module.exports = CollaboratorsHandler =
if member.id == user_id?.toString()
return callback null, member.privilegeLevel
return callback null, PrivilegeLevels.NONE
-
+
getMemberCount: (project_id, callback = (error, count) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
return callback(error) if error?
return callback null, (members or []).length
-
+
getCollaboratorCount: (project_id, callback = (error, count) ->) ->
CollaboratorsHandler.getMemberCount project_id, (error, count) ->
return callback(error) if error?
@@ -69,14 +71,14 @@ module.exports = CollaboratorsHandler =
if member.id.toString() == user_id.toString()
return callback null, true, member.privilegeLevel
return callback null, false, null
-
+
getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) ->
Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=>
return callback(err) if err?
Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=>
return callback(err) if err?
callback(null, readAndWriteProjects, readOnlyProjects)
-
+
removeUserFromProject: (project_id, user_id, callback = (error) ->)->
logger.log user_id: user_id, project_id: project_id, "removing user"
conditions = _id:project_id
@@ -86,7 +88,7 @@ module.exports = CollaboratorsHandler =
if err?
logger.error err: err, "problem removing user from project collaberators"
callback(err)
-
+
removeUserFromAllProjets: (user_id, callback = (error) ->) ->
CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, { _id: 1 }, (error, readAndWriteProjects = [], readOnlyProjects = []) ->
return callback(error) if error?
@@ -98,10 +100,9 @@ module.exports = CollaboratorsHandler =
return cb() if !project?
CollaboratorsHandler.removeUserFromProject project._id, user_id, cb
async.series jobs, callback
-
+
addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) ->
- emails = mimelib.parseAddresses(unparsed_email)
- email = emails[0]?.address?.toLowerCase()
+ email = EmailHelpers.parseEmail(unparsed_email)
if !email? or email == ""
return callback(new Error("no valid email provided: '#{unparsed_email}'"))
UserCreator.getUserOrCreateHoldingAccount email, (error, user) ->
@@ -118,7 +119,7 @@ module.exports = CollaboratorsHandler =
existing_users = existing_users.map (u) -> u.toString()
if existing_users.indexOf(user_id.toString()) > -1
return callback null # User already in Project
-
+
if privilegeLevel == PrivilegeLevels.READ_AND_WRITE
level = {"collaberator_refs":user_id}
logger.log {privileges: "readAndWrite", user_id, project_id}, "adding user"
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 7f5adcc76f..e91dd308a0 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -3,8 +3,9 @@ LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
Project = require("../../models/Project").Project
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
-mimelib = require("mimelib")
logger = require('logger-sharelatex')
+EmailHelpers = require "../Helpers/EmailHelpers"
+
module.exports = CollaboratorsInviteController =
@@ -28,7 +29,7 @@ module.exports = CollaboratorsInviteController =
logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
return res.json {invite: null}
{email, privileges} = req.body
- email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
+ email = EmailHelpers.parseEmail(email)
if !email? or email == ""
logger.log {projectId, email, sendingUserId}, "invalid email address"
return res.sendStatus(400)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 8d71e019fe..57d9169b8f 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -1,5 +1,4 @@
ProjectInvite = require("../../models/ProjectInvite").ProjectInvite
-mimelib = require("mimelib")
logger = require('logger-sharelatex')
CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
CollaboratorsHandler = require "./CollaboratorsHandler"
diff --git a/services/web/app/coffee/Features/Helpers/EmailHelpers.coffee b/services/web/app/coffee/Features/Helpers/EmailHelpers.coffee
new file mode 100644
index 0000000000..6c2644875e
--- /dev/null
+++ b/services/web/app/coffee/Features/Helpers/EmailHelpers.coffee
@@ -0,0 +1,11 @@
+mimelib = require("mimelib")
+
+
+module.exports = EmailHelpers =
+
+ parseEmail: (email) ->
+ email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
+ if !email? or email == ""
+ return null
+ else
+ return email
From 38f76b305ba10f5044fd340f2c97695310ede33c Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Tue, 2 Aug 2016 14:43:09 +0100
Subject: [PATCH 091/378] change message to "Run syntax check"
---
services/web/app/views/project/editor/pdf.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade
index 973562abc0..cf5129708a 100644
--- a/services/web/app/views/project/editor/pdf.jade
+++ b/services/web/app/views/project/editor/pdf.jade
@@ -41,7 +41,7 @@ div.full-size.pdf(ng-controller="PdfController")
li
a(href, ng-click="recompile({check:true})")
i.fa.fa-fw()
- | #{translate("syntax_check")}
+ | #{translate("run_syntax_check")}
a(
href
ng-click="stop()"
From 2ad0bab9764a94a8cb53d21428ec9709253cb5d1 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Tue, 2 Aug 2016 15:28:53 +0100
Subject: [PATCH 092/378] =?UTF-8?q?use=20underscore.each=20so=20it=20doesn?=
=?UTF-8?q?=E2=80=99t=20blow=20up=20on=20non=20array.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Features/References/ReferencesHandler.coffee | 14 +++++++-------
.../References/ReferencesHandlerTests.coffee | 5 +++++
2 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee
index fa38ddedf6..3ceeab93f8 100644
--- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee
+++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee
@@ -4,7 +4,7 @@ settings = require("settings-sharelatex")
ProjectGetter = require "../Project/ProjectGetter"
UserGetter = require "../User/UserGetter"
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
-U = require('underscore')
+_ = require('underscore')
Async = require('async')
oneMinInMs = 60 * 1000
@@ -22,24 +22,24 @@ module.exports = ReferencesHandler =
_findBibFileIds: (project) ->
ids = []
_process = (folder) ->
- (folder.fileRefs or []).forEach (file) ->
+ _.each (folder.fileRefs or []), (file) ->
if file?.name?.match(/^.*\.bib$/)
ids.push(file._id)
- (folder.folders or []).forEach (folder) ->
+ _.each (folder.folders or []), (folder) ->
_process(folder)
- (project.rootFolder or []).forEach (rootFolder) ->
+ _.each (project.rootFolder or []), (rootFolder) ->
_process(rootFolder)
return ids
_findBibDocIds: (project) ->
ids = []
_process = (folder) ->
- (folder.docs or []).forEach (doc) ->
+ _.each (folder.docs or []), (doc) ->
if doc?.name?.match(/^.*\.bib$/)
ids.push(doc._id)
- (folder.folders or []).forEach (folder) ->
+ _.each (folder.folders or []), (folder) ->
_process(folder)
- (project.rootFolder or []).forEach (rootFolder) ->
+ _.each (project.rootFolder or []), (rootFolder) ->
_process(rootFolder)
return ids
diff --git a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee
index c46e71a542..c69cb3ee0a 100644
--- a/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/References/ReferencesHandlerTests.coffee
@@ -322,6 +322,11 @@ describe 'ReferencesHandler', ->
result = @handler._findBibDocIds(@fakeProject)
expect(result).to.deep.equal @expectedIds
+ it 'should not error with a non array of folders from dirty data', ->
+ @fakeProject.rootFolder[0].folders[0].folders = {}
+ result = @handler._findBibDocIds(@fakeProject)
+ expect(result).to.deep.equal @expectedIds
+
describe '_findBibFileIds', ->
beforeEach ->
From 928777b61ce6522ea4b09047ff1fc6ef02c9a896 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Tue, 2 Aug 2016 15:34:44 +0100
Subject: [PATCH 093/378] add null check to redis return value for
getValueFromTokenAndExpire
---
.../web/app/coffee/Features/Security/OneTimeTokenHandler.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee b/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee
index 66581a02b3..b84e1c9b33 100644
--- a/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee
+++ b/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee
@@ -30,5 +30,5 @@ module.exports =
multi.get buildKey(token)
multi.del buildKey(token)
multi.exec (err, results)->
- callback err, results[0]
+ callback err, results?[0]
From 418d1c56da241ace30725c312c82ea357d23de14 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Tue, 2 Aug 2016 15:36:59 +0100
Subject: [PATCH 094/378] nullcheck qqfile on upload files
---
.../coffee/Features/Uploads/ProjectUploadController.coffee | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee
index b7cab7ac5c..557a1d5449 100644
--- a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee
+++ b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee
@@ -27,8 +27,8 @@ module.exports = ProjectUploadController =
uploadFile: (req, res, next) ->
timer = new metrics.Timer("file-upload")
- name = req.files.qqfile.originalname
- path = req.files.qqfile.path
+ name = req.files.qqfile?.originalname
+ path = req.files.qqfile?.path
project_id = req.params.Project_id
folder_id = req.query.folder_id
if !name? or name.length == 0 or name.length > 150
From 2494026b85218d95812c7e82f437eda4d4f9ebbf Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 2 Aug 2016 15:42:26 +0100
Subject: [PATCH 095/378] Move Helpers/EmailHelpers to Helpers/EmailHelper
---
.../Features/Collaborators/CollaboratorsController.coffee | 4 ++--
.../coffee/Features/Collaborators/CollaboratorsHandler.coffee | 4 ++--
.../Collaborators/CollaboratorsInviteController.coffee | 4 ++--
.../Helpers/{EmailHelpers.coffee => EmailHelper.coffee} | 2 +-
4 files changed, 7 insertions(+), 7 deletions(-)
rename services/web/app/coffee/Features/Helpers/{EmailHelpers.coffee => EmailHelper.coffee} (86%)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
index 662e3109e3..9e04b5e990 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
@@ -4,7 +4,7 @@ ProjectEditorHandler = require "../Project/ProjectEditorHandler"
EditorRealTimeController = require "../Editor/EditorRealTimeController"
LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
-EmailHelpers = require "../Helpers/EmailHelpers"
+EmailHelper = require "../Helpers/EmailHelper"
module.exports = CollaboratorsController =
@@ -18,7 +18,7 @@ module.exports = CollaboratorsController =
else
{email, privileges} = req.body
- email = EmailHelpers.parseEmail(email)
+ email = EmailHelper.parseEmail(email)
if !email? or email == ""
return res.status(400).send("invalid email address")
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
index 4b48f97304..58ad246b60 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
@@ -7,7 +7,7 @@ CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
async = require "async"
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
Errors = require "../Errors/Errors"
-EmailHelpers = require "../Helpers/EmailHelpers"
+EmailHelper = require "../Helpers/EmailHelper"
module.exports = CollaboratorsHandler =
@@ -102,7 +102,7 @@ module.exports = CollaboratorsHandler =
async.series jobs, callback
addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) ->
- email = EmailHelpers.parseEmail(unparsed_email)
+ email = EmailHelper.parseEmail(unparsed_email)
if !email? or email == ""
return callback(new Error("no valid email provided: '#{unparsed_email}'"))
UserCreator.getUserOrCreateHoldingAccount email, (error, user) ->
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index e91dd308a0..11d8230059 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -4,7 +4,7 @@ UserGetter = require "../User/UserGetter"
Project = require("../../models/Project").Project
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
logger = require('logger-sharelatex')
-EmailHelpers = require "../Helpers/EmailHelpers"
+EmailHelper = require "../Helpers/EmailHelper"
module.exports = CollaboratorsInviteController =
@@ -29,7 +29,7 @@ module.exports = CollaboratorsInviteController =
logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
return res.json {invite: null}
{email, privileges} = req.body
- email = EmailHelpers.parseEmail(email)
+ email = EmailHelper.parseEmail(email)
if !email? or email == ""
logger.log {projectId, email, sendingUserId}, "invalid email address"
return res.sendStatus(400)
diff --git a/services/web/app/coffee/Features/Helpers/EmailHelpers.coffee b/services/web/app/coffee/Features/Helpers/EmailHelper.coffee
similarity index 86%
rename from services/web/app/coffee/Features/Helpers/EmailHelpers.coffee
rename to services/web/app/coffee/Features/Helpers/EmailHelper.coffee
index 6c2644875e..7bc93888fd 100644
--- a/services/web/app/coffee/Features/Helpers/EmailHelpers.coffee
+++ b/services/web/app/coffee/Features/Helpers/EmailHelper.coffee
@@ -1,7 +1,7 @@
mimelib = require("mimelib")
-module.exports = EmailHelpers =
+module.exports = EmailHelper =
parseEmail: (email) ->
email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
From 3a3688d3d064c50d83e5caf9b7a9156dc989d5f7 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 2 Aug 2016 15:42:50 +0100
Subject: [PATCH 096/378] Include invites count in `canAddXCollaborators`
---
.../CollaboratorsInviteHandler.coffee | 8 ++++
.../Subscription/LimitationsManager.coffee | 14 ++++---
.../LimitationsManagerTests.coffee | 41 +++++++++++++++++--
3 files changed, 54 insertions(+), 9 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 57d9169b8f..25008cfde7 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -17,6 +17,14 @@ module.exports = CollaboratorsInviteHandler =
return callback(err)
callback(null, invites)
+ getInviteCount: (projectId, callback=(err, count)->) ->
+ logger.log {projectId}, "counting invites for project"
+ ProjectInvite.count {projectId: projectId}, (err, count) ->
+ if err?
+ logger.err {err, projectId}, "error getting invites from mongo"
+ return callback(err)
+ callback(null, count)
+
inviteToProject: (projectId, sendingUserId, email, privileges, callback=(err,invite)->) ->
logger.log {projectId, sendingUserId, email, privileges}, "adding invite"
Crypto.randomBytes 24, (err, buffer) ->
diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee
index 6c402220a9..59a0748f36 100644
--- a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee
+++ b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee
@@ -4,6 +4,7 @@ User = require("../../models/User").User
SubscriptionLocator = require("./SubscriptionLocator")
Settings = require("settings-sharelatex")
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
+CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler")
module.exports =
@@ -20,10 +21,12 @@ module.exports =
return callback(error) if error?
CollaboratorsHandler.getCollaboratorCount project_id, (error, current_number) =>
return callback(error) if error?
- if current_number + x_collaborators <= allowed_number or allowed_number < 0
- callback null, true
- else
- callback null, false
+ CollaboratorsInvitesHandler.getInviteCount project_id, (error, invite_count) =>
+ return callback(error) if error?
+ if current_number + invite_count + x_collaborators <= allowed_number or allowed_number < 0
+ callback null, true
+ else
+ callback null, false
userHasSubscriptionOrIsGroupMember: (user, callback = (err, hasSubscriptionOrIsMember)->) ->
@userHasSubscription user, (err, hasSubscription, subscription)=>
@@ -41,7 +44,7 @@ module.exports =
hasValidSubscription = subscription? and (subscription.recurlySubscription_id? or subscription?.customAccount == true)
logger.log user:user, hasValidSubscription:hasValidSubscription, subscription:subscription, "checking if user has subscription"
callback err, hasValidSubscription, subscription
-
+
userIsMemberOfGroupSubscription: (user, callback = (error, isMember, subscriptions) ->) ->
logger.log user_id: user._id, "checking is user is member of subscription groups"
SubscriptionLocator.getMemberSubscriptions user._id, (err, subscriptions = []) ->
@@ -65,4 +68,3 @@ getOwnerOfProject = (project_id, callback)->
return callback(error) if error?
User.findById project.owner_ref, (error, owner) ->
callback(error, owner)
-
diff --git a/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee
index 94a1edb5f2..93f00afad5 100644
--- a/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Subscription/LimitationsManagerTests.coffee
@@ -30,6 +30,7 @@ describe "LimitationsManager", ->
'./SubscriptionLocator':@SubscriptionLocator
'settings-sharelatex' : @Settings = {}
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {}
+ "../Collaborators/CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
'logger-sharelatex':log:->
describe "allowedNumberOfCollaboratorsInProject", ->
@@ -56,6 +57,7 @@ describe "LimitationsManager", ->
describe "canAddXCollaborators", ->
beforeEach ->
@CollaboratorsHandler.getCollaboratorCount = (project_id, callback) => callback(null, @current_number)
+ @CollaboratorsInviteHandler.getInviteCount = (project_id, callback) => callback(null, @invite_count)
sinon.stub @LimitationsManager,
"allowedNumberOfCollaboratorsInProject",
(project_id, callback) => callback(null, @allowed_number)
@@ -65,6 +67,17 @@ describe "LimitationsManager", ->
beforeEach ->
@current_number = 1
@allowed_number = 2
+ @invite_count = 0
+ @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
+
+ it "should return true", ->
+ @callback.calledWith(null, true).should.equal true
+
+ describe "when the project has fewer collaborators and invites than allowed", ->
+ beforeEach ->
+ @current_number = 1
+ @allowed_number = 4
+ @invite_count = 1
@LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
it "should return true", ->
@@ -74,6 +87,7 @@ describe "LimitationsManager", ->
beforeEach ->
@current_number = 1
@allowed_number = 2
+ @invite_count = 0
@LimitationsManager.canAddXCollaborators(@project_id, 2, @callback)
it "should return false", ->
@@ -83,6 +97,7 @@ describe "LimitationsManager", ->
beforeEach ->
@current_number = 3
@allowed_number = 2
+ @invite_count = 0
@LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
it "should return false", ->
@@ -92,11 +107,31 @@ describe "LimitationsManager", ->
beforeEach ->
@current_number = 100
@allowed_number = -1
+ @invite_count = 0
@LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
it "should return true", ->
@callback.calledWith(null, true).should.equal true
+ describe 'when the project has more invites than allowed', ->
+ beforeEach ->
+ @current_number = 0
+ @allowed_number = 2
+ @invite_count = 2
+ @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
+
+ it "should return false", ->
+ @callback.calledWith(null, false).should.equal true
+
+ describe 'when the project has more invites and collaborators than allowed', ->
+ beforeEach ->
+ @current_number = 1
+ @allowed_number = 2
+ @invite_count = 1
+ @LimitationsManager.canAddXCollaborators(@project_id, 1, @callback)
+
+ it "should return false", ->
+ @callback.calledWith(null, false).should.equal true
describe "userHasSubscription", ->
beforeEach ->
@@ -193,7 +228,7 @@ describe "LimitationsManager", ->
@LimitationsManager.userHasSubscriptionOrIsGroupMember @user, (err, hasSubOrIsGroupMember)->
hasSubOrIsGroupMember.should.equal false
done()
-
+
describe "hasGroupMembersLimitReached", ->
beforeEach ->
@@ -214,10 +249,10 @@ describe "LimitationsManager", ->
@LimitationsManager.hasGroupMembersLimitReached @user_id, (err, limitReached)->
limitReached.should.equal false
done()
-
+
it "should return true if the limit has been exceded", (done)->
@subscription.membersLimit = 0
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription)
@LimitationsManager.hasGroupMembersLimitReached @user_id, (err, limitReached)->
limitReached.should.equal true
- done()
\ No newline at end of file
+ done()
From 5f8952450e0deee81391a05b045dc34226daef92 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 2 Aug 2016 16:08:05 +0100
Subject: [PATCH 097/378] Test `getInviteCount`
---
.../CollaboratorsInviteHandlerTests.coffee | 29 +++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index e89488598f..4e9a2cc95a 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -20,6 +20,7 @@ describe "CollaboratorsInviteHandler", ->
@findOne: sinon.stub()
@find: sinon.stub()
@remove: sinon.stub()
+ @count: sinon.stub()
@Crypto = Crypto
@CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires:
'settings-sharelatex': @settings = {}
@@ -48,6 +49,34 @@ describe "CollaboratorsInviteHandler", ->
privileges: @privileges
createdAt: new Date()
+ describe 'getInviteCount', ->
+
+ beforeEach ->
+ @ProjectInvite.count.callsArgWith(1, null, 2)
+ @call = (callback) =>
+ @CollaboratorsInviteHandler.getInviteCount @projectId, callback
+
+ it 'should not produce an error', (done) ->
+ @call (err, invites) =>
+ expect(err).to.not.be.instanceof Error
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should produce the count of documents', (done) ->
+ @call (err, count) =>
+ expect(count).to.equal 2
+ done()
+
+ describe 'when model.count produces an error', ->
+
+ beforeEach ->
+ @ProjectInvite.count.callsArgWith(1, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err, count) =>
+ expect(err).to.be.instanceof Error
+ done()
+
describe 'getAllInvites', ->
beforeEach ->
From 892511820ec66ceeaf53446546d4a47f5adc6db7 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Tue, 2 Aug 2016 17:09:42 +0100
Subject: [PATCH 098/378] fix logging on groupPlan invite notification
---
.../coffee/Features/Notifications/NotificationsBuilder.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
index 9f960b1d15..6f89525629 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
@@ -9,7 +9,7 @@ module.exports =
messageOpts =
groupName: licence.name
subscription_id: licence.subscription_id
- logger.log user_id:user._id, key:key, "creating notification key for user"
+ logger.log user_id:user._id, key:@key, "creating notification key for user"
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, callback
read: (callback = ->)->
From 6ea690225fa1c4c8357e92a0f10578543e2c13cf Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 3 Aug 2016 10:23:34 +0100
Subject: [PATCH 099/378] Refactor view-invite to not use model calls.
---
.../CollaboratorsInviteController.coffee | 27 +-
.../CollaboratorsInviteControllerTests.coffee | 234 +++++++++++-------
2 files changed, 161 insertions(+), 100 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 11d8230059..05792679a6 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -1,7 +1,7 @@
ProjectGetter = require "../Project/ProjectGetter"
LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
-Project = require("../../models/Project").Project
+CollaboratorsHandler = require('./CollaboratorsHandler')
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
logger = require('logger-sharelatex')
EmailHelper = require "../Helpers/EmailHelper"
@@ -57,17 +57,12 @@ module.exports = CollaboratorsInviteController =
_renderInvalidPage = () ->
logger.log {projectId, token}, "invite not valid, rendering not-valid page"
res.render "project/invite/not-valid", {title: "Invalid Invite"}
- # get the target project
- Project.findOne {_id: projectId}, {owner_ref: 1, name: 1, collaberator_refs: 1, readOnly_refs: 1}, (err, project) ->
+ # check if the user is already a member of the project
+ CollaboratorsHandler.isUserMemberOfProject currentUser._id, projectId, (err, isMember, _privilegeLevel) ->
if err?
- logger.err {err, projectId}, "error getting project"
+ logger.err {err, projectId}, "error checking if user is member of project"
return next(err)
- if !project
- logger.log {projectId}, "no project found"
- return _renderInvalidPage()
- # check if user is already a member of the project, redirect to project if so
- allMembers = (project.collaberator_refs || []).concat(project.readOnly_refs || []).map((oid) -> oid.toString())
- if currentUser._id in allMembers
+ if isMember
logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting"
return res.redirect "/project/#{projectId}"
# get the invite
@@ -87,8 +82,16 @@ module.exports = CollaboratorsInviteController =
if !owner
logger.log {projectId}, "no project owner found"
return _renderInvalidPage()
- # finally render the invite
- res.render "project/invite/show", {invite, project, owner, title: "Project Invite"}
+ # fetch the project name
+ ProjectGetter.getProject projectId, {}, (err, project) ->
+ if err?
+ logger.err {err, projectId}, "error getting project"
+ return next(err)
+ if !project
+ logger.log {projectId}, "no project found"
+ return _renderInvalidPage()
+ # finally render the invite
+ res.render "project/invite/show", {invite, project, owner, title: "Project Invite"}
acceptInvite: (req, res, next) ->
projectId = req.params.Project_id
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index ee36ac3da6..4cfac39232 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -17,12 +17,10 @@ describe "CollaboratorsInviteController", ->
@findOne: sinon.stub()
@CollaboratorsInviteController = SandboxedModule.require modulePath, requires:
"../Project/ProjectGetter": @ProjectGetter = {}
- "./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
- "../Editor/EditorRealTimeController": @EditorRealTimeController = {}
'../Subscription/LimitationsManager' : @LimitationsManager = {}
- '../Project/ProjectEditorHandler' : @ProjectEditorHandler = {}
'../User/UserGetter': @UserGetter = {getUser: sinon.stub()}
- '../../models/Project': {Project: @Project}
+ "./CollaboratorsHandler": @CollaboratorsHandler = {}
+ "./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
@res = new MockResponse()
@req = new MockRequest()
@@ -163,9 +161,10 @@ describe "CollaboratorsInviteController", ->
describe "viewInvite", ->
beforeEach ->
+ @token = "some-opaque-token"
@req.params =
Project_id: @project_id
- token: "some-opaque-token"
+ token: @token
@req.session =
user: _id: @current_user_id = "current-user-id"
@res.render = sinon.stub()
@@ -173,7 +172,7 @@ describe "CollaboratorsInviteController", ->
@res.sendStatus = sinon.stub()
@invite = {
_id: ObjectId(),
- token: "htnseuthaouse",
+ token: @token,
sendingUserId: ObjectId(),
projectId: @project_id,
targetEmail: 'user@example.com'
@@ -190,16 +189,18 @@ describe "CollaboratorsInviteController", ->
first_name: "John"
last_name: "Doe"
email: "john@example.com"
- @Project.findOne.callsArgWith(2, null, @fakeProject)
- @UserGetter.getUser.callsArgWith(2, null, @owner)
+
+ @CollaboratorsHandler.isUserMemberOfProject = sinon.stub().callsArgWith(2, null, false, null)
@CollaboratorsInviteHandler.getInviteByToken = sinon.stub().callsArgWith(2, null, @invite)
+ @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @fakeProject)
+ @UserGetter.getUser.callsArgWith(2, null, @owner)
+
@callback = sinon.stub()
@next = sinon.stub()
describe 'when the token is valid', ->
beforeEach ->
- @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, @invite)
@CollaboratorsInviteController.viewInvite @req, @res, @next
it 'should render the view template', ->
@@ -209,30 +210,61 @@ describe "CollaboratorsInviteController", ->
it 'should not call next', ->
@next.callCount.should.equal 0
- it 'should call Project.findOne', ->
- @Project.findOne.callCount.should.equal 1
- @Project.findOne.calledWith({_id: @project_id}).should.equal true
+ it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
+ @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
+ @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+ @CollaboratorsInviteHandler.getInviteByToken.calledWith(@fakeProject._id, @invite.token).should.equal true
it 'should call User.getUser', ->
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true
- describe 'when Project.findOne produces an error', ->
+ it 'should call ProjectGetter.getProject', ->
+ @ProjectGetter.getProject.callCount.should.equal 1
+ @ProjectGetter.getProject.calledWith(@project_id).should.equal true
+
+ describe 'when user is already a member of the project', ->
beforeEach ->
- @Project.findOne.callsArgWith(2, new Error('woops'))
+ @CollaboratorsHandler.isUserMemberOfProject = sinon.stub().callsArgWith(2, null, true, null)
@CollaboratorsInviteController.viewInvite @req, @res, @next
- it 'should produce an error', ->
+ it 'should redirect to the project page', ->
+ @res.redirect.callCount.should.equal 1
+ @res.redirect.calledWith("/project/#{@project_id}").should.equal true
+
+ it 'should not call next with an error', ->
+ @next.callCount.should.equal 0
+
+ it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
+ @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
+ @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
+
+ it 'should not call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0
+
+ it 'should not call User.getUser', ->
+ @UserGetter.getUser.callCount.should.equal 0
+
+ it 'should not call ProjectGetter.getProject', ->
+ @ProjectGetter.getProject.callCount.should.equal 0
+
+ describe 'when isUserMemberOfProject produces an error', ->
+
+ beforeEach ->
+ @CollaboratorsHandler.isUserMemberOfProject = sinon.stub().callsArgWith(2, new Error('woops'))
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
+
+ it 'should call next with an error', ->
@next.callCount.should.equal 1
expect(@next.firstCall.args[0]).to.be.instanceof Error
- it 'should call Project.findOne', ->
- @Project.findOne.callCount.should.equal 1
- @Project.findOne.calledWith({_id: @project_id}).should.equal true
+ it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
+ @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
+ @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should not call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0
@@ -240,28 +272,8 @@ describe "CollaboratorsInviteController", ->
it 'should not call User.getUser', ->
@UserGetter.getUser.callCount.should.equal 0
- describe 'when Project.findOne does not find a project', ->
-
- beforeEach ->
- @Project.findOne.callsArgWith(2, null, null)
- @CollaboratorsInviteController.viewInvite @req, @res, @next
-
- it 'should render the not-valid view template', ->
- @res.render.callCount.should.equal 1
- @res.render.calledWith('project/invite/not-valid').should.equal true
-
- it 'should not call next', ->
- @next.callCount.should.equal 0
-
- it 'should call Project.findOne', ->
- @Project.findOne.callCount.should.equal 1
- @Project.findOne.calledWith({_id: @project_id}).should.equal true
-
- it 'should not call getInviteByToken', ->
- @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0
-
- it 'should not call User.getUser', ->
- @UserGetter.getUser.callCount.should.equal 0
+ it 'should not call ProjectGetter.getProject', ->
+ @ProjectGetter.getProject.callCount.should.equal 0
describe 'when the getInviteByToken produces an error', ->
@@ -274,62 +286,46 @@ describe "CollaboratorsInviteController", ->
@next.callCount.should.equal 1
@next.calledWith(@err).should.equal true
- it 'should call Project.findOne', ->
- @Project.findOne.callCount.should.equal 1
+ it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
+ @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
+ @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+ @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
it 'should not call User.getUser', ->
@UserGetter.getUser.callCount.should.equal 0
+ it 'should not call ProjectGetter.getProject', ->
+ @ProjectGetter.getProject.callCount.should.equal 0
+
describe 'when the getInviteByToken does not produce an invite', ->
- describe 'when the user is not already a member of this project', ->
+ beforeEach ->
+ @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null)
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
- beforeEach ->
- @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null)
- @CollaboratorsInviteController.viewInvite @req, @res, @next
+ it 'should render the not-valid view template', ->
+ @res.render.callCount.should.equal 1
+ @res.render.calledWith('project/invite/not-valid').should.equal true
- it 'should render the not-valid view template', ->
- @res.render.callCount.should.equal 1
- @res.render.calledWith('project/invite/not-valid').should.equal true
+ it 'should not call next', ->
+ @next.callCount.should.equal 0
- it 'should not call next', ->
- @next.callCount.should.equal 0
+ it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
+ @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
+ @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
- it 'should call Project.findOne', ->
- @Project.findOne.callCount.should.equal 1
+ it 'should call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+ @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
- it 'should call getInviteByToken', ->
- @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+ it 'should not call User.getUser', ->
+ @UserGetter.getUser.callCount.should.equal 0
- it 'should not call User.getUser', ->
- @UserGetter.getUser.callCount.should.equal 0
-
- describe 'when the user is already a member of the project', ->
-
- beforeEach ->
- @fakeProject.collaberator_refs = [ObjectId(), @current_user_id, ObjectId()]
- @Project.findOne.callsArgWith(2, null, @fakeProject)
- @CollaboratorsInviteHandler.getInviteByToken.callsArgWith(2, null, null)
- @CollaboratorsInviteController.viewInvite @req, @res, @next
-
- it 'should redirect to the project page', ->
- @res.redirect.callCount.should.equal 1
- @res.redirect.calledWith("/project/#{@project_id}").should.equal true
-
- it 'should not call next', ->
- @next.callCount.should.equal 0
-
- it 'should call Project.findOne', ->
- @Project.findOne.callCount.should.equal 1
-
- it 'should not call getInviteByToken', ->
- @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 0
-
- it 'should not call User.getUser', ->
- @UserGetter.getUser.callCount.should.equal 0
+ it 'should not call ProjectGetter.getProject', ->
+ @ProjectGetter.getProject.callCount.should.equal 0
describe 'when User.getUser produces an error', ->
@@ -341,15 +337,19 @@ describe "CollaboratorsInviteController", ->
@next.callCount.should.equal 1
expect(@next.firstCall.args[0]).to.be.instanceof Error
+ it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
+ @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
+ @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
+
it 'should call getInviteByToken', ->
@CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
- it 'should call Project.findOne', ->
- @Project.findOne.callCount.should.equal 1
- @Project.findOne.calledWith({_id: @project_id}).should.equal true
-
it 'should call User.getUser', ->
@UserGetter.getUser.callCount.should.equal 1
+ @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true
+
+ it 'should not call ProjectGetter.getProject', ->
+ @ProjectGetter.getProject.callCount.should.equal 0
describe 'when User.getUser does not find a user', ->
@@ -364,12 +364,70 @@ describe "CollaboratorsInviteController", ->
it 'should not call next', ->
@next.callCount.should.equal 0
- it 'should call Project.findOne', ->
- @Project.findOne.callCount.should.equal 1
- @Project.findOne.calledWith({_id: @project_id}).should.equal true
+ it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
+ @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
+ @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
+
+ it 'should call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
it 'should call User.getUser', ->
@UserGetter.getUser.callCount.should.equal 1
+ @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true
+
+ it 'should not call ProjectGetter.getProject', ->
+ @ProjectGetter.getProject.callCount.should.equal 0
+
+ describe 'when getProject produces an error', ->
+
+ beforeEach ->
+ @ProjectGetter.getProject.callsArgWith(2, new Error('woops'))
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
+
+ it 'should produce an error', ->
+ @next.callCount.should.equal 1
+ expect(@next.firstCall.args[0]).to.be.instanceof Error
+
+ it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
+ @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
+ @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
+
+ it 'should call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+
+ it 'should call User.getUser', ->
+ @UserGetter.getUser.callCount.should.equal 1
+ @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true
+
+ it 'should call ProjectGetter.getProject', ->
+ @ProjectGetter.getProject.callCount.should.equal 1
+
+ describe 'when Project.getUser does not find a user', ->
+
+ beforeEach ->
+ @ProjectGetter.getProject.callsArgWith(2, null, null)
+ @CollaboratorsInviteController.viewInvite @req, @res, @next
+
+ it 'should render the not-valid view template', ->
+ @res.render.callCount.should.equal 1
+ @res.render.calledWith('project/invite/not-valid').should.equal true
+
+ it 'should not call next', ->
+ @next.callCount.should.equal 0
+
+ it 'should call CollaboratorsHandler.isUserMemberOfProject', ->
+ @CollaboratorsHandler.isUserMemberOfProject.callCount.should.equal 1
+ @CollaboratorsHandler.isUserMemberOfProject.calledWith(@current_user_id, @project_id).should.equal true
+
+ it 'should call getInviteByToken', ->
+ @CollaboratorsInviteHandler.getInviteByToken.callCount.should.equal 1
+
+ it 'should call getUser', ->
+ @UserGetter.getUser.callCount.should.equal 1
+ @UserGetter.getUser.calledWith({_id: @fakeProject.owner_ref}).should.equal true
+
+ it 'should call ProjectGetter.getProject', ->
+ @ProjectGetter.getProject.callCount.should.equal 1
describe "revokeInvite", ->
From 8cb93511df0ca7b572ddf1ded14f6ab55db1e876 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 3 Aug 2016 11:55:24 +0100
Subject: [PATCH 100/378] Update UI of share modal
---
services/web/app/views/project/editor/share.jade | 4 +++-
.../ide/share/controllers/ShareProjectModalController.coffee | 3 +++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade
index a39f66055f..147803786d 100644
--- a/services/web/app/views/project/editor/share.jade
+++ b/services/web/app/views/project/editor/share.jade
@@ -45,7 +45,9 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
i.fa.fa-times
.row.project-invite(ng-repeat="invite in project.invites")
.col-xs-8 {{ invite.email }}
- span.label.label-primary #{translate("pending")}
+ div.small
+ | #{translate("invite_not_accepted")}.
+ a(href="#", ng-click="resendInvite(invite)") #{translate("resend")}
.col-xs-3.text-left
// todo: get invite privileges
span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")}
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index dc7e8a630e..942db3e71c 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -135,6 +135,9 @@ define [
$scope.state.inflight = false
$scope.state.error = "Sorry, something went wrong :("
+ $scope.resendInvite = (invite) ->
+ console.log ">> resend"
+
$scope.openMakePublicModal = () ->
$modal.open {
templateUrl: "makePublicModalTemplate"
From e6898d64c953ae10f15e0ca552af42dfd5d6c54d Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Wed, 3 Aug 2016 12:17:19 +0100
Subject: [PATCH 101/378] Track hovering of header features.
---
services/web/app/views/project/editor/header.jade | 3 +++
services/web/public/coffee/ide.coffee | 3 +++
.../public/coffee/ide/track-changes/TrackChangesManager.coffee | 1 +
.../controllers/TrackChangesDiffController.coffee | 2 ++
.../controllers/TrackChangesListController.coffee | 1 +
5 files changed, 10 insertions(+)
diff --git a/services/web/app/views/project/editor/header.jade b/services/web/app/views/project/editor/header.jade
index 06ac949c04..8fef09c9c2 100644
--- a/services/web/app/views/project/editor/header.jade
+++ b/services/web/app/views/project/editor/header.jade
@@ -91,12 +91,14 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
ng-if="permissions.admin",
tooltip="#{translate('share')}",
tooltip-placement="bottom",
+ ng-mouseenter="trackHover('share')"
ng-click="openShareProjectModal()",
ng-controller="ShareController"
)
i.fa.fa-fw.fa-group
a.btn.btn-full-height(
href,
+ ng-mouseenter="trackHover('track-changes')"
ng-click="toggleTrackChanges()",
ng-class="{ active: (ui.view == 'track-changes') }"
tooltip="#{translate('recent_changes')}",
@@ -108,6 +110,7 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
tooltip="#{translate('chat')}",
tooltip-placement="bottom",
ng-class="{ active: ui.chatOpen }",
+ ng-mouseenter="trackHover('chat')"
ng-click="toggleChat()",
ng-controller="ChatButtonController",
ng-show="!anonymous"
diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee
index e62227c9be..f18260d7d4 100644
--- a/services/web/public/coffee/ide.coffee
+++ b/services/web/public/coffee/ide.coffee
@@ -81,6 +81,9 @@ define [
$scope.$watch "ui.leftMenuShown", (isOpen) ->
event_tracking.sendCountlyOnce "ide-open-left-menu-once" if isOpen
+
+ $scope.trackHover = (feature) ->
+ event_tracking.sendCountlyOnce "ide-hover-#{feature}-once"
# End of tracking code.
window._ide = ide
diff --git a/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee
index 5270754db7..270c69bc6a 100644
--- a/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee
+++ b/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee
@@ -119,6 +119,7 @@ define [
diff.deleted = true
restoreDeletedDoc: (doc) ->
+ console.log("track-changes-restore-deleted")
url = "/project/#{@$scope.project_id}/doc/#{doc.id}/restore"
@ide.$http.post(url, name: doc.name, _csrf: window.csrfToken)
diff --git a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee
index 16547a491a..7e9354b887 100644
--- a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee
+++ b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee
@@ -8,6 +8,7 @@ define [
)
$scope.openRestoreDiffModal = () ->
+ console.log("track-changes-restore-modal")
$modal.open {
templateUrl: "trackChangesRestoreDiffModalTemplate"
controller: "TrackChangesRestoreDiffModalController"
@@ -22,6 +23,7 @@ define [
$scope.diff = diff
$scope.restore = () ->
+ console.log("track-changes-restored")
$scope.state.inflight = true
ide.trackChangesManager
.restoreDiff(diff)
diff --git a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee
index 8172b00727..1b85035b28 100644
--- a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee
+++ b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee
@@ -92,6 +92,7 @@ define [
$scope.recalculateSelectedUpdates()
$scope.select = () ->
+ console.log("track-changes-view")
$scope.update.selectedTo = true
$scope.update.selectedFrom = true
From 216779fafe141977b9f621055bfed73b71b37eec Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Wed, 3 Aug 2016 12:36:42 +0100
Subject: [PATCH 102/378] Track-changes usage.
---
.../coffee/ide/track-changes/TrackChangesManager.coffee | 1 -
.../controllers/TrackChangesDiffController.coffee | 9 +++++----
.../controllers/TrackChangesListController.coffee | 4 ++--
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee
index 270c69bc6a..5270754db7 100644
--- a/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee
+++ b/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee
@@ -119,7 +119,6 @@ define [
diff.deleted = true
restoreDeletedDoc: (doc) ->
- console.log("track-changes-restore-deleted")
url = "/project/#{@$scope.project_id}/doc/#{doc.id}/restore"
@ide.$http.post(url, name: doc.name, _csrf: window.csrfToken)
diff --git a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee
index 7e9354b887..94044f8956 100644
--- a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee
+++ b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee
@@ -1,14 +1,15 @@
define [
"base"
], (App) ->
- App.controller "TrackChangesDiffController", ($scope, $modal, ide) ->
+ App.controller "TrackChangesDiffController", ($scope, $modal, ide, event_tracking) ->
$scope.restoreDeletedDoc = () ->
+ event_tracking.sendCountly "track-changes-restore-deleted"
ide.trackChangesManager.restoreDeletedDoc(
$scope.trackChanges.diff.doc
)
$scope.openRestoreDiffModal = () ->
- console.log("track-changes-restore-modal")
+ event_tracking.sendCountly "track-changes-restore-modal"
$modal.open {
templateUrl: "trackChangesRestoreDiffModalTemplate"
controller: "TrackChangesRestoreDiffModalController"
@@ -16,14 +17,14 @@ define [
diff: () -> $scope.trackChanges.diff
}
- App.controller "TrackChangesRestoreDiffModalController", ($scope, $modalInstance, diff, ide) ->
+ App.controller "TrackChangesRestoreDiffModalController", ($scope, $modalInstance, diff, ide, event_tracking) ->
$scope.state =
inflight: false
$scope.diff = diff
$scope.restore = () ->
- console.log("track-changes-restored")
+ event_tracking.sendCountly "track-changes-restored"
$scope.state.inflight = true
ide.trackChangesManager
.restoreDiff(diff)
diff --git a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee
index 1b85035b28..88ab0053ed 100644
--- a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee
+++ b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee
@@ -2,7 +2,7 @@ define [
"base"
], (App) ->
- App.controller "TrackChangesPremiumPopup", ($scope, ide, sixpack)->
+ App.controller "TrackChangesPremiumPopup", ($scope, ide, sixpack, event_tracking)->
$scope.$watch "ui.view", ->
if $scope.ui.view == "track-changes"
if $scope.project?.features?.versioning
@@ -92,7 +92,7 @@ define [
$scope.recalculateSelectedUpdates()
$scope.select = () ->
- console.log("track-changes-view")
+ event_tracking.sendCountly "track-changes-view-change"
$scope.update.selectedTo = true
$scope.update.selectedFrom = true
From e7251aab53c4d7782d2d168b8bf9a576096f9d5e Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 3 Aug 2016 14:06:08 +0100
Subject: [PATCH 103/378] Small wording changes
---
.../app/coffee/Features/Email/EmailBuilder.coffee | 15 +++++++--------
services/web/app/views/project/invite/show.jade | 5 +++--
services/web/public/stylesheets/app/invite.less | 5 +++++
3 files changed, 15 insertions(+), 10 deletions(-)
diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee
index dab9fda839..ae3ee28253 100644
--- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee
+++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee
@@ -7,7 +7,7 @@ settings = require("settings-sharelatex")
templates = {}
-templates.registered =
+templates.registered =
subject: _.template "Activate your #{settings.appName} Account"
layout: PersonalEmailLayout
type: "notification"
@@ -19,7 +19,7 @@ templates.registered =
If you have any questions or problems, please contact #{settings.adminEmail}.
"""
-templates.canceledSubscription =
+templates.canceledSubscription =
subject: _.template "ShareLaTeX thoughts"
layout: PersonalEmailLayout
type:"lifecycle"
@@ -36,7 +36,7 @@ ShareLaTeX Co-founder
'''
-templates.passwordResetRequested =
+templates.passwordResetRequested =
subject: _.template "Password Reset - #{settings.appName}"
layout: NotificationEmailLayout
type:"notification"
@@ -66,7 +66,7 @@ If you didn't request a password reset, let us know.
#{settings.appName}
"""
-templates.projectSharedWithYou =
+templates.projectSharedWithYou =
subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
layout: NotificationEmailLayout
type:"notification"
@@ -88,7 +88,7 @@ templates.projectSharedWithYou =
"""
templates.projectInvite =
- subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
+ subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
layout: NotificationEmailLayout
type:"notification"
compiledTemplate: _.template """
@@ -98,7 +98,7 @@ templates.projectInvite =
@@ -108,7 +108,7 @@ templates.projectInvite =
#{settings.appName}
"""
-templates.completeJoinGroupAccount =
+templates.completeJoinGroupAccount =
subject: _.template "Verify Email to join <%= group_name %> group"
layout: NotificationEmailLayout
type:"notification"
@@ -143,4 +143,3 @@ module.exports =
html: template.layout(opts)
type:template.type
}
-
diff --git a/services/web/app/views/project/invite/show.jade b/services/web/app/views/project/invite/show.jade
index 158e5309f2..833e75085e 100644
--- a/services/web/app/views/project/invite/show.jade
+++ b/services/web/app/views/project/invite/show.jade
@@ -8,7 +8,8 @@ block content
.card.project-invite-accept
.page-header.text-centered
h1 #{translate("user_wants_you_to_see_project", {username:owner.first_name, projectname:""})}
- em #{project.name}
+ em
+ span.project-name #{project.name}
.row.text-center
.col-md-12
p
@@ -25,6 +26,6 @@ block content
input(name='token', type='hidden', value="#{invite.token}")
.form-group.text-center
button.btn.btn-lg.btn-primary(type="submit")
- | #{translate("accept_invite")}
+ | #{translate("join_project")}
.form-group.text-center
\ No newline at end of file
diff --git a/services/web/public/stylesheets/app/invite.less b/services/web/public/stylesheets/app/invite.less
index aaa6f08ab4..4e6f24303a 100644
--- a/services/web/public/stylesheets/app/invite.less
+++ b/services/web/public/stylesheets/app/invite.less
@@ -1,4 +1,9 @@
.project-invite-accept {
+ .page-header {
+ .project-name {
+ white-space: pre;
+ }
+ }
form {
padding-top: 15px;
}
From a5ddcc3df7af06f1f1ec4218fa7ef379d36c9d00 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 3 Aug 2016 15:42:19 +0100
Subject: [PATCH 104/378] Allow resending of invites
---
.../CollaboratorsInviteController.coffee | 10 +++
.../CollaboratorsInviteHandler.coffee | 12 ++++
.../Collaborators/CollaboratorsRouter.coffee | 6 ++
.../ShareProjectModalController.coffee | 10 ++-
.../ide/share/services/projectInvites.coffee | 5 ++
.../CollaboratorsInviteControllerTests.coffee | 44 +++++++++++++
.../CollaboratorsInviteHandlerTests.coffee | 61 +++++++++++++++++++
7 files changed, 147 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 05792679a6..1977a4e01d 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -50,6 +50,16 @@ module.exports = CollaboratorsInviteController =
return next(err)
res.sendStatus(201)
+ resendInvite: (req, res, next) ->
+ projectId = req.params.Project_id
+ inviteId = req.params.invite_id
+ logger.log {projectId, inviteId}, "resending invite"
+ CollaboratorsInviteHandler.resendInvite projectId, inviteId, (err) ->
+ if err?
+ logger.err {projectId, inviteId}, "error revoking invite"
+ return next(err)
+ res.sendStatus(201)
+
viewInvite: (req, res, next) ->
projectId = req.params.Project_id
token = req.params.token
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 25008cfde7..83e3c2bb18 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -54,6 +54,18 @@ module.exports = CollaboratorsInviteHandler =
return callback(err)
callback(null)
+ resendInvite: (projectId, inviteId, callback=(err)->) ->
+ logger.log {projectId, inviteId}, "resending invite email"
+ ProjectInvite.findOne {_id: inviteId, projectId: projectId}, (err, invite) ->
+ if err?
+ logger.err {err, projectId, inviteId}, "error finding invite"
+ return callback(err)
+ if !invite?
+ logger.err {err, projectId, inviteId}, "no invite found, nothing to resend"
+ return callback(null)
+ CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite
+ callback(null)
+
getInviteByToken: (projectId, tokenString, callback=(err,invite)->) ->
logger.log {projectId, tokenString}, "fetching invite by token"
ProjectInvite.findOne {projectId: projectId, token: tokenString}, (err, invite) ->
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
index f67bb3ee6c..0da728542e 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
@@ -29,6 +29,12 @@ module.exports =
CollaboratorsInviteController.revokeInvite
)
+ webRouter.post(
+ '/project/:Project_id/invite/:invite_id/resend',
+ AuthorizationMiddlewear.ensureUserCanAdminProject,
+ CollaboratorsInviteController.resendInvite
+ )
+
webRouter.get(
'/project/:Project_id/invite/token/:token',
AuthenticationController.requireLogin(),
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index 942db3e71c..a368bbb4f2 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -136,7 +136,15 @@ define [
$scope.state.error = "Sorry, something went wrong :("
$scope.resendInvite = (invite) ->
- console.log ">> resend"
+ $scope.state.error = null
+ $scope.state.inflight = true
+ projectInvites
+ .resendInvite(invite._id)
+ .success () ->
+ $scope.state.inflight = false
+ .error () ->
+ $scope.state.inflight = false
+ $scope.state.error = "Sorry, something went wrong resending the invite :("
$scope.openMakePublicModal = () ->
$modal.open {
diff --git a/services/web/public/coffee/ide/share/services/projectInvites.coffee b/services/web/public/coffee/ide/share/services/projectInvites.coffee
index f453792574..cad5122386 100644
--- a/services/web/public/coffee/ide/share/services/projectInvites.coffee
+++ b/services/web/public/coffee/ide/share/services/projectInvites.coffee
@@ -19,6 +19,11 @@ define [
"X-Csrf-Token": window.csrfToken
})
+ resendInvite: (inviteId, privileges) ->
+ $http.post("/project/#{ide.project_id}/invite/#{inviteId}/resend", {
+ _csrf: window.csrfToken
+ })
+
getInvites: () ->
$http.get("/project/#{ide.project_id}/invite", {
json: true
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 4cfac39232..7ac3d784c0 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -429,6 +429,50 @@ describe "CollaboratorsInviteController", ->
it 'should call ProjectGetter.getProject', ->
@ProjectGetter.getProject.callCount.should.equal 1
+ describe "resendInvite", ->
+
+ beforeEach ->
+ @req.params =
+ Project_id: @project_id
+ invite_id: @invite_id = "thuseoautoh"
+ @req.session =
+ user: _id: @current_user_id = "current-user-id"
+ @res.render = sinon.stub()
+ @res.sendStatus = sinon.stub()
+ @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(2, null)
+ @callback = sinon.stub()
+ @next = sinon.stub()
+
+ describe 'when resendInvite does not produce an error', ->
+
+ beforeEach ->
+ @CollaboratorsInviteController.resendInvite @req, @res, @next
+
+ it 'should produce a 201 response', ->
+ @res.sendStatus.callCount.should.equal 1
+ @res.sendStatus.calledWith(201).should.equal true
+
+ it 'should have called resendInvite', ->
+ @CollaboratorsInviteHandler.resendInvite.callCount.should.equal 1
+
+ describe 'when resendInvite produces an error', ->
+
+ beforeEach ->
+ @err = new Error('woops')
+ @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(2, @err)
+ @CollaboratorsInviteController.resendInvite @req, @res, @next
+
+ it 'should not produce a 201 response', ->
+ @res.sendStatus.callCount.should.equal 0
+
+ it 'should call next with the error', ->
+ @next.callCount.should.equal 1
+ @next.calledWith(@err).should.equal true
+
+ it 'should have called resendInvite', ->
+ @CollaboratorsInviteHandler.resendInvite.callCount.should.equal 1
+
+
describe "revokeInvite", ->
beforeEach ->
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index 4e9a2cc95a..0e8ceef0b3 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -209,6 +209,67 @@ describe "CollaboratorsInviteHandler", ->
expect(err).to.be.instanceof Error
done()
+ describe 'resendInvite', ->
+
+ beforeEach ->
+ @ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub()
+ @call = (callback) =>
+ @CollaboratorsInviteHandler.resendInvite @projectId, @inviteId, callback
+
+ describe 'when all goes well', ->
+
+ beforeEach ->
+
+ it 'should not produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.not.be.instanceof Error
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should call ProjectInvite.findOne', (done) ->
+ @call (err, invite) =>
+ @ProjectInvite.findOne.callCount.should.equal 1
+ @ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId}).should.equal true
+ done()
+
+ it 'should have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) ->
+ @call (err, invite) =>
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @email).should.equal true
+ done()
+
+ describe 'when findOne produces an error', ->
+
+ beforeEach ->
+ @ProjectInvite.findOne.callsArgWith(1, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err, invite) =>
+ expect(err).to.be.instanceof Error
+ done()
+
+ it 'should not have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) ->
+ @call (err, invite) =>
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 0
+ done()
+
+ describe 'when findOne does not find an invite', ->
+
+ beforeEach ->
+ @ProjectInvite.findOne.callsArgWith(1, null, null)
+
+ it 'should not produce an error', (done) ->
+ @call (err, invite) =>
+ expect(err).to.not.be.instanceof Error
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should not have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) ->
+ @call (err, invite) =>
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 0
+ done()
+
describe 'getInviteByToken', ->
beforeEach ->
From cf5c8e27e1627da080d33b57ea017da766151719 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Wed, 3 Aug 2016 16:05:19 +0100
Subject: [PATCH 105/378] UI fix - do not allow multiple clicks to restore a
deleted file.
---
.../views/project/editor/track-changes.jade | 18 +++++++++++++++---
.../track-changes/TrackChangesManager.coffee | 3 +++
.../TrackChangesDiffController.coffee | 15 ++++++++++++---
.../TrackChangesListController.coffee | 4 ++--
.../stylesheets/app/editor/track-changes.less | 4 ++++
5 files changed, 36 insertions(+), 8 deletions(-)
diff --git a/services/web/app/views/project/editor/track-changes.jade b/services/web/app/views/project/editor/track-changes.jade
index dc7ce38708..684b89e814 100644
--- a/services/web/app/views/project/editor/track-changes.jade
+++ b/services/web/app/views/project/editor/track-changes.jade
@@ -143,15 +143,27 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
navigate-highlights="true"
)
.diff-deleted.text-centered(
- ng-show="trackChanges.diff.deleted"
+ ng-show="trackChanges.diff.deleted && !trackChanges.diff.restoreDeletedSuccess"
)
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ trackChanges.diff.doc.name }} "})}
-
p
a.btn.btn-primary.btn-lg(
href,
- ng-click="restoreDeletedDoc()"
+ ng-click="restoreDeletedDoc()",
+ ng-disabled="trackChanges.diff.restoreInProgress"
) #{translate("restore")}
+
+ .diff-deleted.text-centered(
+ ng-show="trackChanges.diff.deleted && trackChanges.diff.restoreDeletedSuccess"
+ )
+ p.text-serif Your file ({{ trackChanges.diff.doc.name }}) has been recovered.
+ p.text-serif You can go back to the editor and work on it again.
+ p
+ a.btn.btn-default(
+ href,
+ ng-click="backToEditorAfterRestore()",
+ ) Back to editor
+
.loading-panel(ng-show="trackChanges.diff.loading")
i.fa.fa-spin.fa-refresh
| #{translate("loading")}...
diff --git a/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee
index 5270754db7..ecd3b5a7f8 100644
--- a/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee
+++ b/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee
@@ -117,6 +117,9 @@ define [
diff.error = true
else
diff.deleted = true
+ diff.restoreInProgress = false
+ diff.restoreDeletedSuccess = false
+ diff.restoredDocNewId = null
restoreDeletedDoc: (doc) ->
url = "/project/#{@$scope.project_id}/doc/#{doc.id}/restore"
diff --git a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee
index 94044f8956..77c2720a4f 100644
--- a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee
+++ b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee
@@ -4,9 +4,15 @@ define [
App.controller "TrackChangesDiffController", ($scope, $modal, ide, event_tracking) ->
$scope.restoreDeletedDoc = () ->
event_tracking.sendCountly "track-changes-restore-deleted"
- ide.trackChangesManager.restoreDeletedDoc(
- $scope.trackChanges.diff.doc
- )
+ $scope.trackChanges.diff.restoreInProgress = true
+ ide.trackChangesManager
+ .restoreDeletedDoc(
+ $scope.trackChanges.diff.doc
+ )
+ .success (response) ->
+ $scope.trackChanges.diff.restoredDocNewId = response.doc_id
+ $scope.trackChanges.diff.restoreInProgress = false
+ $scope.trackChanges.diff.restoreDeletedSuccess = true
$scope.openRestoreDiffModal = () ->
event_tracking.sendCountly "track-changes-restore-modal"
@@ -17,6 +23,9 @@ define [
diff: () -> $scope.trackChanges.diff
}
+ $scope.backToEditorAfterRestore = () ->
+ ide.editorManager.openDoc({ id: $scope.trackChanges.diff.restoredDocNewId })
+
App.controller "TrackChangesRestoreDiffModalController", ($scope, $modalInstance, diff, ide, event_tracking) ->
$scope.state =
inflight: false
diff --git a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee
index 88ab0053ed..e0beb3ef4f 100644
--- a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee
+++ b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee
@@ -2,7 +2,7 @@ define [
"base"
], (App) ->
- App.controller "TrackChangesPremiumPopup", ($scope, ide, sixpack, event_tracking)->
+ App.controller "TrackChangesPremiumPopup", ($scope, ide, sixpack)->
$scope.$watch "ui.view", ->
if $scope.ui.view == "track-changes"
if $scope.project?.features?.versioning
@@ -78,7 +78,7 @@ define [
$scope.recalculateSelectedUpdates()
]
- App.controller "TrackChangesListItemController", ["$scope", ($scope) ->
+ App.controller "TrackChangesListItemController", ["$scope", "event_tracking", ($scope, event_tracking) ->
$scope.$watch "update.selectedFrom", (selectedFrom, oldSelectedFrom) ->
if selectedFrom
for update in $scope.trackChanges.updates
diff --git a/services/web/public/stylesheets/app/editor/track-changes.less b/services/web/public/stylesheets/app/editor/track-changes.less
index b57e71ede0..6cf98e53eb 100644
--- a/services/web/public/stylesheets/app/editor/track-changes.less
+++ b/services/web/public/stylesheets/app/editor/track-changes.less
@@ -267,6 +267,10 @@
}
}
+.diff-deleted {
+ padding-top: 15px;
+}
+
.editor-dark {
#trackChanges {
aside.change-list {
From 73f115fc5781e041871a6556b07098c134bf0b21 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Wed, 3 Aug 2016 16:09:45 +0100
Subject: [PATCH 106/378] Add i18n keys.
---
services/web/app/views/project/editor/track-changes.jade | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/services/web/app/views/project/editor/track-changes.jade b/services/web/app/views/project/editor/track-changes.jade
index 684b89e814..de3c0e1730 100644
--- a/services/web/app/views/project/editor/track-changes.jade
+++ b/services/web/app/views/project/editor/track-changes.jade
@@ -156,13 +156,13 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
.diff-deleted.text-centered(
ng-show="trackChanges.diff.deleted && trackChanges.diff.restoreDeletedSuccess"
)
- p.text-serif Your file ({{ trackChanges.diff.doc.name }}) has been recovered.
- p.text-serif You can go back to the editor and work on it again.
+ p.text-serif #{translate("file_restored", {filename:"{{ trackChanges.diff.doc.name }} "})}
+ p.text-serif #{translate("file_restored_back_to_editor")}
p
a.btn.btn-default(
href,
ng-click="backToEditorAfterRestore()",
- ) Back to editor
+ ) #{translate("file_restored_back_to_editor_btn")}
.loading-panel(ng-show="trackChanges.diff.loading")
i.fa.fa-spin.fa-refresh
From 4d3629e5db00618a8b76ea7b50d5bd5debeb3a01 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Wed, 3 Aug 2016 16:29:34 +0100
Subject: [PATCH 107/378] add beta feature badge to syntax check
---
services/web/app/views/project/editor/pdf.jade | 1 +
1 file changed, 1 insertion(+)
diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade
index cf5129708a..2754e8499d 100644
--- a/services/web/app/views/project/editor/pdf.jade
+++ b/services/web/app/views/project/editor/pdf.jade
@@ -42,6 +42,7 @@ div.full-size.pdf(ng-controller="PdfController")
a(href, ng-click="recompile({check:true})")
i.fa.fa-fw()
| #{translate("run_syntax_check")}
+ span.beta-feature-badge
a(
href
ng-click="stop()"
From 7ce406dd384ccd66c41cddf0cdcbb67406df54fd Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Wed, 3 Aug 2016 16:30:18 +0100
Subject: [PATCH 108/378] switch the compile dropdown menu to left, to avoid
overflow into editor
---
services/web/app/views/project/editor/pdf.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade
index 2754e8499d..37f3b53822 100644
--- a/services/web/app/views/project/editor/pdf.jade
+++ b/services/web/app/views/project/editor/pdf.jade
@@ -25,7 +25,7 @@ div.full-size.pdf(ng-controller="PdfController")
dropdown-toggle
)
span.caret
- ul.dropdown-menu.dropdown-menu-right
+ ul.dropdown-menu.dropdown-menu-left
li.dropdown-header #{translate("compile_mode")}
li
a(href, ng-click="draft = false")
From 42388f8b762aa5549298ba3d26aff090ab17efff Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Wed, 3 Aug 2016 16:30:33 +0100
Subject: [PATCH 109/378] fix missing space in pdf jade template
---
services/web/app/views/project/editor/pdf.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade
index 37f3b53822..844f0f51dc 100644
--- a/services/web/app/views/project/editor/pdf.jade
+++ b/services/web/app/views/project/editor/pdf.jade
@@ -123,7 +123,7 @@ div.full-size.pdf(ng-controller="PdfController")
span(ng-show="entry.file") {{ entry.file }}
span(ng-show="entry.line") , line {{ entry.line }}
p.entry-message(ng-show="entry.message")
- {{ entry.type }} {{ entry.message }}
+ | {{ entry.type }} {{ entry.message }}
.card.card-hint(
ng-if="entry.humanReadableHint"
stop-propagation="click"
From 721ea88bd05e880c4bb720b03b81fe6ed2330060 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 3 Aug 2016 16:30:34 +0100
Subject: [PATCH 110/378] If email is already invited, resend the invite
---
.../Collaborators/CollaboratorsInviteController.coffee | 2 +-
.../share/controllers/ShareProjectModalController.coffee | 8 +++++++-
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 1977a4e01d..69b85a0a4d 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -56,7 +56,7 @@ module.exports = CollaboratorsInviteController =
logger.log {projectId, inviteId}, "resending invite"
CollaboratorsInviteHandler.resendInvite projectId, inviteId, (err) ->
if err?
- logger.err {projectId, inviteId}, "error revoking invite"
+ logger.err {projectId, inviteId}, "error resending invite"
return next(err)
res.sendStatus(201)
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index a368bbb4f2..0f93d925d4 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -43,6 +43,9 @@ define [
getCurrentMemberEmails = () ->
$scope.project.members.map (u) -> u.email
+ getCurrentInviteEmails = () ->
+ $scope.project.invites.map (u) -> u.email
+
$scope.filterAutocompleteUsers = ($query) ->
currentMemberEmails = getCurrentMemberEmails()
return $scope.autocompleteContacts.filter (contact) ->
@@ -63,6 +66,7 @@ define [
$scope.state.inflight = true
currentMemberEmails = getCurrentMemberEmails()
+ currentInviteEmails = getCurrentInviteEmails()
do addNextMember = () ->
if members.length == 0 or !$scope.canAddCollaborators
$scope.state.inflight = false
@@ -75,7 +79,9 @@ define [
return addNextMember()
# NOTE: groups aren't really a thing in ShareLaTeX, partially inherited from DJ
- if member.type == "user"
+ if member.display in currentInviteEmails and inviteId = _.find($scope.project.invites, (invite) -> invite.email == member.display)?._id
+ request = projectInvites.resendInvite(inviteId)
+ else if member.type == "user"
request = projectInvites.sendInvite(member.email, $scope.inputs.privileges)
else if member.type == "group"
request = projectMembers.addGroup(member.id, $scope.inputs.privileges)
From 092c0364067134d68ed466752ada7d02fe487a6d Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 4 Aug 2016 09:50:47 +0100
Subject: [PATCH 111/378] Rate-limit calls to invite api
---
.../Collaborators/CollaboratorsRouter.coffee | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
index 0da728542e..b19011c5f9 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
@@ -2,6 +2,7 @@ CollaboratorsController = require('./CollaboratorsController')
AuthenticationController = require('../Authentication/AuthenticationController')
AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear')
CollaboratorsInviteController = require('./CollaboratorsInviteController')
+RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear')
module.exports =
apply: (webRouter, apiRouter) ->
@@ -13,24 +14,40 @@ module.exports =
# invites
webRouter.post(
'/project/:Project_id/invite',
+ RateLimiterMiddlewear.rateLimit({
+ endpointName: "invite-to-project"
+ params: ["Project_id"]
+ maxRequests: 200
+ timeInterval: 60 * 10
+ }),
+ AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.inviteToProject
)
webRouter.get(
'/project/:Project_id/invite',
+ AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.getAllInvites
)
webRouter.delete(
'/project/:Project_id/invite/:invite_id',
+ AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.revokeInvite
)
webRouter.post(
'/project/:Project_id/invite/:invite_id/resend',
+ RateLimiterMiddlewear.rateLimit({
+ endpointName: "resend-invite"
+ params: ["Project_id"]
+ maxRequests: 200
+ timeInterval: 60 * 10
+ }),
+ AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.resendInvite
)
From 8f7603c324beabad9ede1ad2dbaaa9f5c27c1a54 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 4 Aug 2016 16:47:48 +0100
Subject: [PATCH 112/378] Add an endpoint to access project members
---
.../CollaboratorsController.coffee | 10 ++
.../Collaborators/CollaboratorsHandler.coffee | 10 ++
.../Collaborators/CollaboratorsRouter.coffee | 9 +-
.../Project/ProjectEditorHandler.coffee | 25 ++--
.../ide/share/services/projectInvites.coffee | 2 +-
.../ide/share/services/projectMembers.coffee | 8 ++
.../CollaboratorsControllerTests.coffee | 53 ++++++--
.../CollaboratorsHandlerTests.coffee | 117 +++++++++++++-----
.../Project/ProjectEditorHandlerTests.coffee | 41 +++++-
9 files changed, 224 insertions(+), 51 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
index 9e04b5e990..9757a3e741 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
@@ -5,6 +5,7 @@ EditorRealTimeController = require "../Editor/EditorRealTimeController"
LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
EmailHelper = require "../Helpers/EmailHelper"
+logger = require 'logger-sharelatex'
module.exports = CollaboratorsController =
@@ -50,3 +51,12 @@ module.exports = CollaboratorsController =
return callback(error) if error?
EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id)
callback()
+
+ getAllMembers: (req, res, next) ->
+ projectId = req.params.Project_id
+ logger.log {projectId}, "getting all active members for project"
+ CollaboratorsHandler.getAllMembers projectId, (err, members) ->
+ if err?
+ logger.err {projectId}, "error getting members for project"
+ return next(err)
+ res.json({members: members})
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
index 58ad246b60..822201a83e 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
@@ -8,6 +8,7 @@ async = require "async"
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
Errors = require "../Errors/Errors"
EmailHelper = require "../Helpers/EmailHelper"
+ProjectEditorHandler = require "../Project/ProjectEditorHandler"
module.exports = CollaboratorsHandler =
@@ -144,3 +145,12 @@ module.exports = CollaboratorsHandler =
if error?
logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator"
callback()
+
+ getAllMembers: (projectId, callback=(err, members)->) ->
+ logger.log {projectId}, "fetching all members"
+ CollaboratorsHandler.getMembersWithPrivilegeLevels projectId, (error, rawMembers) ->
+ if error?
+ logger.err {projectId, error}, "error getting members for project"
+ return callback(error)
+ {owner, members} = ProjectEditorHandler.buildOwnerAndMembersViews(rawMembers)
+ callback(null, members)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
index b19011c5f9..7fc4722ef2 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
@@ -11,6 +11,13 @@ module.exports =
webRouter.post '/project/:Project_id/users', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.addUserToProject
webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject
+ webRouter.get(
+ '/project/:Project_id/members',
+ AuthenticationController.requireLogin(),
+ AuthorizationMiddlewear.ensureUserCanAdminProject,
+ CollaboratorsController.getAllMembers
+ )
+
# invites
webRouter.post(
'/project/:Project_id/invite',
@@ -26,7 +33,7 @@ module.exports =
)
webRouter.get(
- '/project/:Project_id/invite',
+ '/project/:Project_id/invites',
AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.getAllInvites
diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
index 28d637eab6..a20cc99fc0 100644
--- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
@@ -17,16 +17,11 @@ module.exports = ProjectEditorHandler =
members: []
invites: invites || []
- owner = null
- for member in members
- if member.privilegeLevel == "owner"
- owner = member.user
- else
- result.members.push @buildUserModelView member.user, member.privilegeLevel
- if owner?
- result.owner = @buildUserModelView owner, "owner"
+ {owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
+ result.owner = owner
+ result.members = members
- result.features = _.defaults(owner?.features or {}, {
+ result.features = _.defaults(ownerFeatures or {}, {
collaborators: -1 # Infinite
versioning: false
dropbox:false
@@ -38,6 +33,18 @@ module.exports = ProjectEditorHandler =
return result
+ buildOwnerAndMembersViews: (members) ->
+ owner = null
+ ownerFeatures = null
+ filteredMembers = []
+ for member in members
+ if member.privilegeLevel == "owner"
+ ownerFeatures = member.user.features
+ owner = @buildUserModelView member.user, "owner"
+ else
+ filteredMembers.push @buildUserModelView member.user, member.privilegeLevel
+ {owner: owner, ownerFeatures: ownerFeatures, members: filteredMembers}
+
buildUserModelView: (user, privileges) ->
_id : user._id
first_name : user.first_name
diff --git a/services/web/public/coffee/ide/share/services/projectInvites.coffee b/services/web/public/coffee/ide/share/services/projectInvites.coffee
index cad5122386..4c0d30add6 100644
--- a/services/web/public/coffee/ide/share/services/projectInvites.coffee
+++ b/services/web/public/coffee/ide/share/services/projectInvites.coffee
@@ -25,7 +25,7 @@ define [
})
getInvites: () ->
- $http.get("/project/#{ide.project_id}/invite", {
+ $http.get("/project/#{ide.project_id}/invites", {
json: true
headers:
"X-Csrf-Token": window.csrfToken
diff --git a/services/web/public/coffee/ide/share/services/projectMembers.coffee b/services/web/public/coffee/ide/share/services/projectMembers.coffee
index 3f86b45cd4..f1b2c8c3fe 100644
--- a/services/web/public/coffee/ide/share/services/projectMembers.coffee
+++ b/services/web/public/coffee/ide/share/services/projectMembers.coffee
@@ -17,5 +17,13 @@ define [
privileges: privileges
_csrf: window.csrfToken
})
+
+ getMembers: () ->
+ $http.get("/project/#{ide.project_id}/members", {
+ json: true
+ headers:
+ "X-Csrf-Token": window.csrfToken
+ })
+
}
]
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee
index 32de9ebe0a..6a76afc459 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee
@@ -18,6 +18,7 @@ describe "CollaboratorsController", ->
'../Subscription/LimitationsManager' : @LimitationsManager = {}
'../Project/ProjectEditorHandler' : @ProjectEditorHandler = {}
'../User/UserGetter': @UserGetter = {}
+ 'logger-sharelatex': @logger = {err: sinon.stub(), erro: sinon.stub(), log: sinon.stub()}
@res = new MockResponse()
@req = new MockRequest()
@@ -73,10 +74,10 @@ describe "CollaboratorsController", ->
beforeEach ->
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false)
@CollaboratorsController.addUserToProject @req, @res, @next
-
+
it "should not add the user to the project", ->
@CollaboratorsHandler.addEmailToProject.called.should.equal false
-
+
it "should not emit a userAddedToProject event", ->
@EditorRealTimeController.emitToRoom.called.should.equal false
@@ -93,17 +94,17 @@ describe "CollaboratorsController", ->
@res.status = sinon.stub().returns @res
@res.send = sinon.stub()
@CollaboratorsController.addUserToProject @req, @res, @next
-
+
it "should not add the user to the project", ->
@CollaboratorsHandler.addEmailToProject.called.should.equal false
-
+
it "should not emit a userAddedToProject event", ->
@EditorRealTimeController.emitToRoom.called.should.equal false
it "should return a 400 response", ->
@res.status.calledWith(400).should.equal true
@res.send.calledWith("invalid email address").should.equal true
-
+
describe "removeUserFromProject", ->
beforeEach ->
@req.params =
@@ -113,17 +114,17 @@ describe "CollaboratorsController", ->
@EditorRealTimeController.emitToRoom = sinon.stub()
@CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArg(2)
@CollaboratorsController.removeUserFromProject @req, @res
-
+
it "should from the user from the project", ->
@CollaboratorsHandler.removeUserFromProject
.calledWith(@project_id, @user_id)
.should.equal true
-
+
it "should emit a userRemovedFromProject event to the proejct", ->
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, 'userRemovedFromProject', @user_id)
.should.equal true
-
+
it "should send the back a success response", ->
@res.sendStatus.calledWith(204).should.equal true
@@ -141,7 +142,7 @@ describe "CollaboratorsController", ->
@CollaboratorsHandler.removeUserFromProject
.calledWith(@project_id, @user_id)
.should.equal true
-
+
it "should emit a userRemovedFromProject event to the proejct", ->
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, 'userRemovedFromProject', @user_id)
@@ -150,3 +151,37 @@ describe "CollaboratorsController", ->
it "should return a success code", ->
@res.sendStatus.calledWith(204).should.equal true
+ describe 'getAllMembers', ->
+ beforeEach ->
+ @req.session =
+ user: _id: @user_id = "user-id-123"
+ @req.params = Project_id: @project_id
+ @res.json = sinon.stub()
+ @next = sinon.stub()
+ @members = [{a: 1}]
+ @CollaboratorsHandler.getAllMembers = sinon.stub().callsArgWith(1, null, @members)
+ @CollaboratorsController.getAllMembers(@req, @res, @next)
+
+ it 'should not produce an error', ->
+ @next.callCount.should.equal 0
+
+ it 'should produce a json response', ->
+ @res.json.callCount.should.equal 1
+ @res.json.calledWith({members: @members}).should.equal true
+
+ it 'should call CollaboratorsHandler.getAllMembers', ->
+ @CollaboratorsHandler.getAllMembers.callCount.should.equal 1
+
+ describe 'when CollaboratorsHandler.getAllMembers produces an error', ->
+ beforeEach ->
+ @res.json = sinon.stub()
+ @next = sinon.stub()
+ @CollaboratorsHandler.getAllMembers = sinon.stub().callsArgWith(1, new Error('woops'))
+ @CollaboratorsController.getAllMembers(@req, @res, @next)
+
+ it 'should produce an error', ->
+ @next.callCount.should.equal 1
+ @next.firstCall.args[0].should.be.instanceof Error
+
+ it 'should not produce a json response', ->
+ @res.json.callCount.should.equal 0
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee
index 37e39aaacb..efbf30487e 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee
@@ -18,13 +18,14 @@ describe "CollaboratorsHandler", ->
"../Project/ProjectEntityHandler": @ProjectEntityHandler = {}
"./CollaboratorsEmailHandler": @CollaboratorsEmailHandler = {}
"../Errors/Errors": Errors
+ "../Project/ProjectEditorHandler": @ProjectEditorHandler = {}
@project_id = "mock-project-id"
@user_id = "mock-user-id"
@adding_user_id = "adding-user-id"
@email = "joe@sharelatex.com"
@callback = sinon.stub()
-
+
describe "getMemberIdsWithPrivilegeLevels", ->
describe "with project", ->
beforeEach ->
@@ -46,16 +47,16 @@ describe "CollaboratorsHandler", ->
{ id: "read-write-ref-2", privilegeLevel: "readAndWrite" }
])
.should.equal true
-
+
describe "with a missing project", ->
beforeEach ->
@Project.findOne = sinon.stub().yields(null, null)
-
+
it "should return a NotFoundError", (done) ->
@CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, (error) ->
error.should.be.instanceof Errors.NotFoundError
done()
-
+
describe "getMemberIds", ->
beforeEach ->
@CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub()
@@ -68,7 +69,7 @@ describe "CollaboratorsHandler", ->
@callback
.calledWith(null, ["member-id-1", "member-id-2"])
.should.equal true
-
+
describe "getMembersWithPrivilegeLevels", ->
beforeEach ->
@CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub()
@@ -86,7 +87,7 @@ describe "CollaboratorsHandler", ->
@UserGetter.getUser.withArgs("read-write-ref-2").yields(null, { _id: "read-write-ref-2" })
@UserGetter.getUser.withArgs("doesnt-exist").yields(null, null)
@CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback
-
+
it "should return an array of members with their privilege levels", ->
@callback
.calledWith(null, [
@@ -96,7 +97,7 @@ describe "CollaboratorsHandler", ->
{ user: { _id: "read-write-ref-2" }, privilegeLevel: "readAndWrite" }
])
.should.equal true
-
+
describe "getMemberIdPrivilegeLevel", ->
beforeEach ->
@CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub()
@@ -116,7 +117,7 @@ describe "CollaboratorsHandler", ->
@CollaboratorHandler.getMemberIdPrivilegeLevel "member-id-3", @project_id, (error, level) ->
expect(level).to.equal false
done()
-
+
describe "isUserMemberOfProject", ->
beforeEach ->
@CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub()
@@ -128,7 +129,7 @@ describe "CollaboratorsHandler", ->
{ id: @user_id, privilegeLevel: "readAndWrite" }
])
@CollaboratorHandler.isUserMemberOfProject @user_id, @project_id, @callback
-
+
it "should return true and the privilegeLevel", ->
@callback
.calledWith(null, true, "readAndWrite")
@@ -140,12 +141,12 @@ describe "CollaboratorsHandler", ->
{ id: "not-the-user", privilegeLevel: "readOnly" }
])
@CollaboratorHandler.isUserMemberOfProject @user_id, @project_id, @callback
-
+
it "should return false", ->
@callback
.calledWith(null, false, null)
.should.equal true
-
+
describe "getProjectsUserIsCollaboratorOf", ->
beforeEach ->
@fields = "mock fields"
@@ -153,7 +154,7 @@ describe "CollaboratorsHandler", ->
@Project.find.withArgs({collaberator_refs:@user_id}, @fields).yields(null, ["mock-read-write-project-1", "mock-read-write-project-2"])
@Project.find.withArgs({readOnly_refs:@user_id}, @fields).yields(null, ["mock-read-only-project-1", "mock-read-only-project-2"])
@CollaboratorHandler.getProjectsUserIsCollaboratorOf @user_id, @fields, @callback
-
+
it "should call the callback with the projects", ->
@callback
.calledWith(null, ["mock-read-write-project-1", "mock-read-write-project-2"], ["mock-read-only-project-1", "mock-read-only-project-2"])
@@ -172,7 +173,7 @@ describe "CollaboratorsHandler", ->
"$pull":{collaberator_refs:@user_id, readOnly_refs:@user_id}
})
.should.equal true
-
+
describe "addUserToProject", ->
beforeEach ->
@Project.update = sinon.stub().callsArg(2)
@@ -182,7 +183,7 @@ describe "CollaboratorsHandler", ->
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user = { _id: @user_id, email: @email })
@CollaboratorsEmailHandler.notifyUserOfProjectShare = sinon.stub()
@ContactManager.addContact = sinon.stub()
-
+
describe "as readOnly", ->
beforeEach ->
@CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readOnly", @callback
@@ -195,7 +196,7 @@ describe "CollaboratorsHandler", ->
"$addToSet":{ readOnly_refs: @user_id }
})
.should.equal true
-
+
it "should flush the project to the TPDS", ->
@ProjectEntityHandler.flushProjectToThirdPartyDataStore
.calledWith(@project_id)
@@ -205,12 +206,12 @@ describe "CollaboratorsHandler", ->
@CollaboratorsEmailHandler.notifyUserOfProjectShare
.calledWith(@project_id, @email)
.should.equal true
-
+
it "should add the user as a contact for the adding user", ->
@ContactManager.addContact
.calledWith(@adding_user_id, @user_id)
.should.equal true
-
+
describe "as readAndWrite", ->
beforeEach ->
@CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readAndWrite", @callback
@@ -223,19 +224,19 @@ describe "CollaboratorsHandler", ->
"$addToSet":{ collaberator_refs: @user_id }
})
.should.equal true
-
+
it "should flush the project to the TPDS", ->
@ProjectEntityHandler.flushProjectToThirdPartyDataStore
.calledWith(@project_id)
.should.equal true
-
+
describe "with invalid privilegeLevel", ->
beforeEach ->
@CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "notValid", @callback
it "should call the callback with an error", ->
@callback.calledWith(new Error()).should.equal true
-
+
describe "when user already exists as a collaborator", ->
beforeEach ->
@project.collaberator_refs = [@user_id]
@@ -243,7 +244,7 @@ describe "CollaboratorsHandler", ->
it "should not add the user again", ->
@Project.update.called.should.equal false
-
+
describe "addEmailToProject", ->
beforeEach ->
@UserCreator.getUserOrCreateHoldingAccount = sinon.stub().callsArgWith(1, null, @user = {_id: @user_id})
@@ -252,27 +253,27 @@ describe "CollaboratorsHandler", ->
describe "with a valid email", ->
beforeEach ->
@CollaboratorHandler.addEmailToProject @project_id, @adding_user_id, (@email = "Joe@example.com"), (@privilegeLevel = "readAndWrite"), @callback
-
+
it "should get the user with the lowercased email", ->
@UserCreator.getUserOrCreateHoldingAccount
.calledWith(@email.toLowerCase())
.should.equal true
-
+
it "should add the user to the project by id", ->
@CollaboratorHandler.addUserIdToProject
.calledWith(@project_id, @adding_user_id, @user_id, @privilegeLevel)
.should.equal true
-
+
it "should return the callback with the user_id", ->
@callback.calledWith(null, @user_id).should.equal true
-
+
describe "with an invalid email", ->
beforeEach ->
@CollaboratorHandler.addEmailToProject @project_id, @adding_user_id, "not-and-email", (@privilegeLevel = "readAndWrite"), @callback
-
+
it "should call the callback with an error", ->
@callback.calledWith(new Error()).should.equal true
-
+
it "should not add any users to the proejct", ->
@CollaboratorHandler.addUserIdToProject.called.should.equal false
@@ -286,9 +287,67 @@ describe "CollaboratorsHandler", ->
)
@CollaboratorHandler.removeUserFromProject = sinon.stub().yields()
@CollaboratorHandler.removeUserFromAllProjets @user_id, done
-
+
it "should remove the user from each project", ->
for project_id in ["read-and-write-0", "read-and-write-1", "read-only-0", "read-only-1"]
@CollaboratorHandler.removeUserFromProject
.calledWith(project_id, @user_id)
- .should.equal true
\ No newline at end of file
+ .should.equal true
+
+ describe 'getAllMembers', ->
+
+ beforeEach ->
+ @owning_user = {_id: 'owner-id', email: 'owner@example.com', features: {a: 1}}
+ @readwrite_user = {_id: 'readwrite-id', email: 'readwrite@example.com'}
+ @members = [
+ {user: @owning_user, privilegeLevel: "owner"},
+ {user: @readwrite_user, privilegeLevel: "readAndWrite"}
+ ]
+ @CollaboratorHandler.getMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, null, @members)
+ @ProjectEditorHandler.buildOwnerAndMembersViews = sinon.stub().returns(@views = {
+ owner: @owning_user,
+ ownerFeatures: @owning_user.features,
+ members: [ {_id: @readwrite_user._id, email: @readwrite_user.email} ]
+ })
+ @callback = sinon.stub()
+ @CollaboratorHandler.getAllMembers @project_id, @callback
+
+ it 'should not produce an error', ->
+ @callback.callCount.should.equal 1
+ expect(@callback.firstCall.args[0]).to.equal null
+
+ it 'should produce a list of members', ->
+ @callback.callCount.should.equal 1
+ expect(@callback.firstCall.args[1]).to.deep.equal @views.members
+
+ it 'should call getMembersWithPrivileges', ->
+ @CollaboratorHandler.getMembersWithPrivilegeLevels.callCount.should.equal 1
+ @CollaboratorHandler.getMembersWithPrivilegeLevels.firstCall.args[0].should.equal @project_id
+
+ it 'should call ProjectEditorHandler.buildOwnerAndMembersViews', ->
+ @ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal 1
+ @ProjectEditorHandler.buildOwnerAndMembersViews.firstCall.args[0].should.equal @members
+
+ describe 'when getMembersWithPrivileges produces an error', ->
+
+ beforeEach ->
+ @CollaboratorHandler.getMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, new Error('woops'))
+ @ProjectEditorHandler.buildOwnerAndMembersViews = sinon.stub().returns(@views = {
+ owner: @owning_user,
+ ownerFeatures: @owning_user.features,
+ members: [ {_id: @readwrite_user._id, email: @readwrite_user.email} ]
+ })
+ @callback = sinon.stub()
+ @CollaboratorHandler.getAllMembers @project_id, @callback
+
+ it 'should produce an error', ->
+ @callback.callCount.should.equal 1
+ expect(@callback.firstCall.args[0]).to.not.equal null
+ expect(@callback.firstCall.args[0]).to.be.instanceof Error
+
+ it 'should call getMembersWithPrivileges', ->
+ @CollaboratorHandler.getMembersWithPrivilegeLevels.callCount.should.equal 1
+ @CollaboratorHandler.getMembersWithPrivilegeLevels.firstCall.args[0].should.equal @project_id
+
+ it 'should not call ProjectEditorHandler.buildOwnerAndMembersViews', ->
+ @ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal 0
diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee
index 671ae4a65b..dc68e5dda6 100644
--- a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee
@@ -1,4 +1,5 @@
chai = require('chai')
+expect = chai.expect
should = chai.should()
modulePath = "../../../../app/js/Features/Project/ProjectEditorHandler"
@@ -105,7 +106,7 @@ describe "ProjectEditorHandler", ->
it "should include the deletedDocs", ->
should.exist @result.deletedDocs
@result.deletedDocs.should.equal @project.deletedDocs
-
+
it "should gather readOnly_refs and collaberators_refs into a list of members", ->
findMember = (id) =>
for member in @result.members
@@ -176,10 +177,46 @@ describe "ProjectEditorHandler", ->
compileGroup:"priority"
compileTimeout: 96
@result = @handler.buildProjectModelView @project, @members
-
+
it "should copy the owner features to the project", ->
@result.features.versioning.should.equal @owner.features.versioning
@result.features.collaborators.should.equal @owner.features.collaborators
@result.features.compileGroup.should.equal @owner.features.compileGroup
@result.features.compileTimeout.should.equal @owner.features.compileTimeout
+ describe 'buildOwnerAndMembersViews', ->
+ beforeEach ->
+ @owner.features =
+ versioning: true
+ collaborators: 3
+ compileGroup:"priority"
+ compileTimeout: 22
+ @result = @handler.buildOwnerAndMembersViews @members
+
+ it 'should produce an object with owner, ownerFeatures and members keys', ->
+ expect(@result).to.have.all.keys ['owner', 'ownerFeatures', 'members']
+
+ it 'should separate the owner from the members', ->
+ @result.members.length.should.equal(@members.length-1)
+ expect(@result.owner._id).to.equal @owner._id
+ expect(@result.owner.email).to.equal @owner.email
+ expect(@result.members.filter((m) => m._id == @owner._id).length).to.equal 0
+
+ it 'should extract the ownerFeatures from the owner object', ->
+ expect(@result.ownerFeatures).to.deep.equal @owner.features
+
+ describe 'when there is no owner', ->
+ beforeEach ->
+ # remove the owner from members list
+ @membersWithoutOwner = @members.filter((m) => m.user._id != @owner._id)
+ @result = @handler.buildOwnerAndMembersViews @membersWithoutOwner
+
+ it 'should produce an object with owner, ownerFeatures and members keys', ->
+ expect(@result).to.have.all.keys ['owner', 'ownerFeatures', 'members']
+
+ it 'should not separate out an owner', ->
+ @result.members.length.should.equal @membersWithoutOwner.length
+ expect(@result.owner).to.equal null
+
+ it 'should not extract the ownerFeatures from the owner object', ->
+ expect(@result.ownerFeatures).to.equal null
From eafd61a90edca8417050fc202d01f69e1c9e89ae Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 5 Aug 2016 14:01:08 +0100
Subject: [PATCH 113/378] Refresh members and invites in client when status
changes
---
.../CollaboratorsController.coffee | 1 +
.../CollaboratorsInviteController.coffee | 4 +++
.../share/controllers/ShareController.coffee | 27 +++++++++++++------
.../ShareProjectModalController.coffee | 2 ++
.../CollaboratorsControllerTests.coffee | 3 +++
.../CollaboratorsInviteControllerTests.coffee | 13 +++++++++
6 files changed, 42 insertions(+), 8 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
index 9757a3e741..a889e9183a 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee
@@ -37,6 +37,7 @@ module.exports = CollaboratorsController =
user_id = req.params.user_id
CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) ->
return next(error) if error?
+ EditorRealTimeController.emitToRoom project_id, 'project:membership:changed', {members: true}
res.sendStatus 204
removeSelfFromProject: (req, res, next = (error) ->) ->
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 69b85a0a4d..c149998eef 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -5,6 +5,7 @@ CollaboratorsHandler = require('./CollaboratorsHandler')
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
logger = require('logger-sharelatex')
EmailHelper = require "../Helpers/EmailHelper"
+EditorRealTimeController = require("../Editor/EditorRealTimeController")
module.exports = CollaboratorsInviteController =
@@ -38,6 +39,7 @@ module.exports = CollaboratorsInviteController =
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) ->
@@ -48,6 +50,7 @@ module.exports = CollaboratorsInviteController =
if err?
logger.err {projectId, inviteId}, "error revoking invite"
return next(err)
+ EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true}
res.sendStatus(201)
resendInvite: (req, res, next) ->
@@ -113,4 +116,5 @@ module.exports = CollaboratorsInviteController =
if err?
logger.err {projectId, inviteId}, "error accepting invite by token"
return next(err)
+ EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
res.redirect "/project/#{projectId}"
diff --git a/services/web/public/coffee/ide/share/controllers/ShareController.coffee b/services/web/public/coffee/ide/share/controllers/ShareController.coffee
index 2378391974..6614cfb7fc 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareController.coffee
@@ -1,13 +1,24 @@
define [
"base"
], (App) ->
- App.controller "ShareController", ["$scope", "$modal", "event_tracking", ($scope, $modal, event_tracking) ->
- $scope.openShareProjectModal = () ->
- event_tracking.sendCountlyOnce "ide-open-share-modal-once"
+ App.controller "ShareController", ["$scope", "$modal", "ide", "projectInvites", "projectMembers", "event_tracking",
+ ($scope, $modal, ide, projectInvites, projectMembers, event_tracking) ->
+ $scope.openShareProjectModal = () ->
+ event_tracking.sendCountlyOnce "ide-open-share-modal-once"
- $modal.open(
- templateUrl: "shareProjectModalTemplate"
- controller: "ShareProjectModalController"
- scope: $scope
- )
+ $modal.open(
+ templateUrl: "shareProjectModalTemplate"
+ controller: "ShareProjectModalController"
+ scope: $scope
+ )
+
+ ide.socket.on 'project:membership:changed', (data) =>
+ if data.members
+ projectMembers.getMembers().success (responseData) =>
+ if responseData.members
+ $scope.project.members = responseData.members
+ if data.invites
+ projectInvites.getInvites().success (responseData) =>
+ if responseData.invites
+ $scope.project.invites = responseData.invites
]
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index 0f93d925d4..7cbfcd6a57 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -23,6 +23,8 @@ define [
allowedNoOfMembers = $scope.project.features.collaborators
$scope.canAddCollaborators = noOfMembers < allowedNoOfMembers or allowedNoOfMembers == INFINITE_COLLABORATORS
+ window._m = projectMembers
+
$scope.autocompleteContacts = []
do loadAutocompleteUsers = () ->
$http.get "/user/contacts"
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee
index 6a76afc459..35ae828708 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee
@@ -128,6 +128,9 @@ describe "CollaboratorsController", ->
it "should send the back a success response", ->
@res.sendStatus.calledWith(204).should.equal true
+ it 'should have called emitToRoom', ->
+ @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true
+
describe "removeSelfFromProject", ->
beforeEach ->
@req.session =
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 7ac3d784c0..680f7da8ad 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -22,6 +22,7 @@ describe "CollaboratorsInviteController", ->
"./CollaboratorsHandler": @CollaboratorsHandler = {}
"./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
+ "../Editor/EditorRealTimeController": @EditorRealTimeController = {emitToRoom: sinon.stub()}
@res = new MockResponse()
@req = new MockRequest()
@@ -112,6 +113,10 @@ describe "CollaboratorsInviteController", ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
@CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true
+ it 'should have called emitToRoom', ->
+ @EditorRealTimeController.emitToRoom.callCount.should.equal 1
+ @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true
+
describe 'when the user is not allowed to add more collaborators', ->
beforeEach ->
@@ -499,6 +504,10 @@ describe "CollaboratorsInviteController", ->
it 'should have called revokeInvite', ->
@CollaboratorsInviteHandler.revokeInvite.callCount.should.equal 1
+ it 'should have called emitToRoom', ->
+ @EditorRealTimeController.emitToRoom.callCount.should.equal 1
+ @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true
+
describe 'when revokeInvite produces an error', ->
beforeEach ->
@@ -544,6 +553,10 @@ describe "CollaboratorsInviteController", ->
it 'should have called acceptInvite', ->
@CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1
+ it 'should have called emitToRoom', ->
+ @EditorRealTimeController.emitToRoom.callCount.should.equal 1
+ @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true
+
describe 'when revokeInvite produces an error', ->
beforeEach ->
From d59b51aacd3420e28d8eda515f503594dffaa5d3 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 5 Aug 2016 14:09:37 +0100
Subject: [PATCH 114/378] Add error handlers.
---
.../share/controllers/ShareController.coffee | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/services/web/public/coffee/ide/share/controllers/ShareController.coffee b/services/web/public/coffee/ide/share/controllers/ShareController.coffee
index 6614cfb7fc..1400a47371 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareController.coffee
@@ -14,11 +14,17 @@ define [
ide.socket.on 'project:membership:changed', (data) =>
if data.members
- projectMembers.getMembers().success (responseData) =>
- if responseData.members
- $scope.project.members = responseData.members
+ projectMembers.getMembers()
+ .success (responseData) =>
+ if responseData.members
+ $scope.project.members = responseData.members
+ .error (responseDate) =>
+ console.error "Error fetching members for project"
if data.invites
- projectInvites.getInvites().success (responseData) =>
- if responseData.invites
- $scope.project.invites = responseData.invites
+ projectInvites.getInvites()
+ .success (responseData) =>
+ if responseData.invites
+ $scope.project.invites = responseData.invites
+ .error (responseDate) =>
+ console.error "Error fetching invites for project"
]
From 24d3eed77cb9f9cfa9eea881b6bdea1d1ed05790 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Fri, 5 Aug 2016 15:15:29 +0100
Subject: [PATCH 115/378] resized harvard
---
services/web/public/img/crests/harvard.gif | Bin 3314 -> 3314 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/services/web/public/img/crests/harvard.gif b/services/web/public/img/crests/harvard.gif
index f9ba6a2298dfb1c55d58b3dcab97a93f09f297b5..092e17f4b3b2771bf63f7c7c773d81fc6344a38b 100644
GIT binary patch
delta 21
ccmew)`AL$;-P6s&GVuw66~i}%jXckI09D-wjsO4v
delta 21
ccmew)`AL$;-P6s&GVuw66~nW*jXckI09R87+yDRo
From 90e0f829f54a0326e7c64ddc55e097088c29ce61 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Fri, 5 Aug 2016 15:15:45 +0100
Subject: [PATCH 116/378] removed old grunt tasks, moved to
sharelatex/sharelatex
---
services/web/Gruntfile.coffee | 54 -----------------------------------
1 file changed, 54 deletions(-)
diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee
index 7c966260e8..63d0eb6f79 100644
--- a/services/web/Gruntfile.coffee
+++ b/services/web/Gruntfile.coffee
@@ -386,57 +386,3 @@ module.exports = (grunt) ->
grunt.registerTask 'version', "Write the version number into sentry.jade", ['git-rev-parse', 'sed']
-
- grunt.registerTask 'create-admin-user', "Create a user with the given email address and make them an admin. Update in place if the user already exists", () ->
- done = @async()
- email = grunt.option("email")
- if !email?
- console.error "Usage: grunt create-admin-user --email joe@example.com"
- process.exit(1)
-
- settings = require "settings-sharelatex"
- UserRegistrationHandler = require "./app/js/Features/User/UserRegistrationHandler"
- OneTimeTokenHandler = require "./app/js/Features/Security/OneTimeTokenHandler"
- UserRegistrationHandler.registerNewUser {
- email: email
- password: require("crypto").randomBytes(32).toString("hex")
- }, (error, user) ->
- if error? and error?.message != "EmailAlreadyRegistered"
- throw error
- user.isAdmin = true
- user.save (error) ->
- throw error if error?
- ONE_WEEK = 7 * 24 * 60 * 60 # seconds
- OneTimeTokenHandler.getNewToken user._id, { expiresIn: ONE_WEEK }, (err, token)->
- return next(err) if err?
-
- console.log ""
- console.log """
- Successfully created #{email} as an admin user.
-
- Please visit the following URL to set a password for #{email} and log in:
-
- #{settings.siteUrl}/user/password/set?passwordResetToken=#{token}
-
- """
- done()
-
- grunt.registerTask 'delete-user', "deletes a user and all their data", () ->
- done = @async()
- email = grunt.option("email")
- if !email?
- console.error "Usage: grunt delete-user --email joe@example.com"
- process.exit(1)
- settings = require "settings-sharelatex"
- UserGetter = require "./app/js/Features/User/UserGetter"
- UserDeleter = require "./app/js/Features/User/UserDeleter"
- UserGetter.getUser email:email, (error, user) ->
- if error?
- throw error
- if !user?
- console.log("user #{email} not in database, potentially already deleted")
- return done()
- UserDeleter.deleteUser user._id, (err)->
- if err?
- throw err
- done()
\ No newline at end of file
From 93af1a70ac29cc69e3d4b0879908118423e2276d Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Fri, 5 Aug 2016 15:15:56 +0100
Subject: [PATCH 117/378] added notifications into settings.defaults
---
services/web/config/settings.defaults.coffee | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee
index f345d14129..a326c3b07d 100644
--- a/services/web/config/settings.defaults.coffee
+++ b/services/web/config/settings.defaults.coffee
@@ -106,8 +106,8 @@ module.exports = settings =
url: ""
# references:
# url: "http://localhost:3040"
- # notifications:
- # url: "http://localhost:3042"
+ notifications:
+ url: "http://localhost:3042"
templates:
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
From 9b46c1b1f76b0968289391063adbee1c6c05f122 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 5 Aug 2016 16:11:03 +0100
Subject: [PATCH 118/378] WIP: notification when user is sent an invite
---
.../CollaboratorsInviteController.coffee | 22 ++++++++++++++++++-
.../CollaboratorsInviteHandler.coffee | 1 +
.../Notifications/NotificationsBuilder.coffee | 17 ++++++++++++--
3 files changed, 37 insertions(+), 3 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index c149998eef..cf1964600d 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -6,6 +6,7 @@ CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
logger = require('logger-sharelatex')
EmailHelper = require "../Helpers/EmailHelper"
EditorRealTimeController = require("../Editor/EditorRealTimeController")
+NotificationsBuilder = require("../Notifications/NotificationsBuilder")
module.exports = CollaboratorsInviteController =
@@ -19,10 +20,27 @@ module.exports = CollaboratorsInviteController =
return next(err)
res.json({invites: invites})
+ _trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) ->
+ email = invite.email
+ UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) ->
+ if err?
+ logger.err {projectId, email}, "error checking if user exists"
+ return next(err)
+ if existingUser
+ ProjectGetter.getProject projectId, (err, project) ->
+ if err?
+ logger.err {projectId, email}, "error getting project"
+ return next(err)
+ if !project
+ logger.log {projectId}, "no project found while sending notification, returning"
+ return callback()
+ NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback)
+
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
email = req.body.email
- sendingUserId = req.session?.user?._id
+ sendingUser = req.session.user
+ sendingUserId = sendingUser._id
logger.log {projectId, email, sendingUserId}, "inviting to project"
LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
return next(error) if error?
@@ -40,6 +58,8 @@ module.exports = CollaboratorsInviteController =
return next(err)
logger.log {projectId, email, sendingUserId}, "invite created"
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true}
+ # async check if email is for an existing user, send a notification
+ CollaboratorsInviteController._trySendInviteNotification(projectId, sendingUser, invite, ()->)
return res.json {invite: invite}
revokeInvite: (req, res, next) ->
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 83e3c2bb18..681d400205 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -7,6 +7,7 @@ PrivilegeLevels = require "../Authorization/PrivilegeLevels"
Errors = require "../Errors/Errors"
Crypto = require 'crypto'
+
module.exports = CollaboratorsInviteHandler =
getAllInvites: (projectId, callback=(err, invites)->) ->
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
index 9f960b1d15..da1a6c4957 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
@@ -1,12 +1,12 @@
logger = require("logger-sharelatex")
NotificationsHandler = require("./NotificationsHandler")
-module.exports =
+module.exports =
groupPlan: (user, licence)->
key : "join-sub-#{licence.subscription_id}"
create: (callback = ->)->
- messageOpts =
+ messageOpts =
groupName: licence.name
subscription_id: licence.subscription_id
logger.log user_id:user._id, key:key, "creating notification key for user"
@@ -14,3 +14,16 @@ module.exports =
read: (callback = ->)->
NotificationsHandler.markAsReadWithKey user._id, @key, callback
+
+ projectInvite: (invite, project, sendingUser, user) ->
+ key: "project-invite-#{invite._id}"
+ create: (callback=()->) ->
+ messageOpts =
+ userName: sendingUser.first_name
+ projectName: project.name
+ projectId: project._id.toString()
+ token: invite.token
+ logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user"
+ NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, callback
+ read: (callback=()->) ->
+ NotificationsHandler.markAsReadWithKey user._id, @key, callback
From e0444cfc62ac800c4a7f056c80c774cd0ec7cdac Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 5 Aug 2016 16:41:11 +0100
Subject: [PATCH 119/378] Make notification column layout explicit.
---
.../web/app/views/project/list/notifications.jade | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/services/web/app/views/project/list/notifications.jade b/services/web/app/views/project/list/notifications.jade
index 29f371b724..dd6886fba6 100644
--- a/services/web/app/views/project/list/notifications.jade
+++ b/services/web/app/views/project/list/notifications.jade
@@ -9,7 +9,10 @@ span(ng-controller="NotificationsController").userNotifications
.row(ng-hide="unreadNotification.hide")
.col-xs-12
.alert.alert-info
- span(ng-bind-html="unreadNotification.html")
- button(ng-click="dismiss(unreadNotification)").close.pull-right
- span(aria-hidden="true") ×
- span.sr-only #{translate("close")}
+ .row
+ .col-xs-11
+ span(ng-bind-html="unreadNotification.html")
+ .col-xs-1
+ button(ng-click="dismiss(unreadNotification)").close.pull-right
+ span(aria-hidden="true") ×
+ span.sr-only #{translate("close")}
From 110082390efbec3ce4a406fae5ba557765e29dea Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 8 Aug 2016 10:34:54 +0100
Subject: [PATCH 120/378] Test the _trySendInviteNotfification helper
---
.../CollaboratorsInviteController.coffee | 22 +--
.../CollaboratorsInviteControllerTests.coffee | 126 +++++++++++++++++-
2 files changed, 134 insertions(+), 14 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index cf1964600d..7fae2ff104 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -25,16 +25,18 @@ module.exports = CollaboratorsInviteController =
UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) ->
if err?
logger.err {projectId, email}, "error checking if user exists"
- return next(err)
- if existingUser
- ProjectGetter.getProject projectId, (err, project) ->
- if err?
- logger.err {projectId, email}, "error getting project"
- return next(err)
- if !project
- logger.log {projectId}, "no project found while sending notification, returning"
- return callback()
- NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback)
+ return callback(err)
+ if !existingUser
+ logger.log {projectId, email}, "no existing user found, returning"
+ return callback(null)
+ ProjectGetter.getProject projectId, (err, project) ->
+ if err?
+ logger.err {projectId, email}, "error getting project"
+ return callback(err)
+ if !project
+ logger.log {projectId}, "no project found while sending notification, returning"
+ return callback(null)
+ NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback)
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 680f7da8ad..50d3b33ed1 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -11,10 +11,6 @@ ObjectId = require("mongojs").ObjectId
describe "CollaboratorsInviteController", ->
beforeEach ->
- @Project = class Project
- constructor: () ->
- this
- @findOne: sinon.stub()
@CollaboratorsInviteController = SandboxedModule.require modulePath, requires:
"../Project/ProjectGetter": @ProjectGetter = {}
'../Subscription/LimitationsManager' : @LimitationsManager = {}
@@ -23,12 +19,130 @@ describe "CollaboratorsInviteController", ->
"./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
"../Editor/EditorRealTimeController": @EditorRealTimeController = {emitToRoom: sinon.stub()}
+ "../Notifications/NotificationsBuilder": @NotificationsBuilder = {}
@res = new MockResponse()
@req = new MockRequest()
@project_id = "project-id-123"
@callback = sinon.stub()
+ describe "_trySendInviteNotification", ->
+
+ beforeEach ->
+ @invite =
+ _id: ObjectId(),
+ token: "some_token",
+ sendingUserId: ObjectId(),
+ projectId: @project_id,
+ targetEmail: 'user@example.com'
+ createdAt: new Date(),
+ @sendingUser =
+ _id: ObjectId()
+ first_name: "jim"
+ @existingUser = {_id: ObjectId()}
+ @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @existingUser)
+ @fakeProject =
+ _id: @project_id
+ name: "some project"
+ @ProjectGetter.getProject = sinon.stub().callsArgWith(1, null, @fakeProject)
+ @notification = {create: sinon.stub()}
+ @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification)
+ @call = (callback) =>
+ @CollaboratorsInviteController._trySendInviteNotification @project_id, @sendingUser, @invite, callback
+
+ describe 'when the user exists', ->
+
+ beforeEach ->
+
+ it 'should not produce an error', ->
+ @call (err) =>
+ expect(err).to.be.oneOf [null, undefined]
+
+ it 'should call getUser', ->
+ @call (err) =>
+ @UserGetter.getUser.callCount.should.equal 1
+ @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
+
+ it 'should call getProject', ->
+ @call (err) =>
+ @ProjectGetter.getProject.callCount.should.equal 1
+ @ProjectGetter.getProject.calledWith(@project_id).should.equal true
+
+ it 'should call NotificationsBuilder.projectInvite.create', ->
+ @call (err) =>
+ @NotificationsBuilder.projectInvite.callCount.should.equal 1
+ @notification.create.callCount.should.equal 1
+
+ describe 'when getProject produces an error', ->
+
+ beforeEach ->
+ @ProjectGetter.getProject.callsArgWith(1, new Error('woops'))
+
+ it 'should produce an error', ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+
+ it 'should not call NotificationsBuilder.projectInvite.create', ->
+ @call (err) =>
+ @NotificationsBuilder.projectInvite.callCount.should.equal 0
+ @notification.create.callCount.should.equal 0
+
+ describe 'when projectInvite.create produces an error', ->
+
+ beforeEach ->
+ @notification.create.callsArgWith(0, new Error('woops'))
+
+ it 'should produce an error', ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+
+ describe 'when the user does not exist', ->
+
+ beforeEach ->
+ @UserGetter.getUser = sinon.stub().callsArgWith(2, null, null)
+
+ it 'should not produce an error', ->
+ @call (err) =>
+ expect(err).to.be.oneOf [null, undefined]
+
+ it 'should call getUser', ->
+ @call (err) =>
+ @UserGetter.getUser.callCount.should.equal 1
+ @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
+
+ it 'should not call getProject', ->
+ @call (err) =>
+ @ProjectGetter.getProject.callCount.should.equal 0
+
+ it 'should not call NotificationsBuilder.projectInvite.create', ->
+ @call (err) =>
+ @NotificationsBuilder.projectInvite.callCount.should.equal 0
+ @notification.create.callCount.should.equal 0
+
+ describe 'when the getUser produces an error', ->
+
+ beforeEach ->
+ @UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops'))
+
+ it 'should produce an error', ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+
+ it 'should call getUser', ->
+ @call (err) =>
+ @UserGetter.getUser.callCount.should.equal 1
+ @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
+
+ it 'should not call getProject', ->
+ @call (err) =>
+ @ProjectGetter.getProject.callCount.should.equal 0
+
+ it 'should not call NotificationsBuilder.projectInvite.create', ->
+ @call (err) =>
+ @NotificationsBuilder.projectInvite.callCount.should.equal 0
+ @notification.create.callCount.should.equal 0
+
+
describe 'getAllInvites', ->
@@ -92,6 +206,7 @@ describe "CollaboratorsInviteController", ->
}
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
@CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite)
+ @CollaboratorsInviteController._trySendInviteNotification = sinon.stub()
@callback = sinon.stub()
@next = sinon.stub()
@@ -117,6 +232,9 @@ describe "CollaboratorsInviteController", ->
@EditorRealTimeController.emitToRoom.callCount.should.equal 1
@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true
+ it 'should call _trySendInviteNotification', ->
+ @CollaboratorsInviteController._trySendInviteNotification.callCount.should.equal 1
+
describe 'when the user is not allowed to add more collaborators', ->
beforeEach ->
From 0e0ccb41ff33b328ef4f2ca239f386e6aba80a48 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 8 Aug 2016 13:57:33 +0100
Subject: [PATCH 121/378] cancel notification when accepting invite
---
.../CollaboratorsInviteController.coffee | 4 +
.../CollaboratorsInviteControllerTests.coffee | 82 +++++++++++++++----
2 files changed, 68 insertions(+), 18 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 7fae2ff104..256697ecbd 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -38,6 +38,9 @@ module.exports = CollaboratorsInviteController =
return callback(null)
NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback)
+ _tryCancelInviteNotification: (inviteId, currentUser, callback=()->) ->
+ NotificationsBuilder.projectInvite({_id: inviteId}, null, null, currentUser).read(callback)
+
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
email = req.body.email
@@ -139,4 +142,5 @@ module.exports = CollaboratorsInviteController =
logger.err {projectId, inviteId}, "error accepting invite by token"
return next(err)
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
+ CollaboratorsInviteController._tryCancelInviteNotification inviteId, currentUser, () ->
res.redirect "/project/#{projectId}"
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 50d3b33ed1..dc15fbc65f 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -26,6 +26,35 @@ describe "CollaboratorsInviteController", ->
@project_id = "project-id-123"
@callback = sinon.stub()
+ describe '_tryCancelInviteNotification', ->
+ beforeEach ->
+ @inviteId = ObjectId()
+ @currentUser = {_id: ObjectId()}
+ @notification = {read: sinon.stub().callsArgWith(0, null)}
+ @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification)
+ @call = (callback) =>
+ @CollaboratorsInviteController._tryCancelInviteNotification @inviteId, @currentUser, callback
+
+ it 'should not produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should call notification.read', (done) ->
+ @call (err) =>
+ @notification.read.callCount.should.equal 1
+ done()
+
+ describe 'when notification.read produces an error', ->
+ beforeEach ->
+ @notification = {read: sinon.stub().callsArgWith(0, new Error('woops'))}
+ @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification)
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ done()
+
describe "_trySendInviteNotification", ->
beforeEach ->
@@ -45,7 +74,7 @@ describe "CollaboratorsInviteController", ->
_id: @project_id
name: "some project"
@ProjectGetter.getProject = sinon.stub().callsArgWith(1, null, @fakeProject)
- @notification = {create: sinon.stub()}
+ @notification = {create: sinon.stub().callsArgWith(0, null)}
@NotificationsBuilder.projectInvite = sinon.stub().returns(@notification)
@call = (callback) =>
@CollaboratorsInviteController._trySendInviteNotification @project_id, @sendingUser, @invite, callback
@@ -54,95 +83,108 @@ describe "CollaboratorsInviteController", ->
beforeEach ->
- it 'should not produce an error', ->
+ it 'should not produce an error', (done) ->
@call (err) =>
expect(err).to.be.oneOf [null, undefined]
+ done()
- it 'should call getUser', ->
+ it 'should call getUser', (done) ->
@call (err) =>
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
+ done()
- it 'should call getProject', ->
+ it 'should call getProject', (done) ->
@call (err) =>
@ProjectGetter.getProject.callCount.should.equal 1
@ProjectGetter.getProject.calledWith(@project_id).should.equal true
+ done()
- it 'should call NotificationsBuilder.projectInvite.create', ->
+ it 'should call NotificationsBuilder.projectInvite.create', (done) ->
@call (err) =>
@NotificationsBuilder.projectInvite.callCount.should.equal 1
@notification.create.callCount.should.equal 1
+ done()
describe 'when getProject produces an error', ->
beforeEach ->
@ProjectGetter.getProject.callsArgWith(1, new Error('woops'))
- it 'should produce an error', ->
+ it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.be.instanceof Error
+ done()
- it 'should not call NotificationsBuilder.projectInvite.create', ->
+ it 'should not call NotificationsBuilder.projectInvite.create', (done) ->
@call (err) =>
@NotificationsBuilder.projectInvite.callCount.should.equal 0
@notification.create.callCount.should.equal 0
+ done()
describe 'when projectInvite.create produces an error', ->
beforeEach ->
@notification.create.callsArgWith(0, new Error('woops'))
- it 'should produce an error', ->
+ it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.be.instanceof Error
+ done()
describe 'when the user does not exist', ->
beforeEach ->
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, null)
- it 'should not produce an error', ->
+ it 'should not produce an error', (done) ->
@call (err) =>
expect(err).to.be.oneOf [null, undefined]
+ done()
- it 'should call getUser', ->
+ it 'should call getUser', (done) ->
@call (err) =>
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
+ done()
- it 'should not call getProject', ->
+ it 'should not call getProject', (done) ->
@call (err) =>
@ProjectGetter.getProject.callCount.should.equal 0
+ done()
- it 'should not call NotificationsBuilder.projectInvite.create', ->
+ it 'should not call NotificationsBuilder.projectInvite.create', (done) ->
@call (err) =>
@NotificationsBuilder.projectInvite.callCount.should.equal 0
@notification.create.callCount.should.equal 0
+ done()
describe 'when the getUser produces an error', ->
beforeEach ->
@UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops'))
- it 'should produce an error', ->
+ it 'should produce an error', (done) ->
@call (err) =>
expect(err).to.be.instanceof Error
+ done()
- it 'should call getUser', ->
+ it 'should call getUser', (done) ->
@call (err) =>
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
+ done()
- it 'should not call getProject', ->
+ it 'should not call getProject', (done) ->
@call (err) =>
@ProjectGetter.getProject.callCount.should.equal 0
+ done()
- it 'should not call NotificationsBuilder.projectInvite.create', ->
+ it 'should not call NotificationsBuilder.projectInvite.create', (done) ->
@call (err) =>
@NotificationsBuilder.projectInvite.callCount.should.equal 0
@notification.create.callCount.should.equal 0
-
-
+ done()
describe 'getAllInvites', ->
@@ -656,6 +698,7 @@ describe "CollaboratorsInviteController", ->
@res.render = sinon.stub()
@res.redirect = sinon.stub()
@CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, null)
+ @CollaboratorsInviteController._tryCancelInviteNotification = sinon.stub()
@callback = sinon.stub()
@next = sinon.stub()
@@ -675,6 +718,9 @@ describe "CollaboratorsInviteController", ->
@EditorRealTimeController.emitToRoom.callCount.should.equal 1
@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true
+ it 'should call _tryCancelInviteNotification', ->
+ @CollaboratorsInviteController._tryCancelInviteNotification.callCount.should.equal 1
+
describe 'when revokeInvite produces an error', ->
beforeEach ->
From 5351e79c7a80eabf648f2213a89a704b6c98d4fc Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 10 Aug 2016 14:39:27 +0100
Subject: [PATCH 122/378] Test creating, listing and revoking invites as owner
---
.../coffee/ProjectInviteTests.coffee | 66 +++++++++++++++++++
1 file changed, 66 insertions(+)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index 74bced1c5a..d3c6e9706d 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -87,6 +87,14 @@ tryLoginUser = (user, redir, callback=(err, response, body)->) ->
redir: redir
}, callback
+tryGetInviteList = (user, projectId, callback=(err, response, body)->) ->
+ user.getCsrfToken (error) =>
+ return callback(error) if error?
+ user.request.get {
+ url: "/project/#{projectId}/invites"
+ json: true
+ }, callback
+
# Expectations
expectProjectAccess = (user, projectId, callback=(err,result)->) ->
@@ -169,6 +177,14 @@ expectAcceptInviteAndRedirect = (user, invite, callback=(err,result)->) ->
expect(response.headers.location).to.equal "/project/#{invite.projectId}"
callback()
+expectInviteListCount = (user, projectId, count, callback=(err)->) ->
+ tryGetInviteList user, projectId, (err, response, body) ->
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 200
+ expect(body).to.have.all.keys ['invites']
+ expect(body.invites.length).to.equal count
+ callback()
+
describe "ProjectInviteTests", ->
before (done) ->
@@ -183,6 +199,56 @@ describe "ProjectInviteTests", ->
(cb) => @sendingUser.login cb
], done
+ describe 'creating invites', ->
+
+ beforeEach (done) ->
+ @projectName = "wat"
+ @projectId = null
+ @fakeProject = null
+ done()
+
+ afterEach ->
+
+ describe 'creating two invites', ->
+
+ beforeEach (done) ->
+ Async.series [
+ (cb) =>
+ createProject @sendingUser, @projectName, (err, projectId, project) =>
+ @projectId = projectId
+ @fakeProject = project
+ cb()
+ ], done
+
+ afterEach (done) ->
+ Async.series [
+ (cb) => @sendingUser.deleteProject(@projectId, cb)
+ (cb) => @sendingUser.deleteProject(@projectId, cb)
+ ], done
+
+ it 'should allow the project owner to create and remove invites', (done) ->
+ @invite = null
+ Async.series [
+ (cb) => expectProjectAccess @sendingUser, @projectId, cb
+ (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb
+ # create invite, check invite list count
+ (cb) => createInvite @sendingUser, @projectId, @email, (err, invite) =>
+ return cb(err) if err
+ @invite = invite
+ cb()
+ (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb
+ (cb) => revokeInvite @sendingUser, @projectId, @invite._id, cb
+ (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb
+ # and a second time
+ (cb) => createInvite @sendingUser, @projectId, @email, (err, invite) =>
+ return cb(err) if err
+ @invite = invite
+ cb()
+ (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb
+ (cb) => revokeInvite @sendingUser, @projectId, @invite._id, cb
+ (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb
+ ], done
+
describe 'clicking the invite link', ->
beforeEach (done) ->
From 3cec6affabfa14d8326c9b836f2c2681cb81f920 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 10 Aug 2016 15:24:09 +0100
Subject: [PATCH 123/378] Test creating two invites at once
---
.../coffee/ProjectInviteTests.coffee | 27 +++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index d3c6e9706d..c2614b5eb0 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -249,6 +249,33 @@ describe "ProjectInviteTests", ->
(cb) => expectInviteListCount @sendingUser, @projectId, 0, cb
], done
+ it 'should allow the project owner to many invites at once', (done) ->
+ @inviteOne = null
+ @inviteTwo = null
+ Async.series [
+ (cb) => expectProjectAccess @sendingUser, @projectId, cb
+ (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb
+ # create first invite
+ (cb) => createInvite @sendingUser, @projectId, @email, (err, invite) =>
+ return cb(err) if err
+ @inviteOne = invite
+ cb()
+ (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb
+ # and a second
+ (cb) => createInvite @sendingUser, @projectId, @email, (err, invite) =>
+ return cb(err) if err
+ @inviteTwo = invite
+ cb()
+ # should have two
+ (cb) => expectInviteListCount @sendingUser, @projectId, 2, cb
+ # revoke first
+ (cb) => revokeInvite @sendingUser, @projectId, @inviteOne._id, cb
+ (cb) => expectInviteListCount @sendingUser, @projectId, 1, cb
+ # revoke second
+ (cb) => revokeInvite @sendingUser, @projectId, @inviteTwo._id, cb
+ (cb) => expectInviteListCount @sendingUser, @projectId, 0, cb
+ ], done
+
describe 'clicking the invite link', ->
beforeEach (done) ->
From 93cd51121107fe778030b5fd342740240c2dd72e Mon Sep 17 00:00:00 2001
From: James Allen
Date: Wed, 10 Aug 2016 16:42:56 +0100
Subject: [PATCH 124/378] Send events to custom DB backend
---
.../Analytics/AnalyticsController.coffee | 7 +++
.../Analytics/AnalyticsManager.coffee | 45 +++++++++++++++++++
services/web/app/coffee/router.coffee | 6 ++-
services/web/package.json | 3 ++
services/web/public/coffee/main/event.coffee | 17 +++++++
5 files changed, 76 insertions(+), 2 deletions(-)
create mode 100644 services/web/app/coffee/Features/Analytics/AnalyticsController.coffee
create mode 100644 services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee
diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee
new file mode 100644
index 0000000000..a49a419e07
--- /dev/null
+++ b/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee
@@ -0,0 +1,7 @@
+AnalyticsManager = require "./AnalyticsManager"
+
+module.exports = AnalyticsController =
+ recordEvent: (req, res, next) ->
+ AnalyticsManager.recordEvent req.session?.user?._id, req.params.event, req.body, (error) ->
+ return next(error) if error?
+ res.send 204
\ No newline at end of file
diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee
new file mode 100644
index 0000000000..abdd9ebef8
--- /dev/null
+++ b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee
@@ -0,0 +1,45 @@
+Settings = require "settings-sharelatex"
+logger = require "logger-sharelatex"
+_ = require "underscore"
+
+if !Settings.analytics?.postgres?
+ module.exports =
+ recordEvent: (user_id, event, metadata, callback = () ->) ->
+ logger.log {user_id, event, metadata}, "no event tracking configured, logging event"
+ callback()
+else
+ Sequelize = require "sequelize"
+ options = _.extend {logging:false}, Settings.analytics.postgres
+
+ sequelize = new Sequelize(
+ Settings.analytics.postgres.database,
+ Settings.analytics.postgres.username,
+ Settings.analytics.postgres.password,
+ options
+ )
+
+ Event = sequelize.define("Event", {
+ user_id: Sequelize.STRING,
+ event: Sequelize.STRING,
+ metadata: Sequelize.STRING
+ })
+
+ module.exports =
+ recordEvent: (user_id, event, metadata = {}, callback = (error) ->) ->
+ if typeof(metadata) != "string"
+ metadata = JSON.stringify(metadata)
+ if user_id? and typeof(user_id) != "string"
+ user_id = user_id.toString()
+ if user_id == Settings.smokeTest?.userId
+ # Don't record smoke tests analytics
+ return callback()
+ Event
+ .create({ user_id, event, metadata })
+ .then(
+ (result) -> callback(),
+ (error) ->
+ logger.err {err: error, user_id, event, metadata}, "error recording analytics event"
+ callback(error)
+ )
+
+ sync: () -> sequelize.sync()
\ No newline at end of file
diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee
index d6deffe755..5d07dc9e82 100644
--- a/services/web/app/coffee/router.coffee
+++ b/services/web/app/coffee/router.coffee
@@ -38,6 +38,7 @@ ContactRouter = require("./Features/Contacts/ContactRouter")
ReferencesController = require('./Features/References/ReferencesController')
AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear')
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
+AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
logger = require("logger-sharelatex")
_ = require("underscore")
@@ -68,7 +69,8 @@ module.exports = class Router
StaticPagesRouter.apply(webRouter, apiRouter)
RealTimeProxyRouter.apply(webRouter, apiRouter)
ContactRouter.apply(webRouter, apiRouter)
-
+ AnalyticsRouter.apply(webRouter, apiRouter)
+
Modules.applyRouter(webRouter, apiRouter)
@@ -280,4 +282,4 @@ module.exports = class Router
metrics.inc("client-side-error")
res.sendStatus(204)
- webRouter.get '*', ErrorController.notFound
\ No newline at end of file
+ webRouter.get '*', ErrorController.notFound
diff --git a/services/web/package.json b/services/web/package.json
index 71fba8f719..83381005f0 100644
--- a/services/web/package.json
+++ b/services/web/package.json
@@ -46,6 +46,8 @@
"nodemailer-sendgrid-transport": "^0.2.0",
"nodemailer-ses-transport": "^1.3.0",
"optimist": "0.6.1",
+ "pg": "^6.0.3",
+ "pg-hstore": "^2.3.2",
"redback": "0.4.0",
"redis": "0.10.1",
"redis-sharelatex": "0.0.9",
@@ -53,6 +55,7 @@
"requests": "^0.1.7",
"rimraf": "2.2.6",
"sanitizer": "0.1.1",
+ "sequelize": "^3.2.0",
"settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
"sixpack-client": "^1.0.0",
"temp": "^0.8.3",
diff --git a/services/web/public/coffee/main/event.coffee b/services/web/public/coffee/main/event.coffee
index 371bd1004e..48ed802442 100644
--- a/services/web/public/coffee/main/event.coffee
+++ b/services/web/public/coffee/main/event.coffee
@@ -3,6 +3,11 @@ define [
"modules/localStorage"
], (App) ->
CACHE_KEY = "countlyEvents"
+ send = (category, action, attributes = {})->
+ ga('send', 'event', category, action)
+ event_name = "#{action}-#{category}"
+ Intercom?("trackEvent", event_name, attributes)
+
App.factory "event_tracking", (localStorage) ->
_getEventCache = () ->
@@ -41,6 +46,18 @@ define [
if ! _eventInCache(key)
_addEventToCache(key)
@sendCountly key, segmentation
+
+ send: (category, action, attributes = {})->
+ event_name = "#{action}-#{category}"
+ $.ajax {
+ url: "/event/#{event_name}",
+ method: "POST",
+ data: attributes,
+ dataType: "json",
+ headers: {
+ "X-CSRF-Token": window.csrfToken
+ }
+ }
}
# App.directive "countlyTrack", () ->
From 056bb6b0f49e98a5d44e05e14944d8fdfbbb44f7 Mon Sep 17 00:00:00 2001
From: James Allen
Date: Wed, 10 Aug 2016 17:17:59 +0100
Subject: [PATCH 125/378] Use a JSON column for metadata
---
.../web/app/coffee/Features/Analytics/AnalyticsManager.coffee | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee
index abdd9ebef8..a902005ff0 100644
--- a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee
+++ b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee
@@ -21,13 +21,11 @@ else
Event = sequelize.define("Event", {
user_id: Sequelize.STRING,
event: Sequelize.STRING,
- metadata: Sequelize.STRING
+ metadata: Sequelize.JSON
})
module.exports =
recordEvent: (user_id, event, metadata = {}, callback = (error) ->) ->
- if typeof(metadata) != "string"
- metadata = JSON.stringify(metadata)
if user_id? and typeof(user_id) != "string"
user_id = user_id.toString()
if user_id == Settings.smokeTest?.userId
From 4886e8ba0ec9bdcb66219cbd21d1c5a77e1937f0 Mon Sep 17 00:00:00 2001
From: James Allen
Date: Wed, 10 Aug 2016 17:22:35 +0100
Subject: [PATCH 126/378] Rename metadata -> segmentation in Events table to
play well with metabase
---
.../Features/Analytics/AnalyticsManager.coffee | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee
index a902005ff0..df1ba0be7c 100644
--- a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee
+++ b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee
@@ -4,8 +4,8 @@ _ = require "underscore"
if !Settings.analytics?.postgres?
module.exports =
- recordEvent: (user_id, event, metadata, callback = () ->) ->
- logger.log {user_id, event, metadata}, "no event tracking configured, logging event"
+ recordEvent: (user_id, event, segmentation, callback = () ->) ->
+ logger.log {user_id, event, segmentation}, "no event tracking configured, logging event"
callback()
else
Sequelize = require "sequelize"
@@ -21,22 +21,22 @@ else
Event = sequelize.define("Event", {
user_id: Sequelize.STRING,
event: Sequelize.STRING,
- metadata: Sequelize.JSON
+ segmentation: Sequelize.JSON
})
module.exports =
- recordEvent: (user_id, event, metadata = {}, callback = (error) ->) ->
+ recordEvent: (user_id, event, segmentation = {}, callback = (error) ->) ->
if user_id? and typeof(user_id) != "string"
user_id = user_id.toString()
if user_id == Settings.smokeTest?.userId
# Don't record smoke tests analytics
return callback()
Event
- .create({ user_id, event, metadata })
+ .create({ user_id, event, segmentation })
.then(
(result) -> callback(),
(error) ->
- logger.err {err: error, user_id, event, metadata}, "error recording analytics event"
+ logger.err {err: error, user_id, event, segmentation}, "error recording analytics event"
callback(error)
)
From 0ec8e22ccf60f92519d9e112169b0f695e201104 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Wed, 10 Aug 2016 17:28:13 +0100
Subject: [PATCH 127/378] Send events to Postgre.
---
services/web/public/coffee/main/event.coffee | 37 ++++++++------------
1 file changed, 15 insertions(+), 22 deletions(-)
diff --git a/services/web/public/coffee/main/event.coffee b/services/web/public/coffee/main/event.coffee
index 48ed802442..b2847bc5a0 100644
--- a/services/web/public/coffee/main/event.coffee
+++ b/services/web/public/coffee/main/event.coffee
@@ -2,14 +2,14 @@ define [
"base"
"modules/localStorage"
], (App) ->
- CACHE_KEY = "countlyEvents"
+ CACHE_KEY = "mbEvents"
+
send = (category, action, attributes = {})->
ga('send', 'event', category, action)
event_name = "#{action}-#{category}"
Intercom?("trackEvent", event_name, attributes)
-
- App.factory "event_tracking", (localStorage) ->
+ App.factory "event_tracking", ($http, localStorage) ->
_getEventCache = () ->
eventCache = localStorage CACHE_KEY
@@ -34,30 +34,23 @@ define [
send: (category, action, label, value)->
ga('send', 'event', category, action, label, value)
- sendCountly: (key, segmentation) ->
- eventData = { key }
- eventData.segmentation = segmentation if segmentation?
- Countly?.q.push([ "add_event", eventData ])
-
- sendCountlySampled: (key, segmentation) ->
- @sendCountly key, segmentation if Math.random() < .01
-
- sendCountlyOnce: (key, segmentation) ->
- if ! _eventInCache(key)
- _addEventToCache(key)
- @sendCountly key, segmentation
-
- send: (category, action, attributes = {})->
- event_name = "#{action}-#{category}"
- $.ajax {
- url: "/event/#{event_name}",
+ sendMB: (key, segmentation = {}) ->
+ $http {
+ url: "/event/#{key}",
method: "POST",
- data: attributes,
- dataType: "json",
+ data: segmentation
headers: {
"X-CSRF-Token": window.csrfToken
}
}
+
+ sendMBSampled: (key, segmentation) ->
+ @sendMB key, segmentation if Math.random() < .01
+
+ sendMBOnce: (key, segmentation) ->
+ if ! _eventInCache(key)
+ _addEventToCache(key)
+ @sendMB key, segmentation
}
# App.directive "countlyTrack", () ->
From 6a3372fcbf4819dca9e26ba49a82d2e295698a91 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Wed, 10 Aug 2016 17:29:43 +0100
Subject: [PATCH 128/378] Use new event tracking methods.
---
services/web/public/coffee/ide.coffee | 8 ++++----
.../hotkeys/controllers/HotkeysController.coffee | 2 +-
.../ide/pdf/controllers/PdfController.coffee | 14 +++++++-------
.../coffee/ide/settings/services/settings.coffee | 6 +++---
.../ide/share/controllers/ShareController.coffee | 2 +-
.../controllers/TrackChangesDiffController.coffee | 6 +++---
.../controllers/TrackChangesListController.coffee | 2 +-
.../web/public/coffee/main/account-upgrade.coffee | 2 +-
services/web/public/coffee/main/contact-us.coffee | 4 ++--
.../web/public/coffee/main/new-subscription.coffee | 6 +++---
10 files changed, 26 insertions(+), 26 deletions(-)
diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee
index f18260d7d4..be82aa72a8 100644
--- a/services/web/public/coffee/ide.coffee
+++ b/services/web/public/coffee/ide.coffee
@@ -74,16 +74,16 @@ define [
# Tracking code.
$scope.$watch "ui.view", (newView, oldView) ->
if newView? and newView != "editor" and newView != "pdf"
- event_tracking.sendCountlyOnce "ide-open-view-#{ newView }-once"
+ event_tracking.sendMBOnce "ide-open-view-#{ newView }-once"
$scope.$watch "ui.chatOpen", (isOpen) ->
- event_tracking.sendCountlyOnce "ide-open-chat-once" if isOpen
+ event_tracking.sendMBOnce "ide-open-chat-once" if isOpen
$scope.$watch "ui.leftMenuShown", (isOpen) ->
- event_tracking.sendCountlyOnce "ide-open-left-menu-once" if isOpen
+ event_tracking.sendMBOnce "ide-open-left-menu-once" if isOpen
$scope.trackHover = (feature) ->
- event_tracking.sendCountlyOnce "ide-hover-#{feature}-once"
+ event_tracking.sendMBOnce "ide-hover-#{feature}-once"
# End of tracking code.
window._ide = ide
diff --git a/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee b/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee
index 36eaa5d555..18d075c094 100644
--- a/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee
+++ b/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee
@@ -4,7 +4,7 @@ define [
], (App) ->
App.controller "HotkeysController", ($scope, $modal, event_tracking) ->
$scope.openHotkeysModal = ->
- event_tracking.sendCountly "ide-open-hotkeys-modal"
+ event_tracking.sendMB "ide-open-hotkeys-modal"
$modal.open {
templateUrl: "hotkeysModalTemplate"
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index 780fda5aa3..fd133e1bc3 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -36,11 +36,11 @@ define [
$scope.logHintsNegFeedbackValues = logHintsFeedback.feedbackOpts
$scope.trackLogHintsLearnMore = () ->
- event_tracking.sendCountly "logs-hints-learn-more"
+ event_tracking.sendMB "logs-hints-learn-more"
trackLogHintsFeedback = (isPositive, hintId) ->
event_tracking.send "log-hints", (if isPositive then "feedback-positive" else "feedback-negative"), hintId
- event_tracking.sendCountly (if isPositive then "log-hints-feedback-positive" else "log-hints-feedback-negative"), { hintId }
+ event_tracking.sendMB (if isPositive then "log-hints-feedback-positive" else "log-hints-feedback-negative"), { hintId }
$scope.trackLogHintsNegFeedbackDetails = (hintId, feedbackOpt, feedbackOtherVal) ->
logHintsFeedback.submitFeedback hintId, feedbackOpt, feedbackOtherVal
@@ -338,7 +338,7 @@ define [
$scope.recompile = (options = {}) ->
return if $scope.pdf.compiling
- event_tracking.sendCountlySampled "editor-recompile-sampled", options
+ event_tracking.sendMBSampled "editor-recompile-sampled", options
$scope.pdf.compiling = true
@@ -387,7 +387,7 @@ define [
$scope.toggleLogs = () ->
$scope.shouldShowLogs = !$scope.shouldShowLogs
- event_tracking.sendCountlyOnce "ide-open-logs-once" if $scope.shouldShowLogs
+ event_tracking.sendMBOnce "ide-open-logs-once" if $scope.shouldShowLogs
$scope.showPdf = () ->
$scope.pdf.view = "pdf"
@@ -395,7 +395,7 @@ define [
$scope.toggleRawLog = () ->
$scope.pdf.showRawLog = !$scope.pdf.showRawLog
- event_tracking.sendCountly "logs-view-raw" if $scope.pdf.showRawLog
+ event_tracking.sendMB "logs-view-raw" if $scope.pdf.showRawLog
$scope.openClearCacheModal = () ->
modalInstance = $modal.open(
@@ -430,7 +430,7 @@ define [
$scope.startFreeTrial = (source) ->
ga?('send', 'event', 'subscription-funnel', 'compile-timeout', source)
- event_tracking.sendCountly "subscription-start-trial", { source }
+ event_tracking.sendMB "subscription-start-trial", { source }
window.open("/user/subscription/new?planCode=student_free_trial_7_days")
$scope.startedFreeTrial = true
@@ -553,7 +553,7 @@ define [
App.controller "PdfLogEntryController", ["$scope", "ide", "event_tracking", ($scope, ide, event_tracking) ->
$scope.openInEditor = (entry) ->
- event_tracking.sendCountlyOnce "logs-jump-to-location-once"
+ event_tracking.sendMBOnce "logs-jump-to-location-once"
entity = ide.fileTreeManager.findEntityByPath(entry.file)
return if !entity? or entity.type != "doc"
if entry.line?
diff --git a/services/web/public/coffee/ide/settings/services/settings.coffee b/services/web/public/coffee/ide/settings/services/settings.coffee
index 14dfe92f56..04a9ccb5e3 100644
--- a/services/web/public/coffee/ide/settings/services/settings.coffee
+++ b/services/web/public/coffee/ide/settings/services/settings.coffee
@@ -8,7 +8,7 @@ define [
for key in Object.keys(data)
changedSetting = key
changedSettingVal = data[key]
- event_tracking.sendCountly "setting-changed", { changedSetting, changedSettingVal }
+ event_tracking.sendMB "setting-changed", { changedSetting, changedSettingVal }
# End of tracking code.
data._csrf = window.csrfToken
@@ -20,7 +20,7 @@ define [
for key in Object.keys(data)
changedSetting = key
changedSettingVal = data[key]
- event_tracking.sendCountly "project-setting-changed", { changedSetting, changedSettingVal}
+ event_tracking.sendMB "project-setting-changed", { changedSetting, changedSettingVal}
# End of tracking code.
data._csrf = window.csrfToken
@@ -32,7 +32,7 @@ define [
for key in Object.keys(data)
changedSetting = key
changedSettingVal = data[key]
- event_tracking.sendCountly "project-admin-setting-changed", { changedSetting, changedSettingVal }
+ event_tracking.sendMB "project-admin-setting-changed", { changedSetting, changedSettingVal }
# End of tracking code.
data._csrf = window.csrfToken
diff --git a/services/web/public/coffee/ide/share/controllers/ShareController.coffee b/services/web/public/coffee/ide/share/controllers/ShareController.coffee
index 2378391974..51a7043191 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareController.coffee
@@ -3,7 +3,7 @@ define [
], (App) ->
App.controller "ShareController", ["$scope", "$modal", "event_tracking", ($scope, $modal, event_tracking) ->
$scope.openShareProjectModal = () ->
- event_tracking.sendCountlyOnce "ide-open-share-modal-once"
+ event_tracking.sendMBOnce "ide-open-share-modal-once"
$modal.open(
templateUrl: "shareProjectModalTemplate"
diff --git a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee
index 77c2720a4f..a32acfb2b3 100644
--- a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee
+++ b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee
@@ -3,7 +3,7 @@ define [
], (App) ->
App.controller "TrackChangesDiffController", ($scope, $modal, ide, event_tracking) ->
$scope.restoreDeletedDoc = () ->
- event_tracking.sendCountly "track-changes-restore-deleted"
+ event_tracking.sendMB "track-changes-restore-deleted"
$scope.trackChanges.diff.restoreInProgress = true
ide.trackChangesManager
.restoreDeletedDoc(
@@ -15,7 +15,7 @@ define [
$scope.trackChanges.diff.restoreDeletedSuccess = true
$scope.openRestoreDiffModal = () ->
- event_tracking.sendCountly "track-changes-restore-modal"
+ event_tracking.sendMB "track-changes-restore-modal"
$modal.open {
templateUrl: "trackChangesRestoreDiffModalTemplate"
controller: "TrackChangesRestoreDiffModalController"
@@ -33,7 +33,7 @@ define [
$scope.diff = diff
$scope.restore = () ->
- event_tracking.sendCountly "track-changes-restored"
+ event_tracking.sendMB "track-changes-restored"
$scope.state.inflight = true
ide.trackChangesManager
.restoreDiff(diff)
diff --git a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee
index e0beb3ef4f..3511ddf6c1 100644
--- a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee
+++ b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee
@@ -92,7 +92,7 @@ define [
$scope.recalculateSelectedUpdates()
$scope.select = () ->
- event_tracking.sendCountly "track-changes-view-change"
+ event_tracking.sendMB "track-changes-view-change"
$scope.update.selectedTo = true
$scope.update.selectedFrom = true
diff --git a/services/web/public/coffee/main/account-upgrade.coffee b/services/web/public/coffee/main/account-upgrade.coffee
index 956c340d14..be842b6907 100644
--- a/services/web/public/coffee/main/account-upgrade.coffee
+++ b/services/web/public/coffee/main/account-upgrade.coffee
@@ -6,7 +6,7 @@ define [
$scope.buttonClass = "btn-primary"
$scope.startFreeTrial = (source, couponCode) ->
- event_tracking.sendCountly "subscription-start-trial", { source }
+ event_tracking.sendMB "subscription-start-trial", { source }
w = window.open()
sixpack.convert "track-changes-discount", ->
diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee
index 0f722df369..2f3a2c61e0 100644
--- a/services/web/public/coffee/main/contact-us.coffee
+++ b/services/web/public/coffee/main/contact-us.coffee
@@ -24,7 +24,7 @@ define [
url :"/learn/kb/#{page_underscored}"
name : hit._highlightResult.pageName.value
- event_tracking.sendCountly "contact-form-suggestions-shown" if results.hits.length
+ event_tracking.sendMB "contact-form-suggestions-shown" if results.hits.length
$scope.$applyAsync () ->
$scope.suggestions = suggestions
@@ -60,7 +60,7 @@ define [
$scope.suggestions = [];
$scope.clickSuggestionLink = (url) ->
- event_tracking.sendCountly "contact-form-suggestions-clicked", { url }
+ event_tracking.sendMB "contact-form-suggestions-clicked", { url }
$scope.close = () ->
$modalInstance.close()
diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee
index bdae2b219e..c153392c15 100644
--- a/services/web/public/coffee/main/new-subscription.coffee
+++ b/services/web/public/coffee/main/new-subscription.coffee
@@ -51,7 +51,7 @@ define [
.done()
pricing.on "change", =>
- event_tracking.sendCountly "subscription-form", { plan : pricing.items.plan.code }
+ event_tracking.sendMB "subscription-form", { plan : pricing.items.plan.code }
$scope.planName = pricing.items.plan.name
$scope.price = pricing.price
@@ -125,7 +125,7 @@ define [
state: $scope.data.state
postal_code: $scope.data.postal_code
- event_tracking.sendCountly "subscription-form-submitted", {
+ event_tracking.sendMB "subscription-form-submitted", {
currencyCode : postData.subscriptionDetails.currencyCode,
plan_code : postData.subscriptionDetails.plan_code,
coupon_code : postData.subscriptionDetails.coupon_code,
@@ -135,7 +135,7 @@ define [
$http.post("/user/subscription/create", postData)
.success (data, status, headers)->
sixpack.convert "in-editor-free-trial-plan", pricing.items.plan.code, (err)->
- event_tracking.sendCountly "subscription-submission-success"
+ event_tracking.sendMB "subscription-submission-success"
window.location.href = "/user/subscription/thank-you"
.error (data, status, headers)->
$scope.processing = false
From b405b4dce6647682e4e86897d25986c427ee87da Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Wed, 10 Aug 2016 17:34:32 +0100
Subject: [PATCH 129/378] Remove Countly integration.
---
services/web/app/views/layout.jade | 70 ------------------------------
1 file changed, 70 deletions(-)
diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade
index 83d7da4838..3c781a4b9f 100644
--- a/services/web/app/views/layout.jade
+++ b/services/web/app/views/layout.jade
@@ -46,76 +46,6 @@ html(itemscope, itemtype='http://schema.org/Product')
- else
script(type='text/javascript').
window.ga = function() { console.log("would send to GA", arguments) };
-
- // Countly Analytics
- if (settings.analytics && settings.analytics.countly && settings.analytics.countly.token)
- script(type="text/javascript").
- var Countly = Countly || {};
- Countly.q = Countly.q || [];
- Countly.app_key = '#{settings.analytics.countly.token}';
- Countly.url = '#{settings.analytics.countly.server}';
- !{ session.user ? 'Countly.device_id = "' + session.user._id + '";' : '' }
-
- (function() {
- var cly = document.createElement('script'); cly.type = 'text/javascript';
- cly.async = true;
- //enter url of script here
- cly.src = 'https://cdnjs.cloudflare.com/ajax/libs/countly-sdk-web/16.6.0/countly.min.js';
- cly.onload = function(){Countly.init()};
- var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(cly, s);
- })();
-
- script(type="text/javascript")
- if (session && session.user)
- - var name = session.user.first_name + (session.user.last_name ? ' ' + session.user.last_name : '');
- | Countly.q.push(['user_details', { email: '#{session.user.email}', name: '#{name}' }]);
-
- if (justRegistered)
- | Countly.q.push(['add_event',{ key: 'user-registered' }]);
-
- if (justLoggedIn)
- | Countly.q.push(['add_event',{ key: 'user-logged-in' }]);
-
- if (user && user.features)
- - featureFlagSet = false;
-
- if user.features.hasOwnProperty('collaborators')
- | Countly.q.push([ 'userData.set', 'features-collaborators', #{ user.features.collaborators } ]);
- - featureFlagSet = true;
-
- if user.features.hasOwnProperty('compileGroup')
- | Countly.q.push([ 'userData.set', 'features-compileGroup', '#{ user.features.compileGroup }' ]);
- - featureFlagSet = true;
-
- if user.features.hasOwnProperty('compileTimeout')
- | Countly.q.push([ 'userData.set', 'features-compileTimeout', #{ user.features.compileTimeout } ]);
- - featureFlagSet = true;
-
- if user.features.hasOwnProperty('dropbox')
- | Countly.q.push([ 'userData.set', 'features-dropbox', #{ user.features.dropbox } ]);
- - featureFlagSet = true;
-
- if user.features.hasOwnProperty('github')
- | Countly.q.push([ 'userData.set', 'features-github', #{ user.features.github } ]);
- - featureFlagSet = true;
-
- if user.features.hasOwnProperty('references')
- | Countly.q.push([ 'userData.set', 'features-references', #{ user.features.references } ]);
- - featureFlagSet = true;
-
- if user.features.hasOwnProperty('templates')
- | Countly.q.push([ 'userData.set', 'features-templates', #{ user.features.templates } ]);
- - featureFlagSet = true;
-
- if user.features.hasOwnProperty('versioning')
- | Countly.q.push([ 'userData.set', 'features-versioning', #{ user.features.versioning } ]);
- - featureFlagSet = true;
-
-
- if featureFlagSet
- | Countly.q.push(['userData.save'])
-
- // End countly Analytics
script(type="text/javascript").
window.csrfToken = "#{csrfToken}";
From 64d0b8bc7c363d01beed41bd1449ec486928e0bc Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Wed, 10 Aug 2016 17:46:22 +0100
Subject: [PATCH 130/378] Update shrinkwrap.
---
services/web/npm-shrinkwrap.json | 1503 +++++++++++++++++-------------
1 file changed, 876 insertions(+), 627 deletions(-)
diff --git a/services/web/npm-shrinkwrap.json b/services/web/npm-shrinkwrap.json
index 89d5dfe8bb..a2896277a5 100644
--- a/services/web/npm-shrinkwrap.json
+++ b/services/web/npm-shrinkwrap.json
@@ -9,17 +9,17 @@
"dependencies": {
"buffer-crc32": {
"version": "0.2.5",
- "from": "buffer-crc32@~0.2.1",
+ "from": "buffer-crc32@>=0.2.1 <0.3.0",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.5.tgz"
},
"readable-stream": {
"version": "1.0.34",
- "from": "readable-stream@~1.0.24",
+ "from": "readable-stream@>=1.0.24 <1.1.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"dependencies": {
"core-util-is": {
"version": "1.0.2",
- "from": "core-util-is@~1.0.0",
+ "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"
},
"isarray": {
@@ -29,39 +29,39 @@
},
"string_decoder": {
"version": "0.10.31",
- "from": "string_decoder@~0.10.x",
+ "from": "string_decoder@>=0.10.0 <0.11.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@~2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
}
}
},
"tar-stream": {
"version": "0.3.3",
- "from": "tar-stream@~0.3.0",
+ "from": "tar-stream@>=0.3.0 <0.4.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-0.3.3.tgz",
"dependencies": {
"bl": {
"version": "0.6.0",
- "from": "bl@~0.6.0",
+ "from": "bl@>=0.6.0 <0.7.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-0.6.0.tgz"
},
"end-of-stream": {
"version": "0.1.5",
- "from": "end-of-stream@~0.1.3",
+ "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",
"dependencies": {
"once": {
"version": "1.3.3",
- "from": "once@~1.3.0",
+ "from": "once@>=1.3.0 <2.0.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz",
"dependencies": {
"wrappy": {
"version": "1.0.2",
- "from": "wrappy@1",
+ "from": "wrappy@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
}
}
@@ -72,17 +72,17 @@
},
"zip-stream": {
"version": "0.3.7",
- "from": "zip-stream@~0.3.0",
+ "from": "zip-stream@>=0.3.0 <0.4.0",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-0.3.7.tgz",
"dependencies": {
"crc32-stream": {
"version": "0.2.0",
- "from": "crc32-stream@~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"
},
"debug": {
"version": "1.0.4",
- "from": "debug@~1.0.2",
+ "from": "debug@>=1.0.2 <1.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz",
"dependencies": {
"ms": {
@@ -94,54 +94,54 @@
},
"deflate-crc32-stream": {
"version": "0.1.2",
- "from": "deflate-crc32-stream@~0.1.0",
+ "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"
}
}
},
"lazystream": {
"version": "0.1.0",
- "from": "lazystream@~0.1.0",
+ "from": "lazystream@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-0.1.0.tgz"
},
"file-utils": {
"version": "0.1.5",
- "from": "file-utils@~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",
+ "from": "lodash@>=2.1.0 <2.2.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-2.1.0.tgz"
},
"iconv-lite": {
"version": "0.2.11",
- "from": "iconv-lite@~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"
},
"glob": {
"version": "3.2.11",
- "from": "glob@~3.2.6",
+ "from": "glob@>=3.2.6 <3.3.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
"dependencies": {
"inherits": {
"version": "2.0.1",
- "from": "inherits@2",
+ "from": "inherits@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"minimatch": {
"version": "0.3.0",
- "from": "minimatch@0.3",
+ "from": "minimatch@>=0.3.0 <0.4.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz",
"dependencies": {
"lru-cache": {
"version": "2.7.3",
- "from": "lru-cache@2",
+ "from": "lru-cache@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
},
"sigmund": {
"version": "1.0.1",
- "from": "sigmund@~1.0.0",
+ "from": "sigmund@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
}
}
@@ -150,43 +150,43 @@
},
"minimatch": {
"version": "0.2.14",
- "from": "minimatch@~0.2.12",
+ "from": "minimatch@>=0.2.12 <0.3.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
"dependencies": {
"lru-cache": {
"version": "2.7.3",
- "from": "lru-cache@2",
+ "from": "lru-cache@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
},
"sigmund": {
"version": "1.0.1",
- "from": "sigmund@~1.0.0",
+ "from": "sigmund@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
}
}
},
"findup-sync": {
"version": "0.1.3",
- "from": "findup-sync@~0.1.2",
+ "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",
+ "from": "lodash@>=2.4.1 <2.5.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
}
}
},
"isbinaryfile": {
"version": "0.1.9",
- "from": "isbinaryfile@~0.1.9",
+ "from": "isbinaryfile@>=0.1.9 <0.2.0",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-0.1.9.tgz"
}
}
},
"lodash": {
"version": "2.4.2",
- "from": "lodash@~2.4.1",
+ "from": "lodash@>=2.4.1 <2.5.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
}
}
@@ -198,47 +198,47 @@
},
"base64-stream": {
"version": "0.1.3",
- "from": "base64-stream@^0.1.2",
+ "from": "base64-stream@>=0.1.2 <0.2.0",
"resolved": "https://registry.npmjs.org/base64-stream/-/base64-stream-0.1.3.tgz",
"dependencies": {
"readable-stream": {
"version": "2.1.4",
- "from": "readable-stream@^2.0.2",
+ "from": "readable-stream@>=2.0.2 <3.0.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz",
"dependencies": {
"buffer-shims": {
"version": "1.0.0",
- "from": "buffer-shims@^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"
},
"core-util-is": {
"version": "1.0.2",
- "from": "core-util-is@~1.0.0",
+ "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"
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@~2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"isarray": {
"version": "1.0.0",
- "from": "isarray@~1.0.0",
+ "from": "isarray@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
},
"process-nextick-args": {
"version": "1.0.7",
- "from": "process-nextick-args@~1.0.6",
+ "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"
},
"string_decoder": {
"version": "0.10.31",
- "from": "string_decoder@~0.10.x",
+ "from": "string_decoder@>=0.10.0 <0.11.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
},
"util-deprecate": {
"version": "1.0.2",
- "from": "util-deprecate@~1.0.1",
+ "from": "util-deprecate@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
}
}
@@ -247,7 +247,7 @@
},
"basic-auth-connect": {
"version": "1.0.0",
- "from": "basic-auth-connect@^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": {
@@ -268,23 +268,23 @@
}
},
"body-parser": {
- "version": "1.15.1",
- "from": "body-parser@^1.13.1",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.15.1.tgz",
+ "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": {
"bytes": {
- "version": "2.3.0",
- "from": "bytes@2.3.0",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.3.0.tgz"
+ "version": "2.4.0",
+ "from": "bytes@2.4.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz"
},
"content-type": {
"version": "1.0.2",
- "from": "content-type@~1.0.1",
+ "from": "content-type@>=1.0.2 <1.1.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz"
},
"debug": {
"version": "2.2.0",
- "from": "debug@~2.2.0",
+ "from": "debug@>=2.2.0 <2.3.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
"dependencies": {
"ms": {
@@ -296,22 +296,27 @@
},
"depd": {
"version": "1.1.0",
- "from": "depd@~1.1.0",
+ "from": "depd@>=1.1.0 <1.2.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz"
},
"http-errors": {
- "version": "1.4.0",
- "from": "http-errors@~1.4.0",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz",
+ "version": "1.5.0",
+ "from": "http-errors@>=1.5.0 <1.6.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.0.tgz",
"dependencies": {
"inherits": {
"version": "2.0.1",
"from": "inherits@2.0.1",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
+ "setprototypeof": {
+ "version": "1.0.1",
+ "from": "setprototypeof@1.0.1",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.1.tgz"
+ },
"statuses": {
"version": "1.3.0",
- "from": "statuses@>= 1.2.1 < 2",
+ "from": "statuses@>=1.3.0 <2.0.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz"
}
}
@@ -323,7 +328,7 @@
},
"on-finished": {
"version": "2.3.0",
- "from": "on-finished@~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",
"dependencies": {
"ee-first": {
@@ -334,14 +339,14 @@
}
},
"qs": {
- "version": "6.1.0",
- "from": "qs@6.1.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz"
+ "version": "6.2.0",
+ "from": "qs@6.2.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.0.tgz"
},
"raw-body": {
- "version": "2.1.6",
- "from": "raw-body@~2.1.6",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.6.tgz",
+ "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": {
"unpipe": {
"version": "1.0.0",
@@ -352,7 +357,7 @@
},
"type-is": {
"version": "1.6.13",
- "from": "type-is@~1.6.12",
+ "from": "type-is@>=1.6.13 <1.7.0",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.13.tgz",
"dependencies": {
"media-typer": {
@@ -362,12 +367,12 @@
},
"mime-types": {
"version": "2.1.11",
- "from": "mime-types@~2.1.11",
+ "from": "mime-types@>=2.1.11 <2.2.0",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz",
"dependencies": {
"mime-db": {
"version": "1.23.0",
- "from": "mime-db@~1.23.0",
+ "from": "mime-db@>=1.23.0 <1.24.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz"
}
}
@@ -388,7 +393,7 @@
"dependencies": {
"debug": {
"version": "1.0.4",
- "from": "debug@^1.0.4",
+ "from": "debug@>=1.0.4 <2.0.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz",
"dependencies": {
"ms": {
@@ -400,37 +405,37 @@
},
"redis": {
"version": "0.12.1",
- "from": "redis@^0.12.1",
+ "from": "redis@>=0.12.1 <0.13.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz"
}
}
},
"contentful": {
- "version": "3.3.14",
- "from": "contentful@^3.3.14",
- "resolved": "https://registry.npmjs.org/contentful/-/contentful-3.3.14.tgz",
+ "version": "3.5.0",
+ "from": "contentful@>=3.3.14 <4.0.0",
+ "resolved": "https://registry.npmjs.org/contentful/-/contentful-3.5.0.tgz",
"dependencies": {
"babel-runtime": {
- "version": "6.9.2",
- "from": "babel-runtime@^6.3.19",
- "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.9.2.tgz",
+ "version": "6.11.6",
+ "from": "babel-runtime@>=6.3.19 <7.0.0",
+ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.11.6.tgz",
"dependencies": {
"core-js": {
- "version": "2.4.0",
- "from": "core-js@^2.4.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.0.tgz"
+ "version": "2.4.1",
+ "from": "core-js@>=2.4.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz"
},
"regenerator-runtime": {
"version": "0.9.5",
- "from": "regenerator-runtime@^0.9.5",
+ "from": "regenerator-runtime@>=0.9.5 <0.10.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.9.5.tgz"
}
}
},
"contentful-sdk-core": {
- "version": "2.2.4",
- "from": "contentful-sdk-core@^2.2.1",
- "resolved": "https://registry.npmjs.org/contentful-sdk-core/-/contentful-sdk-core-2.2.4.tgz",
+ "version": "2.3.4",
+ "from": "contentful-sdk-core@>=2.3.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/contentful-sdk-core/-/contentful-sdk-core-2.3.4.tgz",
"dependencies": {
"follow-redirects": {
"version": "0.0.7",
@@ -439,7 +444,7 @@
"dependencies": {
"debug": {
"version": "2.2.0",
- "from": "debug@^2.2.0",
+ "from": "debug@>=2.2.0 <3.0.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
"dependencies": {
"ms": {
@@ -451,28 +456,28 @@
},
"stream-consume": {
"version": "0.1.0",
- "from": "stream-consume@^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"
}
}
},
"qs": {
- "version": "6.2.0",
- "from": "qs@^6.1.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.0.tgz"
+ "version": "6.2.1",
+ "from": "qs@>=6.1.0 <7.0.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.1.tgz"
}
}
},
"json-stringify-safe": {
"version": "5.0.1",
- "from": "json-stringify-safe@^5.0.1",
+ "from": "json-stringify-safe@>=5.0.1 <6.0.0",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
}
}
},
"cookie": {
"version": "0.2.4",
- "from": "cookie@^0.2.3",
+ "from": "cookie@>=0.2.3 <0.3.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.2.4.tgz"
},
"cookie-parser": {
@@ -494,7 +499,7 @@
},
"csurf": {
"version": "1.9.0",
- "from": "csurf@^1.8.3",
+ "from": "csurf@>=1.8.3 <2.0.0",
"resolved": "https://registry.npmjs.org/csurf/-/csurf-1.9.0.tgz",
"dependencies": {
"cookie": {
@@ -509,7 +514,7 @@
},
"csrf": {
"version": "3.0.3",
- "from": "csrf@~3.0.3",
+ "from": "csrf@>=3.0.3 <3.1.0",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.3.tgz",
"dependencies": {
"base64-url": {
@@ -534,7 +539,7 @@
"dependencies": {
"random-bytes": {
"version": "1.0.0",
- "from": "random-bytes@~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"
}
}
@@ -543,7 +548,7 @@
},
"http-errors": {
"version": "1.5.0",
- "from": "http-errors@~1.5.0",
+ "from": "http-errors@>=1.5.0 <1.6.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.0.tgz",
"dependencies": {
"inherits": {
@@ -558,7 +563,7 @@
},
"statuses": {
"version": "1.3.0",
- "from": "statuses@>= 1.3.0 < 2",
+ "from": "statuses@>=1.3.0 <2.0.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz"
}
}
@@ -577,17 +582,17 @@
"dependencies": {
"accepts": {
"version": "1.2.13",
- "from": "accepts@~1.2.9",
+ "from": "accepts@>=1.2.9 <1.3.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz",
"dependencies": {
"mime-types": {
"version": "2.1.11",
- "from": "mime-types@~2.1.11",
+ "from": "mime-types@>=2.1.6 <2.2.0",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz",
"dependencies": {
"mime-db": {
"version": "1.23.0",
- "from": "mime-db@~1.23.0",
+ "from": "mime-db@>=1.23.0 <1.24.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz"
}
}
@@ -611,7 +616,7 @@
},
"content-type": {
"version": "1.0.2",
- "from": "content-type@~1.0.1",
+ "from": "content-type@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz"
},
"cookie": {
@@ -626,7 +631,7 @@
},
"debug": {
"version": "2.2.0",
- "from": "debug@~2.2.0",
+ "from": "debug@>=2.2.0 <2.3.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
"dependencies": {
"ms": {
@@ -638,7 +643,7 @@
},
"depd": {
"version": "1.0.1",
- "from": "depd@~1.0.1",
+ "from": "depd@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
},
"escape-html": {
@@ -648,7 +653,7 @@
},
"etag": {
"version": "1.7.0",
- "from": "etag@~1.7.0",
+ "from": "etag@>=1.7.0 <1.8.0",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz"
},
"finalhandler": {
@@ -658,7 +663,7 @@
"dependencies": {
"unpipe": {
"version": "1.0.0",
- "from": "unpipe@~1.0.0",
+ "from": "unpipe@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
}
}
@@ -675,12 +680,12 @@
},
"methods": {
"version": "1.1.2",
- "from": "methods@~1.1.1",
+ "from": "methods@>=1.1.1 <1.2.0",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
},
"on-finished": {
"version": "2.3.0",
- "from": "on-finished@~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",
"dependencies": {
"ee-first": {
@@ -692,7 +697,7 @@
},
"parseurl": {
"version": "1.3.1",
- "from": "parseurl@~1.3.0",
+ "from": "parseurl@>=1.3.0 <1.4.0",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz"
},
"path-to-regexp": {
@@ -702,12 +707,12 @@
},
"proxy-addr": {
"version": "1.0.10",
- "from": "proxy-addr@~1.0.8",
+ "from": "proxy-addr@>=1.0.8 <1.1.0",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz",
"dependencies": {
"forwarded": {
"version": "0.1.0",
- "from": "forwarded@~0.1.0",
+ "from": "forwarded@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz"
},
"ipaddr.js": {
@@ -724,7 +729,7 @@
},
"range-parser": {
"version": "1.0.3",
- "from": "range-parser@~1.0.2",
+ "from": "range-parser@>=1.0.2 <1.1.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz"
},
"send": {
@@ -739,12 +744,12 @@
},
"http-errors": {
"version": "1.3.1",
- "from": "http-errors@~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",
"dependencies": {
"inherits": {
"version": "2.0.1",
- "from": "inherits@~2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
}
}
@@ -761,19 +766,19 @@
},
"statuses": {
"version": "1.2.1",
- "from": "statuses@~1.2.1",
+ "from": "statuses@>=1.2.1 <1.3.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
}
}
},
"serve-static": {
"version": "1.10.3",
- "from": "serve-static@~1.10.0",
+ "from": "serve-static@>=1.10.0 <1.11.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.10.3.tgz",
"dependencies": {
"escape-html": {
"version": "1.0.3",
- "from": "escape-html@~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"
},
"send": {
@@ -783,22 +788,22 @@
"dependencies": {
"depd": {
"version": "1.1.0",
- "from": "depd@~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.4",
- "from": "destroy@~1.0.4",
+ "from": "destroy@>=1.0.4 <1.1.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz"
},
"http-errors": {
"version": "1.3.1",
- "from": "http-errors@~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",
"dependencies": {
"inherits": {
"version": "2.0.1",
- "from": "inherits@~2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
}
}
@@ -815,7 +820,7 @@
},
"statuses": {
"version": "1.2.1",
- "from": "statuses@~1.2.1",
+ "from": "statuses@>=1.2.1 <1.3.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
}
}
@@ -824,7 +829,7 @@
},
"type-is": {
"version": "1.6.13",
- "from": "type-is@~1.6.3",
+ "from": "type-is@>=1.6.3 <1.7.0",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.13.tgz",
"dependencies": {
"media-typer": {
@@ -834,12 +839,12 @@
},
"mime-types": {
"version": "2.1.11",
- "from": "mime-types@~2.1.11",
+ "from": "mime-types@>=2.1.6 <2.2.0",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz",
"dependencies": {
"mime-db": {
"version": "1.23.0",
- "from": "mime-db@~1.23.0",
+ "from": "mime-db@>=1.23.0 <1.24.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz"
}
}
@@ -848,7 +853,7 @@
},
"vary": {
"version": "1.0.1",
- "from": "vary@~1.0.0",
+ "from": "vary@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz"
},
"utils-merge": {
@@ -880,7 +885,7 @@
},
"debug": {
"version": "2.2.0",
- "from": "debug@~2.2.0",
+ "from": "debug@>=2.2.0 <2.3.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
"dependencies": {
"ms": {
@@ -892,22 +897,22 @@
},
"depd": {
"version": "1.0.1",
- "from": "depd@~1.0.1",
+ "from": "depd@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
},
"on-headers": {
"version": "1.0.1",
- "from": "on-headers@~1.0.0",
+ "from": "on-headers@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz"
},
"parseurl": {
"version": "1.3.1",
- "from": "parseurl@~1.3.0",
+ "from": "parseurl@>=1.3.0 <1.4.0",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz"
},
"uid-safe": {
"version": "2.0.0",
- "from": "uid-safe@~2.0.0",
+ "from": "uid-safe@>=2.0.0 <2.1.0",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.0.0.tgz",
"dependencies": {
"base64-url": {
@@ -926,22 +931,22 @@
},
"grunt": {
"version": "0.4.5",
- "from": "grunt@^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",
+ "from": "async@>=0.1.22 <0.2.0",
"resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz"
},
"coffee-script": {
"version": "1.3.3",
- "from": "coffee-script@~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",
+ "from": "colors@>=0.6.2 <0.7.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz"
},
"dateformat": {
@@ -951,37 +956,37 @@
},
"eventemitter2": {
"version": "0.4.14",
- "from": "eventemitter2@~0.4.13",
+ "from": "eventemitter2@>=0.4.13 <0.5.0",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz"
},
"findup-sync": {
"version": "0.1.3",
- "from": "findup-sync@~0.1.2",
+ "from": "findup-sync@>=0.1.2 <0.2.0",
"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz",
"dependencies": {
"glob": {
"version": "3.2.11",
- "from": "glob@~3.2.9",
+ "from": "glob@>=3.2.9 <3.3.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
"dependencies": {
"inherits": {
"version": "2.0.1",
- "from": "inherits@2",
+ "from": "inherits@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"minimatch": {
"version": "0.3.0",
- "from": "minimatch@0.3",
+ "from": "minimatch@>=0.3.0 <0.4.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz",
"dependencies": {
"lru-cache": {
"version": "2.7.3",
- "from": "lru-cache@2",
+ "from": "lru-cache@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
},
"sigmund": {
"version": "1.0.1",
- "from": "sigmund@~1.0.0",
+ "from": "sigmund@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
}
}
@@ -990,149 +995,149 @@
},
"lodash": {
"version": "2.4.2",
- "from": "lodash@~2.4.1",
+ "from": "lodash@>=2.4.1 <2.5.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
}
}
},
"glob": {
"version": "3.1.21",
- "from": "glob@~3.1.21",
+ "from": "glob@>=3.1.21 <3.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz",
"dependencies": {
"graceful-fs": {
"version": "1.2.3",
- "from": "graceful-fs@~1.2.0",
+ "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",
+ "from": "inherits@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz"
}
}
},
"hooker": {
"version": "0.2.3",
- "from": "hooker@~0.2.3",
+ "from": "hooker@>=0.2.3 <0.3.0",
"resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz"
},
"iconv-lite": {
"version": "0.2.11",
- "from": "iconv-lite@~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"
},
"minimatch": {
"version": "0.2.14",
- "from": "minimatch@~0.2.12",
+ "from": "minimatch@>=0.2.12 <0.3.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
"dependencies": {
"lru-cache": {
"version": "2.7.3",
- "from": "lru-cache@2",
+ "from": "lru-cache@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
},
"sigmund": {
"version": "1.0.1",
- "from": "sigmund@~1.0.0",
+ "from": "sigmund@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
}
}
},
"nopt": {
"version": "1.0.10",
- "from": "nopt@~1.0.10",
+ "from": "nopt@>=1.0.10 <1.1.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
"dependencies": {
"abbrev": {
- "version": "1.0.7",
- "from": "abbrev@1",
- "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz"
+ "version": "1.0.9",
+ "from": "abbrev@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz"
}
}
},
"rimraf": {
"version": "2.2.8",
- "from": "rimraf@~2.2.8",
+ "from": "rimraf@>=2.2.8 <2.3.0",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz"
},
"lodash": {
"version": "0.9.2",
- "from": "lodash@~0.9.2",
+ "from": "lodash@>=0.9.2 <0.10.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz"
},
"underscore.string": {
"version": "2.2.1",
- "from": "underscore.string@~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"
},
"which": {
"version": "1.0.9",
- "from": "which@~1.0.5",
+ "from": "which@>=1.0.5 <1.1.0",
"resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz"
},
"js-yaml": {
"version": "2.0.5",
- "from": "js-yaml@~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",
"dependencies": {
"argparse": {
"version": "0.1.16",
- "from": "argparse@~ 0.1.11",
+ "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",
+ "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",
+ "from": "underscore.string@>=2.4.0 <2.5.0",
"resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz"
}
}
},
"esprima": {
"version": "1.0.4",
- "from": "esprima@~ 1.0.2",
+ "from": "esprima@>=1.0.2 <1.1.0",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz"
}
}
},
"exit": {
"version": "0.1.2",
- "from": "exit@~0.1.1",
+ "from": "exit@>=0.1.1 <0.2.0",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz"
},
"getobject": {
"version": "0.1.0",
- "from": "getobject@~0.1.0",
+ "from": "getobject@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz"
},
"grunt-legacy-util": {
"version": "0.2.0",
- "from": "grunt-legacy-util@~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"
},
"grunt-legacy-log": {
"version": "0.1.3",
- "from": "grunt-legacy-log@~0.1.0",
+ "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": {
"grunt-legacy-log-utils": {
"version": "0.1.1",
- "from": "grunt-legacy-log-utils@~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"
},
"lodash": {
"version": "2.4.2",
- "from": "lodash@~2.4.1",
+ "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",
+ "from": "underscore.string@>=2.3.3 <2.4.0",
"resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz"
}
}
@@ -1141,29 +1146,29 @@
},
"heapdump": {
"version": "0.3.7",
- "from": "heapdump@^0.3.7",
+ "from": "heapdump@>=0.3.7 <0.4.0",
"resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.7.tgz"
},
"http-proxy": {
- "version": "1.13.3",
- "from": "http-proxy@^1.8.1",
- "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.13.3.tgz",
+ "version": "1.14.0",
+ "from": "http-proxy@>=1.8.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.14.0.tgz",
"dependencies": {
"eventemitter3": {
"version": "1.2.0",
- "from": "eventemitter3@1.x.x",
+ "from": "eventemitter3@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz"
},
"requires-port": {
"version": "1.0.0",
- "from": "requires-port@1.x.x",
+ "from": "requires-port@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz"
}
}
},
"jade": {
"version": "1.3.1",
- "from": "jade@~1.3.1",
+ "from": "jade@>=1.3.1 <1.4.0",
"resolved": "https://registry.npmjs.org/jade/-/jade-1.3.1.tgz",
"dependencies": {
"commander": {
@@ -1173,7 +1178,7 @@
},
"mkdirp": {
"version": "0.3.5",
- "from": "mkdirp@~0.3.5",
+ "from": "mkdirp@>=0.3.5 <0.4.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
},
"transformers": {
@@ -1183,19 +1188,19 @@
"dependencies": {
"promise": {
"version": "2.0.0",
- "from": "promise@~2.0",
+ "from": "promise@>=2.0.0 <2.1.0",
"resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz",
"dependencies": {
"is-promise": {
"version": "1.0.1",
- "from": "is-promise@~1",
+ "from": "is-promise@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz"
}
}
},
"css": {
"version": "1.0.8",
- "from": "css@~1.0.8",
+ "from": "css@>=1.0.8 <1.1.0",
"resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz",
"dependencies": {
"css-parse": {
@@ -1212,12 +1217,12 @@
},
"uglify-js": {
"version": "2.2.5",
- "from": "uglify-js@~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",
"dependencies": {
"source-map": {
"version": "0.1.43",
- "from": "source-map@~0.1.7",
+ "from": "source-map@>=0.1.7 <0.2.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
"dependencies": {
"amdefine": {
@@ -1229,12 +1234,12 @@
},
"optimist": {
"version": "0.3.7",
- "from": "optimist@~0.3.5",
+ "from": "optimist@>=0.3.5 <0.4.0",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz",
"dependencies": {
"wordwrap": {
"version": "0.0.3",
- "from": "wordwrap@~0.0.2",
+ "from": "wordwrap@>=0.0.2 <0.1.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz"
}
}
@@ -1255,23 +1260,23 @@
"dependencies": {
"readdirp": {
"version": "0.2.5",
- "from": "readdirp@~0.2.3",
+ "from": "readdirp@>=0.2.3 <0.3.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-0.2.5.tgz",
"dependencies": {
"minimatch": {
- "version": "3.0.0",
+ "version": "3.0.3",
"from": "minimatch@>=0.2.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz",
"dependencies": {
"brace-expansion": {
- "version": "1.1.4",
- "from": "brace-expansion@^1.0.0",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz",
+ "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",
"dependencies": {
"balanced-match": {
- "version": "0.4.1",
- "from": "balanced-match@^0.4.1",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
+ "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"
},
"concat-map": {
"version": "0.0.1",
@@ -1288,17 +1293,17 @@
},
"with": {
"version": "3.0.1",
- "from": "with@~3.0.0",
+ "from": "with@>=3.0.0 <3.1.0",
"resolved": "https://registry.npmjs.org/with/-/with-3.0.1.tgz",
"dependencies": {
"uglify-js": {
"version": "2.4.24",
- "from": "uglify-js@~2.4.12",
+ "from": "uglify-js@>=2.4.12 <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",
+ "from": "async@>=0.2.6 <0.3.0",
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
},
"source-map": {
@@ -1315,22 +1320,22 @@
},
"uglify-to-browserify": {
"version": "1.0.2",
- "from": "uglify-to-browserify@~1.0.0",
+ "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"
},
"yargs": {
"version": "3.5.4",
- "from": "yargs@~3.5.4",
+ "from": "yargs@>=3.5.4 <3.6.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz",
"dependencies": {
"camelcase": {
"version": "1.2.1",
- "from": "camelcase@^1.0.2",
+ "from": "camelcase@>=1.0.2 <2.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz"
},
"decamelize": {
"version": "1.2.0",
- "from": "decamelize@^1.0.0",
+ "from": "decamelize@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
},
"window-size": {
@@ -1351,17 +1356,17 @@
},
"constantinople": {
"version": "2.0.1",
- "from": "constantinople@~2.0.0",
+ "from": "constantinople@>=2.0.0 <2.1.0",
"resolved": "https://registry.npmjs.org/constantinople/-/constantinople-2.0.1.tgz",
"dependencies": {
"uglify-js": {
"version": "2.4.24",
- "from": "uglify-js@~2.4.0",
+ "from": "uglify-js@>=2.4.12 <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",
+ "from": "async@>=0.2.6 <0.3.0",
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
},
"source-map": {
@@ -1378,22 +1383,22 @@
},
"uglify-to-browserify": {
"version": "1.0.2",
- "from": "uglify-to-browserify@~1.0.0",
+ "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"
},
"yargs": {
"version": "3.5.4",
- "from": "yargs@~3.5.4",
+ "from": "yargs@>=3.5.4 <3.6.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz",
"dependencies": {
"camelcase": {
"version": "1.2.1",
- "from": "camelcase@^1.0.2",
+ "from": "camelcase@>=1.0.2 <2.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz"
},
"decamelize": {
"version": "1.2.0",
- "from": "decamelize@^1.0.0",
+ "from": "decamelize@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
},
"window-size": {
@@ -1416,7 +1421,7 @@
},
"ldapjs": {
"version": "1.0.0",
- "from": "ldapjs@^1.0.0",
+ "from": "ldapjs@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.0.tgz",
"dependencies": {
"asn1": {
@@ -1436,12 +1441,12 @@
"dependencies": {
"mv": {
"version": "2.1.1",
- "from": "mv@~2",
+ "from": "mv@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"dependencies": {
"mkdirp": {
"version": "0.5.1",
- "from": "mkdirp@~0.5.1",
+ "from": "mkdirp@>=0.5.1 <0.6.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"dependencies": {
"minimist": {
@@ -1453,50 +1458,50 @@
},
"ncp": {
"version": "2.0.0",
- "from": "ncp@~2.0.0",
+ "from": "ncp@>=2.0.0 <2.1.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz"
},
"rimraf": {
"version": "2.4.5",
- "from": "rimraf@~2.4.0",
+ "from": "rimraf@>=2.4.0 <2.5.0",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"dependencies": {
"glob": {
"version": "6.0.4",
- "from": "glob@^6.0.1",
+ "from": "glob@>=6.0.1 <7.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"dependencies": {
"inflight": {
"version": "1.0.5",
- "from": "inflight@^1.0.4",
+ "from": "inflight@>=1.0.4 <2.0.0",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz",
"dependencies": {
"wrappy": {
"version": "1.0.2",
- "from": "wrappy@1",
+ "from": "wrappy@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
}
}
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@2",
+ "from": "inherits@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"minimatch": {
- "version": "3.0.0",
- "from": "minimatch@2 || 3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz",
+ "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",
"dependencies": {
"brace-expansion": {
- "version": "1.1.4",
- "from": "brace-expansion@^1.0.0",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz",
+ "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",
"dependencies": {
"balanced-match": {
- "version": "0.4.1",
- "from": "balanced-match@^0.4.1",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
+ "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"
},
"concat-map": {
"version": "0.0.1",
@@ -1509,7 +1514,7 @@
},
"path-is-absolute": {
"version": "1.0.0",
- "from": "path-is-absolute@^1.0.0",
+ "from": "path-is-absolute@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz"
}
}
@@ -1520,7 +1525,7 @@
},
"safe-json-stringify": {
"version": "1.0.3",
- "from": "safe-json-stringify@~1",
+ "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"
}
}
@@ -1537,7 +1542,7 @@
"dependencies": {
"precond": {
"version": "0.2.3",
- "from": "precond@0.2",
+ "from": "precond@>=0.2.0 <0.3.0",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz"
}
}
@@ -1554,7 +1559,7 @@
"dependencies": {
"wrappy": {
"version": "1.0.2",
- "from": "wrappy@1",
+ "from": "wrappy@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
}
}
@@ -1578,26 +1583,26 @@
},
"dtrace-provider": {
"version": "0.6.0",
- "from": "dtrace-provider@~0.6",
+ "from": "dtrace-provider@0.6.0",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz",
"dependencies": {
"nan": {
- "version": "2.3.5",
- "from": "nan@^2.0.8",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz"
+ "version": "2.4.0",
+ "from": "nan@>=2.0.8 <3.0.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz"
}
}
}
}
},
"lodash": {
- "version": "4.13.1",
- "from": "lodash@^4.13.1",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.13.1.tgz"
+ "version": "4.14.2",
+ "from": "lodash@>=4.13.1 <5.0.0",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.14.2.tgz"
},
"logger-sharelatex": {
"version": "1.3.1",
- "from": "logger-sharelatex@git+https://github.com/sharelatex/logger-sharelatex.git#v1.3.1",
+ "from": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.3.1",
"resolved": "git+https://github.com/sharelatex/logger-sharelatex.git#bf413ec621a000cf0e08c939de38d5e24541a08c",
"dependencies": {
"bunyan": {
@@ -1607,24 +1612,24 @@
"dependencies": {
"dtrace-provider": {
"version": "0.6.0",
- "from": "dtrace-provider@~0.6",
+ "from": "dtrace-provider@>=0.6.0 <0.7.0",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz",
"dependencies": {
"nan": {
- "version": "2.3.5",
- "from": "nan@^2.0.8",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz"
+ "version": "2.4.0",
+ "from": "nan@>=2.0.8 <3.0.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz"
}
}
},
"mv": {
"version": "2.1.1",
- "from": "mv@~2",
+ "from": "mv@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"dependencies": {
"mkdirp": {
"version": "0.5.1",
- "from": "mkdirp@~0.5.1",
+ "from": "mkdirp@>=0.5.1 <0.6.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"dependencies": {
"minimist": {
@@ -1636,50 +1641,50 @@
},
"ncp": {
"version": "2.0.0",
- "from": "ncp@~2.0.0",
+ "from": "ncp@>=2.0.0 <2.1.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz"
},
"rimraf": {
"version": "2.4.5",
- "from": "rimraf@~2.4.0",
+ "from": "rimraf@>=2.4.0 <2.5.0",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"dependencies": {
"glob": {
"version": "6.0.4",
- "from": "glob@^6.0.1",
+ "from": "glob@>=6.0.1 <7.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"dependencies": {
"inflight": {
"version": "1.0.5",
- "from": "inflight@^1.0.4",
+ "from": "inflight@>=1.0.4 <2.0.0",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz",
"dependencies": {
"wrappy": {
"version": "1.0.2",
- "from": "wrappy@1",
+ "from": "wrappy@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
}
}
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@2",
+ "from": "inherits@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"minimatch": {
- "version": "3.0.0",
- "from": "minimatch@2 || 3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz",
+ "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",
"dependencies": {
"brace-expansion": {
- "version": "1.1.4",
- "from": "brace-expansion@^1.0.0",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz",
+ "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",
"dependencies": {
"balanced-match": {
- "version": "0.4.1",
- "from": "balanced-match@^0.4.1",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
+ "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"
},
"concat-map": {
"version": "0.0.1",
@@ -1692,19 +1697,19 @@
},
"once": {
"version": "1.3.3",
- "from": "once@^1.3.0",
+ "from": "once@>=1.3.0 <2.0.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz",
"dependencies": {
"wrappy": {
"version": "1.0.2",
- "from": "wrappy@1",
+ "from": "wrappy@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
}
}
},
"path-is-absolute": {
"version": "1.0.0",
- "from": "path-is-absolute@^1.0.0",
+ "from": "path-is-absolute@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz"
}
}
@@ -1715,7 +1720,7 @@
},
"safe-json-stringify": {
"version": "1.0.3",
- "from": "safe-json-stringify@~1",
+ "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"
}
}
@@ -1727,7 +1732,7 @@
},
"raven": {
"version": "0.8.1",
- "from": "raven@^0.8.0",
+ "from": "raven@>=0.8.0 <0.9.0",
"resolved": "https://registry.npmjs.org/raven/-/raven-0.8.1.tgz",
"dependencies": {
"cookie": {
@@ -1737,7 +1742,7 @@
},
"lsmod": {
"version": "0.0.3",
- "from": "lsmod@~0.0.3",
+ "from": "lsmod@>=0.0.3 <0.1.0",
"resolved": "https://registry.npmjs.org/lsmod/-/lsmod-0.0.3.tgz"
},
"stack-trace": {
@@ -1756,29 +1761,29 @@
"dependencies": {
"mersenne": {
"version": "0.0.3",
- "from": "mersenne@~0.0.3",
+ "from": "mersenne@>=0.0.3 <0.1.0",
"resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.3.tgz"
},
"statsd-parser": {
"version": "0.0.4",
- "from": "statsd-parser@~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"
}
}
},
"marked": {
- "version": "0.3.5",
- "from": "marked@^0.3.5",
- "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.5.tgz"
+ "version": "0.3.6",
+ "from": "marked@>=0.3.5 <0.4.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz"
},
"method-override": {
"version": "2.3.6",
- "from": "method-override@^2.3.3",
+ "from": "method-override@>=2.3.3 <3.0.0",
"resolved": "https://registry.npmjs.org/method-override/-/method-override-2.3.6.tgz",
"dependencies": {
"debug": {
"version": "2.2.0",
- "from": "debug@~2.2.0",
+ "from": "debug@>=2.2.0 <2.3.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
"dependencies": {
"ms": {
@@ -1790,24 +1795,24 @@
},
"methods": {
"version": "1.1.2",
- "from": "methods@~1.1.2",
+ "from": "methods@>=1.1.2 <1.2.0",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
},
"parseurl": {
"version": "1.3.1",
- "from": "parseurl@~1.3.1",
+ "from": "parseurl@>=1.3.1 <1.4.0",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz"
},
"vary": {
"version": "1.1.0",
- "from": "vary@~1.1.0",
+ "from": "vary@>=1.1.0 <1.2.0",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.0.tgz"
}
}
},
"metrics-sharelatex": {
"version": "1.3.0",
- "from": "metrics-sharelatex@git+https://github.com/sharelatex/metrics-sharelatex.git#v1.3.0",
+ "from": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.3.0",
"resolved": "git+https://github.com/sharelatex/metrics-sharelatex.git#080c4aeb696edcd5d6d86f202f2c528f0661d7a6",
"dependencies": {
"coffee-script": {
@@ -1824,19 +1829,19 @@
"dependencies": {
"encoding": {
"version": "0.1.12",
- "from": "encoding@~0.1",
+ "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.13",
- "from": "iconv-lite@~0.4.13",
+ "from": "iconv-lite@>=0.4.13 <0.5.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz"
}
}
},
"addressparser": {
"version": "0.2.1",
- "from": "addressparser@~0.2.0",
+ "from": "addressparser@>=0.2.0 <0.3.0",
"resolved": "https://registry.npmjs.org/addressparser/-/addressparser-0.2.1.tgz"
}
}
@@ -1853,7 +1858,7 @@
},
"growl": {
"version": "1.7.0",
- "from": "growl@1.7.x",
+ "from": "growl@>=1.7.0 <1.8.0",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz"
},
"jade": {
@@ -1902,29 +1907,29 @@
"dependencies": {
"minimatch": {
"version": "0.2.14",
- "from": "minimatch@~0.2.11",
+ "from": "minimatch@>=0.2.11 <0.3.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
"dependencies": {
"lru-cache": {
"version": "2.7.3",
- "from": "lru-cache@2",
+ "from": "lru-cache@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
},
"sigmund": {
"version": "1.0.1",
- "from": "sigmund@~1.0.0",
+ "from": "sigmund@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
}
}
},
"graceful-fs": {
"version": "2.0.3",
- "from": "graceful-fs@~2.0.0",
+ "from": "graceful-fs@>=2.0.0 <2.1.0",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz"
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@2",
+ "from": "inherits@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
}
}
@@ -1938,17 +1943,17 @@
"dependencies": {
"thunky": {
"version": "0.1.0",
- "from": "thunky@~0.1.0",
+ "from": "thunky@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz"
},
"readable-stream": {
"version": "1.1.14",
- "from": "readable-stream@1.1.x",
+ "from": "readable-stream@>=1.1.9 <1.2.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"dependencies": {
"core-util-is": {
"version": "1.0.2",
- "from": "core-util-is@~1.0.0",
+ "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"
},
"isarray": {
@@ -1958,12 +1963,12 @@
},
"string_decoder": {
"version": "0.10.31",
- "from": "string_decoder@~0.10.x",
+ "from": "string_decoder@>=0.10.0 <0.11.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@~2.0.1",
+ "from": "inherits@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
}
}
@@ -1975,12 +1980,12 @@
"dependencies": {
"bson": {
"version": "0.2.22",
- "from": "bson@~0.2",
+ "from": "bson@>=0.2.0 <0.3.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-0.2.22.tgz",
"dependencies": {
"nan": {
"version": "1.8.4",
- "from": "nan@~1.8",
+ "from": "nan@>=1.8.0 <1.9.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-1.8.4.tgz"
}
}
@@ -2004,37 +2009,37 @@
"dependencies": {
"buffer-shims": {
"version": "1.0.0",
- "from": "buffer-shims@^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"
},
"core-util-is": {
"version": "1.0.2",
- "from": "core-util-is@~1.0.0",
+ "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"
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@~2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"isarray": {
"version": "1.0.0",
- "from": "isarray@~1.0.0",
+ "from": "isarray@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
},
"process-nextick-args": {
"version": "1.0.7",
- "from": "process-nextick-args@~1.0.6",
+ "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"
},
"string_decoder": {
"version": "0.10.31",
- "from": "string_decoder@~0.10.x",
+ "from": "string_decoder@>=0.10.0 <0.11.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
},
"util-deprecate": {
"version": "1.0.2",
- "from": "util-deprecate@~1.0.1",
+ "from": "util-deprecate@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
}
}
@@ -2055,22 +2060,22 @@
},
"bson": {
"version": "0.3.2",
- "from": "bson@~0.3",
+ "from": "bson@>=0.3.0 <0.4.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-0.3.2.tgz",
"dependencies": {
"bson-ext": {
"version": "0.1.13",
- "from": "bson-ext@~0.1",
+ "from": "bson-ext@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/bson-ext/-/bson-ext-0.1.13.tgz",
"dependencies": {
"bindings": {
"version": "1.2.1",
- "from": "bindings@^1.2.1",
+ "from": "bindings@>=1.2.1 <2.0.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz"
},
"nan": {
"version": "2.0.9",
- "from": "nan@~2.0.9",
+ "from": "nan@>=2.0.9 <2.1.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.0.9.tgz"
}
}
@@ -2099,17 +2104,17 @@
"dependencies": {
"bson": {
"version": "0.4.23",
- "from": "bson@~0.4",
+ "from": "bson@>=0.4.0 <0.5.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-0.4.23.tgz"
},
"kerberos": {
"version": "0.0.21",
- "from": "kerberos@~0.0",
+ "from": "kerberos@>=0.0.0 <0.1.0",
"resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.21.tgz",
"dependencies": {
"nan": {
"version": "2.3.5",
- "from": "nan@~2.3",
+ "from": "nan@>=2.3.0 <2.4.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz"
}
}
@@ -2123,7 +2128,7 @@
"dependencies": {
"core-util-is": {
"version": "1.0.2",
- "from": "core-util-is@~1.0.0",
+ "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"
},
"isarray": {
@@ -2133,12 +2138,12 @@
},
"string_decoder": {
"version": "0.10.31",
- "from": "string_decoder@~0.10.x",
+ "from": "string_decoder@>=0.10.0 <0.11.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@~2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
}
}
@@ -2203,12 +2208,12 @@
},
"multer": {
"version": "0.1.8",
- "from": "multer@^0.1.8",
+ "from": "multer@>=0.1.8 <0.2.0",
"resolved": "https://registry.npmjs.org/multer/-/multer-0.1.8.tgz",
"dependencies": {
"busboy": {
"version": "0.2.13",
- "from": "busboy@~0.2.9",
+ "from": "busboy@>=0.2.9 <0.3.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.13.tgz",
"dependencies": {
"dicer": {
@@ -2225,12 +2230,12 @@
},
"readable-stream": {
"version": "1.1.14",
- "from": "readable-stream@1.1.x",
+ "from": "readable-stream@>=1.1.0 <1.2.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"dependencies": {
"core-util-is": {
"version": "1.0.2",
- "from": "core-util-is@~1.0.0",
+ "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"
},
"isarray": {
@@ -2240,12 +2245,12 @@
},
"string_decoder": {
"version": "0.10.31",
- "from": "string_decoder@~0.10.x",
+ "from": "string_decoder@>=0.10.0 <0.11.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@~2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
}
}
@@ -2254,17 +2259,17 @@
},
"mkdirp": {
"version": "0.3.5",
- "from": "mkdirp@~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",
+ "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",
+ "from": "type-is@>=1.5.2 <1.6.0",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz",
"dependencies": {
"media-typer": {
@@ -2274,12 +2279,12 @@
},
"mime-types": {
"version": "2.0.14",
- "from": "mime-types@~2.0.9",
+ "from": "mime-types@>=2.0.9 <2.1.0",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz",
"dependencies": {
"mime-db": {
"version": "1.12.0",
- "from": "mime-db@~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"
}
}
@@ -2426,27 +2431,27 @@
},
"nodemailer-sendgrid-transport": {
"version": "0.2.0",
- "from": "nodemailer-sendgrid-transport@^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",
"dependencies": {
"sendgrid": {
"version": "1.9.2",
- "from": "sendgrid@^1.8.0",
+ "from": "sendgrid@>=1.8.0 <2.0.0",
"resolved": "https://registry.npmjs.org/sendgrid/-/sendgrid-1.9.2.tgz",
"dependencies": {
"mime": {
"version": "1.3.4",
- "from": "mime@^1.2.9",
+ "from": "mime@>=1.2.9 <2.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz"
},
"lodash": {
"version": "3.10.1",
- "from": "lodash@^3.0.1 || ^2.0.0",
+ "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"
},
"smtpapi": {
"version": "1.2.0",
- "from": "smtpapi@^1.2.0",
+ "from": "smtpapi@>=1.2.0 <2.0.0",
"resolved": "https://registry.npmjs.org/smtpapi/-/smtpapi-1.2.0.tgz"
}
}
@@ -2454,14 +2459,14 @@
}
},
"nodemailer-ses-transport": {
- "version": "1.3.1",
- "from": "nodemailer-ses-transport@^1.3.0",
- "resolved": "https://registry.npmjs.org/nodemailer-ses-transport/-/nodemailer-ses-transport-1.3.1.tgz",
+ "version": "1.4.0",
+ "from": "nodemailer-ses-transport@>=1.3.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/nodemailer-ses-transport/-/nodemailer-ses-transport-1.4.0.tgz",
"dependencies": {
"aws-sdk": {
- "version": "2.4.0",
- "from": "aws-sdk@^2.3.11",
- "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.4.0.tgz",
+ "version": "2.4.14",
+ "from": "aws-sdk@>=2.4.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.4.14.tgz",
"dependencies": {
"sax": {
"version": "1.1.5",
@@ -2480,7 +2485,7 @@
"dependencies": {
"lodash": {
"version": "3.5.0",
- "from": "lodash@~3.5.0",
+ "from": "lodash@>=3.5.0 <3.6.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.5.0.tgz"
}
}
@@ -2501,16 +2506,130 @@
"dependencies": {
"wordwrap": {
"version": "0.0.3",
- "from": "wordwrap@~0.0.2",
+ "from": "wordwrap@>=0.0.2 <0.1.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz"
},
"minimist": {
"version": "0.0.10",
- "from": "minimist@~0.0.1",
+ "from": "minimist@>=0.0.1 <0.1.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz"
}
}
},
+ "pg": {
+ "version": "6.0.3",
+ "from": "pg@>=6.0.3 <7.0.0",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-6.0.3.tgz",
+ "dependencies": {
+ "buffer-writer": {
+ "version": "1.0.1",
+ "from": "buffer-writer@1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-1.0.1.tgz"
+ },
+ "packet-reader": {
+ "version": "0.2.0",
+ "from": "packet-reader@0.2.0",
+ "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-0.2.0.tgz"
+ },
+ "pg-connection-string": {
+ "version": "0.1.3",
+ "from": "pg-connection-string@0.1.3",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz"
+ },
+ "pg-pool": {
+ "version": "1.4.0",
+ "from": "pg-pool@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-1.4.0.tgz",
+ "dependencies": {
+ "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"
+ },
+ "object-assign": {
+ "version": "4.1.0",
+ "from": "object-assign@4.1.0",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz"
+ }
+ }
+ },
+ "pg-types": {
+ "version": "1.11.0",
+ "from": "pg-types@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-1.11.0.tgz",
+ "dependencies": {
+ "ap": {
+ "version": "0.2.0",
+ "from": "ap@>=0.2.0 <0.3.0",
+ "resolved": "https://registry.npmjs.org/ap/-/ap-0.2.0.tgz"
+ },
+ "postgres-array": {
+ "version": "1.0.0",
+ "from": "postgres-array@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-1.0.0.tgz"
+ },
+ "postgres-bytea": {
+ "version": "1.0.0",
+ "from": "postgres-bytea@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz"
+ },
+ "postgres-date": {
+ "version": "1.0.3",
+ "from": "postgres-date@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.3.tgz"
+ },
+ "postgres-interval": {
+ "version": "1.0.2",
+ "from": "postgres-interval@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.0.2.tgz",
+ "dependencies": {
+ "xtend": {
+ "version": "4.0.1",
+ "from": "xtend@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
+ }
+ }
+ }
+ }
+ },
+ "pgpass": {
+ "version": "0.0.6",
+ "from": "pgpass@0.0.6",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-0.0.6.tgz",
+ "dependencies": {
+ "split": {
+ "version": "1.0.0",
+ "from": "split@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/split/-/split-1.0.0.tgz",
+ "dependencies": {
+ "through": {
+ "version": "2.3.8",
+ "from": "through@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
+ }
+ }
+ }
+ }
+ },
+ "semver": {
+ "version": "4.3.2",
+ "from": "semver@4.3.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz"
+ }
+ }
+ },
+ "pg-hstore": {
+ "version": "2.3.2",
+ "from": "pg-hstore@>=2.3.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.2.tgz",
+ "dependencies": {
+ "underscore": {
+ "version": "1.8.3",
+ "from": "underscore@>=1.7.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz"
+ }
+ }
+ },
"redback": {
"version": "0.4.0",
"from": "redback@0.4.0",
@@ -2557,7 +2676,7 @@
"dependencies": {
"mkdirp": {
"version": "0.3.5",
- "from": "mkdirp@~0.3.5",
+ "from": "mkdirp@>=0.3.5 <0.4.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
}
}
@@ -2569,65 +2688,65 @@
"dependencies": {
"coffee-script": {
"version": "1.7.1",
- "from": "coffee-script@~1.7.0",
+ "from": "coffee-script@>=1.7.0 <1.8.0",
"resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz",
"dependencies": {
"mkdirp": {
"version": "0.3.5",
- "from": "mkdirp@~0.3.5",
+ "from": "mkdirp@>=0.3.5 <0.4.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
}
}
},
"chalk": {
"version": "0.5.1",
- "from": "chalk@~0.5.0",
+ "from": "chalk@>=0.5.0 <0.6.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz",
"dependencies": {
"ansi-styles": {
"version": "1.1.0",
- "from": "ansi-styles@^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"
},
"escape-string-regexp": {
"version": "1.0.5",
- "from": "escape-string-regexp@^1.0.0",
+ "from": "escape-string-regexp@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
},
"has-ansi": {
"version": "0.1.0",
- "from": "has-ansi@^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",
"dependencies": {
"ansi-regex": {
"version": "0.2.1",
- "from": "ansi-regex@^0.2.0",
+ "from": "ansi-regex@>=0.2.0 <0.3.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz"
}
}
},
"strip-ansi": {
"version": "0.3.0",
- "from": "strip-ansi@^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",
"dependencies": {
"ansi-regex": {
"version": "0.2.1",
- "from": "ansi-regex@^0.2.0",
+ "from": "ansi-regex@>=0.2.0 <0.3.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz"
}
}
},
"supports-color": {
"version": "0.2.0",
- "from": "supports-color@^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"
}
}
},
"lodash": {
"version": "2.4.2",
- "from": "lodash@~2.4.1",
+ "from": "lodash@>=2.4.1 <2.5.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
}
}
@@ -2639,22 +2758,22 @@
"dependencies": {
"hooker": {
"version": "0.2.3",
- "from": "hooker@~0.2.3",
+ "from": "hooker@>=0.2.3 <0.3.0",
"resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz"
},
"fs-extra": {
"version": "0.11.1",
- "from": "fs-extra@~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": {
"ncp": {
"version": "0.6.0",
- "from": "ncp@^0.6.0",
+ "from": "ncp@>=0.6.0 <0.7.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-0.6.0.tgz"
},
"mkdirp": {
"version": "0.5.1",
- "from": "mkdirp@^0.5.0",
+ "from": "mkdirp@>=0.5.0 <0.6.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"dependencies": {
"minimist": {
@@ -2666,50 +2785,55 @@
},
"jsonfile": {
"version": "2.3.1",
- "from": "jsonfile@^2.0.0",
+ "from": "jsonfile@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.3.1.tgz"
},
"rimraf": {
- "version": "2.5.2",
- "from": "rimraf@^2.2.8",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz",
+ "version": "2.5.4",
+ "from": "rimraf@>=2.2.8 <3.0.0",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz",
"dependencies": {
"glob": {
- "version": "7.0.3",
- "from": "glob@^7.0.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz",
+ "version": "7.0.5",
+ "from": "glob@>=7.0.5 <8.0.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz",
"dependencies": {
+ "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"
+ },
"inflight": {
"version": "1.0.5",
- "from": "inflight@^1.0.4",
+ "from": "inflight@>=1.0.4 <2.0.0",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz",
"dependencies": {
"wrappy": {
"version": "1.0.2",
- "from": "wrappy@1",
+ "from": "wrappy@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
}
}
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@2",
+ "from": "inherits@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"minimatch": {
- "version": "3.0.0",
- "from": "minimatch@2 || 3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz",
+ "version": "3.0.3",
+ "from": "minimatch@>=3.0.2 <4.0.0",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz",
"dependencies": {
"brace-expansion": {
- "version": "1.1.4",
- "from": "brace-expansion@^1.0.0",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz",
+ "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",
"dependencies": {
"balanced-match": {
- "version": "0.4.1",
- "from": "balanced-match@^0.4.1",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
+ "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"
},
"concat-map": {
"version": "0.0.1",
@@ -2722,19 +2846,19 @@
},
"once": {
"version": "1.3.3",
- "from": "once@^1.3.0",
+ "from": "once@>=1.3.0 <2.0.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz",
"dependencies": {
"wrappy": {
"version": "1.0.2",
- "from": "wrappy@1",
+ "from": "wrappy@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
}
}
},
"path-is-absolute": {
"version": "1.0.0",
- "from": "path-is-absolute@^1.0.0",
+ "from": "path-is-absolute@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz"
}
}
@@ -2757,7 +2881,7 @@
},
"growl": {
"version": "1.8.1",
- "from": "growl@1.8.x",
+ "from": "growl@>=1.8.0 <1.9.0",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.8.1.tgz"
},
"jade": {
@@ -2806,29 +2930,29 @@
"dependencies": {
"minimatch": {
"version": "0.2.14",
- "from": "minimatch@~0.2.11",
+ "from": "minimatch@>=0.2.11 <0.3.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
"dependencies": {
"lru-cache": {
"version": "2.7.3",
- "from": "lru-cache@2",
+ "from": "lru-cache@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
},
"sigmund": {
"version": "1.0.1",
- "from": "sigmund@~1.0.0",
+ "from": "sigmund@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
}
}
},
"graceful-fs": {
"version": "2.0.3",
- "from": "graceful-fs@~2.0.0",
+ "from": "graceful-fs@>=2.0.0 <2.1.0",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz"
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@2",
+ "from": "inherits@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
}
}
@@ -2847,7 +2971,7 @@
"dependencies": {
"redis": {
"version": "0.11.0",
- "from": "redis@0.11.x",
+ "from": "redis@>=0.11.0 <0.12.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-0.11.0.tgz"
},
"q": {
@@ -2881,19 +3005,19 @@
"dependencies": {
"formatio": {
"version": "1.0.2",
- "from": "formatio@~1.0",
+ "from": "formatio@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/formatio/-/formatio-1.0.2.tgz",
"dependencies": {
"samsam": {
"version": "1.1.3",
- "from": "samsam@~1.1",
+ "from": "samsam@>=1.1.0 <1.2.0",
"resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.3.tgz"
}
}
},
"util": {
"version": "0.10.3",
- "from": "util@>=0.10.3 <1",
+ "from": "util@>=0.10.3 <1.0.0",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"dependencies": {
"inherits": {
@@ -2913,58 +3037,58 @@
}
},
"request": {
- "version": "2.72.0",
- "from": "request@^2.69.0",
- "resolved": "https://registry.npmjs.org/request/-/request-2.72.0.tgz",
+ "version": "2.74.0",
+ "from": "request@>=2.69.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.74.0.tgz",
"dependencies": {
"aws-sign2": {
"version": "0.6.0",
- "from": "aws-sign2@~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.4.1",
- "from": "aws4@^1.2.1",
+ "from": "aws4@>=1.2.1 <2.0.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.4.1.tgz"
},
"bl": {
"version": "1.1.2",
- "from": "bl@~1.1.2",
+ "from": "bl@>=1.1.2 <1.2.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz",
"dependencies": {
"readable-stream": {
"version": "2.0.6",
- "from": "readable-stream@~2.0.5",
+ "from": "readable-stream@>=2.0.5 <2.1.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
"dependencies": {
"core-util-is": {
"version": "1.0.2",
- "from": "core-util-is@~1.0.0",
+ "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"
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@~2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"isarray": {
"version": "1.0.0",
- "from": "isarray@~1.0.0",
+ "from": "isarray@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
},
"process-nextick-args": {
"version": "1.0.7",
- "from": "process-nextick-args@~1.0.6",
+ "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"
},
"string_decoder": {
"version": "0.10.31",
- "from": "string_decoder@~0.10.x",
+ "from": "string_decoder@>=0.10.0 <0.11.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
},
"util-deprecate": {
"version": "1.0.2",
- "from": "util-deprecate@~1.0.1",
+ "from": "util-deprecate@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
}
}
@@ -2973,124 +3097,124 @@
},
"caseless": {
"version": "0.11.0",
- "from": "caseless@~0.11.0",
+ "from": "caseless@>=0.11.0 <0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz"
},
"combined-stream": {
"version": "1.0.5",
- "from": "combined-stream@~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",
"dependencies": {
"delayed-stream": {
"version": "1.0.0",
- "from": "delayed-stream@~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"
}
}
},
"extend": {
"version": "3.0.0",
- "from": "extend@~3.0.0",
+ "from": "extend@>=3.0.0 <3.1.0",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz"
},
"forever-agent": {
"version": "0.6.1",
- "from": "forever-agent@~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": "1.0.0-rc4",
- "from": "form-data@~1.0.0-rc3",
+ "from": "form-data@>=1.0.0-rc4 <1.1.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz",
"dependencies": {
"async": {
"version": "1.5.2",
- "from": "async@^1.5.2",
+ "from": "async@>=1.5.2 <2.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
}
}
},
"har-validator": {
"version": "2.0.6",
- "from": "har-validator@~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",
"dependencies": {
"chalk": {
"version": "1.1.3",
- "from": "chalk@^1.1.1",
+ "from": "chalk@>=1.1.1 <2.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"dependencies": {
"ansi-styles": {
"version": "2.2.1",
- "from": "ansi-styles@^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"
},
"escape-string-regexp": {
"version": "1.0.5",
- "from": "escape-string-regexp@^1.0.2",
+ "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"
},
"has-ansi": {
"version": "2.0.0",
- "from": "has-ansi@^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",
"dependencies": {
"ansi-regex": {
"version": "2.0.0",
- "from": "ansi-regex@^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"
}
}
},
"strip-ansi": {
"version": "3.0.1",
- "from": "strip-ansi@^3.0.0",
+ "from": "strip-ansi@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"dependencies": {
"ansi-regex": {
"version": "2.0.0",
- "from": "ansi-regex@^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"
}
}
},
"supports-color": {
"version": "2.0.0",
- "from": "supports-color@^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"
}
}
},
"commander": {
"version": "2.9.0",
- "from": "commander@^2.9.0",
+ "from": "commander@>=2.9.0 <3.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
"dependencies": {
"graceful-readlink": {
"version": "1.0.1",
- "from": "graceful-readlink@>= 1.0.0",
+ "from": "graceful-readlink@>=1.0.0",
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz"
}
}
},
"is-my-json-valid": {
"version": "2.13.1",
- "from": "is-my-json-valid@^2.12.4",
+ "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.13.1.tgz",
"dependencies": {
"generate-function": {
"version": "2.0.0",
- "from": "generate-function@^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",
+ "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",
"dependencies": {
"is-property": {
"version": "1.0.2",
- "from": "is-property@^1.0.0",
+ "from": "is-property@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz"
}
}
@@ -3102,19 +3226,19 @@
},
"xtend": {
"version": "4.0.1",
- "from": "xtend@^4.0.0",
+ "from": "xtend@>=4.0.0 <5.0.0",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
}
}
},
"pinkie-promise": {
"version": "2.0.1",
- "from": "pinkie-promise@^2.0.0",
+ "from": "pinkie-promise@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
"dependencies": {
"pinkie": {
"version": "2.0.4",
- "from": "pinkie@^2.0.0",
+ "from": "pinkie@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz"
}
}
@@ -3123,45 +3247,45 @@
},
"hawk": {
"version": "3.1.3",
- "from": "hawk@~3.1.3",
+ "from": "hawk@>=3.1.3 <3.2.0",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
"dependencies": {
"hoek": {
"version": "2.16.3",
- "from": "hoek@2.x.x",
+ "from": "hoek@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz"
},
"boom": {
"version": "2.10.1",
- "from": "boom@2.x.x",
+ "from": "boom@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz"
},
"cryptiles": {
"version": "2.0.5",
- "from": "cryptiles@2.x.x",
+ "from": "cryptiles@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz"
},
"sntp": {
"version": "1.0.9",
- "from": "sntp@1.x.x",
+ "from": "sntp@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz"
}
}
},
"http-signature": {
"version": "1.1.1",
- "from": "http-signature@~1.1.0",
+ "from": "http-signature@>=1.1.0 <1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
"dependencies": {
"assert-plus": {
"version": "0.2.0",
- "from": "assert-plus@^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"
},
"jsprim": {
- "version": "1.2.2",
- "from": "jsprim@^1.2.2",
- "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz",
+ "version": "1.3.0",
+ "from": "jsprim@>=1.2.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.0.tgz",
"dependencies": {
"extsprintf": {
"version": "1.0.2",
@@ -3181,48 +3305,48 @@
}
},
"sshpk": {
- "version": "1.8.3",
- "from": "sshpk@^1.7.0",
- "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.8.3.tgz",
+ "version": "1.9.2",
+ "from": "sshpk@>=1.7.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.9.2.tgz",
"dependencies": {
"asn1": {
"version": "0.2.3",
- "from": "asn1@~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": "1.0.0",
- "from": "assert-plus@^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"
},
"dashdash": {
"version": "1.14.0",
- "from": "dashdash@^1.12.0",
+ "from": "dashdash@>=1.12.0 <2.0.0",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.0.tgz"
},
"getpass": {
"version": "0.1.6",
- "from": "getpass@^0.1.1",
+ "from": "getpass@>=0.1.1 <0.2.0",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz"
},
"jsbn": {
"version": "0.1.0",
- "from": "jsbn@~0.1.0",
+ "from": "jsbn@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz"
},
"tweetnacl": {
"version": "0.13.3",
- "from": "tweetnacl@~0.13.0",
+ "from": "tweetnacl@>=0.13.0 <0.14.0",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.3.tgz"
},
"jodid25519": {
"version": "1.0.2",
- "from": "jodid25519@^1.0.0",
+ "from": "jodid25519@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz"
},
"ecc-jsbn": {
"version": "0.1.1",
- "from": "ecc-jsbn@~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"
}
}
@@ -3231,118 +3355,118 @@
},
"is-typedarray": {
"version": "1.0.0",
- "from": "is-typedarray@~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"
},
"isstream": {
"version": "0.1.2",
- "from": "isstream@~0.1.2",
+ "from": "isstream@>=0.1.2 <0.2.0",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz"
},
"json-stringify-safe": {
"version": "5.0.1",
- "from": "json-stringify-safe@~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"
},
"mime-types": {
"version": "2.1.11",
- "from": "mime-types@~2.1.7",
+ "from": "mime-types@>=2.1.7 <2.2.0",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz",
"dependencies": {
"mime-db": {
"version": "1.23.0",
- "from": "mime-db@~1.23.0",
+ "from": "mime-db@>=1.23.0 <1.24.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz"
}
}
},
"node-uuid": {
"version": "1.4.7",
- "from": "node-uuid@~1.4.7",
+ "from": "node-uuid@>=1.4.7 <1.5.0",
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz"
},
"oauth-sign": {
"version": "0.8.2",
- "from": "oauth-sign@~0.8.1",
+ "from": "oauth-sign@>=0.8.1 <0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz"
},
"qs": {
- "version": "6.1.0",
- "from": "qs@~6.1.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz"
+ "version": "6.2.1",
+ "from": "qs@>=6.2.0 <6.3.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.1.tgz"
},
"stringstream": {
"version": "0.0.5",
- "from": "stringstream@~0.0.4",
+ "from": "stringstream@>=0.0.4 <0.1.0",
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz"
},
"tough-cookie": {
- "version": "2.2.2",
- "from": "tough-cookie@~2.2.0",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz"
+ "version": "2.3.1",
+ "from": "tough-cookie@>=2.3.0 <2.4.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.1.tgz"
},
"tunnel-agent": {
"version": "0.4.3",
- "from": "tunnel-agent@~0.4.1",
+ "from": "tunnel-agent@>=0.4.1 <0.5.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz"
}
}
},
"requests": {
"version": "0.1.7",
- "from": "requests@^0.1.7",
+ "from": "requests@>=0.1.7 <0.2.0",
"resolved": "https://registry.npmjs.org/requests/-/requests-0.1.7.tgz",
"dependencies": {
"axo": {
"version": "0.0.1",
- "from": "axo@0.0.x",
+ "from": "axo@>=0.0.0 <0.1.0",
"resolved": "https://registry.npmjs.org/axo/-/axo-0.0.1.tgz"
},
"eventemitter3": {
"version": "1.1.1",
- "from": "eventemitter3@1.1.x",
+ "from": "eventemitter3@>=1.1.0 <1.2.0",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.1.1.tgz"
},
"extendible": {
"version": "0.1.1",
- "from": "extendible@0.1.x",
+ "from": "extendible@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/extendible/-/extendible-0.1.1.tgz"
},
"hang": {
"version": "1.0.0",
- "from": "hang@1.0.x",
+ "from": "hang@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/hang/-/hang-1.0.0.tgz"
},
"loads": {
"version": "0.0.4",
- "from": "loads@0.0.x",
+ "from": "loads@>=0.0.0 <0.1.0",
"resolved": "https://registry.npmjs.org/loads/-/loads-0.0.4.tgz",
"dependencies": {
"failure": {
"version": "1.1.1",
- "from": "failure@1.1.x",
+ "from": "failure@>=1.1.0 <1.2.0",
"resolved": "https://registry.npmjs.org/failure/-/failure-1.1.1.tgz"
},
"one-time": {
"version": "0.0.4",
- "from": "one-time@0.0.x",
+ "from": "one-time@>=0.0.0 <0.1.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz"
},
"xhr-response": {
"version": "1.0.1",
- "from": "xhr-response@1.0.x",
+ "from": "xhr-response@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/xhr-response/-/xhr-response-1.0.1.tgz"
},
"xhr-status": {
"version": "1.0.0",
- "from": "xhr-status@1.0.x",
+ "from": "xhr-status@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/xhr-status/-/xhr-status-1.0.0.tgz"
}
}
},
"xhr-send": {
"version": "1.0.0",
- "from": "xhr-send@1.0.x",
+ "from": "xhr-send@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/xhr-send/-/xhr-send-1.0.0.tgz"
}
}
@@ -3357,9 +3481,117 @@
"from": "sanitizer@0.1.1",
"resolved": "https://registry.npmjs.org/sanitizer/-/sanitizer-0.1.1.tgz"
},
+ "sequelize": {
+ "version": "3.23.6",
+ "from": "sequelize@>=3.2.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-3.23.6.tgz",
+ "dependencies": {
+ "bluebird": {
+ "version": "3.4.1",
+ "from": "bluebird@>=3.3.4 <4.0.0",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.1.tgz"
+ },
+ "depd": {
+ "version": "1.1.0",
+ "from": "depd@>=1.1.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.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"
+ },
+ "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"
+ },
+ "inflection": {
+ "version": "1.10.0",
+ "from": "inflection@>=1.6.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.10.0.tgz"
+ },
+ "lodash": {
+ "version": "4.12.0",
+ "from": "lodash@4.12.0",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.12.0.tgz"
+ },
+ "moment": {
+ "version": "2.14.1",
+ "from": "moment@>=2.13.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.14.1.tgz"
+ },
+ "moment-timezone": {
+ "version": "0.5.5",
+ "from": "moment-timezone@>=0.5.4 <0.6.0",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.5.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"
+ },
+ "retry-as-promised": {
+ "version": "2.0.1",
+ "from": "retry-as-promised@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-2.0.1.tgz",
+ "dependencies": {
+ "debug": {
+ "version": "2.2.0",
+ "from": "debug@>=2.2.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+ "dependencies": {
+ "ms": {
+ "version": "0.7.1",
+ "from": "ms@0.7.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+ }
+ }
+ }
+ }
+ },
+ "semver": {
+ "version": "5.3.0",
+ "from": "semver@>=5.0.1 <6.0.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz"
+ },
+ "shimmer": {
+ "version": "1.1.0",
+ "from": "shimmer@1.1.0",
+ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.1.0.tgz"
+ },
+ "terraformer-wkt-parser": {
+ "version": "1.1.0",
+ "from": "terraformer-wkt-parser@>=1.1.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.1.0.tgz",
+ "dependencies": {
+ "terraformer": {
+ "version": "1.0.5",
+ "from": "terraformer@>=1.0.5 <1.1.0",
+ "resolved": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.5.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"
+ },
+ "validator": {
+ "version": "5.5.0",
+ "from": "validator@>=5.2.0 <6.0.0",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-5.5.0.tgz"
+ },
+ "wkx": {
+ "version": "0.2.0",
+ "from": "wkx@0.2.0",
+ "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.2.0.tgz"
+ }
+ }
+ },
"settings-sharelatex": {
"version": "1.0.0",
- "from": "settings-sharelatex@git+https://github.com/sharelatex/settings-sharelatex.git#v1.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": {
@@ -3371,17 +3603,17 @@
},
"sixpack-client": {
"version": "1.0.0",
- "from": "sixpack-client@^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"
},
"temp": {
"version": "0.8.3",
- "from": "temp@^0.8.3",
+ "from": "temp@>=0.8.3 <0.9.0",
"resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz",
"dependencies": {
"os-tmpdir": {
"version": "1.0.1",
- "from": "os-tmpdir@^1.0.0",
+ "from": "os-tmpdir@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.1.tgz"
}
}
@@ -3393,22 +3625,22 @@
},
"v8-profiler": {
"version": "5.6.5",
- "from": "v8-profiler@^5.2.3",
+ "from": "v8-profiler@>=5.2.3 <6.0.0",
"resolved": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.6.5.tgz",
"dependencies": {
"nan": {
- "version": "2.3.5",
- "from": "nan@^2.3.2",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz"
+ "version": "2.4.0",
+ "from": "nan@>=2.3.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz"
},
"node-pre-gyp": {
- "version": "0.6.28",
- "from": "node-pre-gyp@^0.6.5",
- "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.28.tgz",
+ "version": "0.6.29",
+ "from": "node-pre-gyp@>=0.6.5 <0.7.0",
+ "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.29.tgz",
"dependencies": {
"mkdirp": {
"version": "0.5.1",
- "from": "mkdirp@~0.5.0",
+ "from": "mkdirp@>=0.5.0 <0.6.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"dependencies": {
"minimist": {
@@ -3420,228 +3652,240 @@
},
"nopt": {
"version": "3.0.6",
- "from": "nopt@~3.0.1",
+ "from": "nopt@>=3.0.1 <3.1.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
"dependencies": {
"abbrev": {
- "version": "1.0.7",
- "from": "abbrev@1",
- "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz"
+ "version": "1.0.9",
+ "from": "abbrev@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz"
}
}
},
"npmlog": {
- "version": "2.0.4",
- "from": "npmlog@~2.0.0",
- "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-2.0.4.tgz",
+ "version": "3.1.2",
+ "from": "npmlog@>=3.1.2 <3.2.0",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-3.1.2.tgz",
"dependencies": {
- "ansi": {
- "version": "0.3.1",
- "from": "ansi@~0.3.1",
- "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz"
- },
"are-we-there-yet": {
"version": "1.1.2",
- "from": "are-we-there-yet@~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": {
"delegates": {
"version": "1.0.0",
- "from": "delegates@^1.0.0",
+ "from": "delegates@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz"
},
"readable-stream": {
"version": "2.1.4",
- "from": "readable-stream@^2.0.0 || ^1.1.13",
+ "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.1.4.tgz",
"dependencies": {
"buffer-shims": {
"version": "1.0.0",
- "from": "buffer-shims@^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"
},
"core-util-is": {
"version": "1.0.2",
- "from": "core-util-is@~1.0.0",
+ "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"
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@~2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"isarray": {
"version": "1.0.0",
- "from": "isarray@~1.0.0",
+ "from": "isarray@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
},
"process-nextick-args": {
"version": "1.0.7",
- "from": "process-nextick-args@~1.0.6",
+ "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"
},
"string_decoder": {
"version": "0.10.31",
- "from": "string_decoder@~0.10.x",
+ "from": "string_decoder@>=0.10.0 <0.11.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
},
"util-deprecate": {
"version": "1.0.2",
- "from": "util-deprecate@~1.0.1",
+ "from": "util-deprecate@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.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"
+ },
"gauge": {
- "version": "1.2.7",
- "from": "gauge@~1.2.5",
- "resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz",
+ "version": "2.6.0",
+ "from": "gauge@>=2.6.0 <2.7.0",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.6.0.tgz",
"dependencies": {
+ "aproba": {
+ "version": "1.0.4",
+ "from": "aproba@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.0.4.tgz"
+ },
+ "has-color": {
+ "version": "0.1.7",
+ "from": "has-color@>=0.1.7 <0.2.0",
+ "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz"
+ },
"has-unicode": {
- "version": "2.0.0",
- "from": "has-unicode@^2.0.0",
- "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.0.tgz"
+ "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"
},
- "lodash.pad": {
- "version": "4.4.0",
- "from": "lodash.pad@^4.1.0",
- "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.4.0.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"
+ },
+ "signal-exit": {
+ "version": "3.0.0",
+ "from": "signal-exit@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.0.tgz"
+ },
+ "string-width": {
+ "version": "1.0.1",
+ "from": "string-width@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.1.tgz",
"dependencies": {
- "lodash._baseslice": {
- "version": "4.0.0",
- "from": "lodash._baseslice@~4.0.0",
- "resolved": "https://registry.npmjs.org/lodash._baseslice/-/lodash._baseslice-4.0.0.tgz"
+ "code-point-at": {
+ "version": "1.0.0",
+ "from": "code-point-at@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.0.tgz",
+ "dependencies": {
+ "number-is-nan": {
+ "version": "1.0.0",
+ "from": "number-is-nan@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz"
+ }
+ }
},
- "lodash._basetostring": {
- "version": "4.12.0",
- "from": "lodash._basetostring@~4.12.0",
- "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz"
- },
- "lodash.tostring": {
- "version": "4.1.3",
- "from": "lodash.tostring@^4.0.0",
- "resolved": "https://registry.npmjs.org/lodash.tostring/-/lodash.tostring-4.1.3.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",
+ "dependencies": {
+ "number-is-nan": {
+ "version": "1.0.0",
+ "from": "number-is-nan@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz"
+ }
+ }
}
}
},
- "lodash.padend": {
- "version": "4.5.0",
- "from": "lodash.padend@^4.1.0",
- "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.5.0.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",
"dependencies": {
- "lodash._baseslice": {
- "version": "4.0.0",
- "from": "lodash._baseslice@~4.0.0",
- "resolved": "https://registry.npmjs.org/lodash._baseslice/-/lodash._baseslice-4.0.0.tgz"
- },
- "lodash._basetostring": {
- "version": "4.12.0",
- "from": "lodash._basetostring@~4.12.0",
- "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz"
- },
- "lodash.tostring": {
- "version": "4.1.3",
- "from": "lodash.tostring@^4.0.0",
- "resolved": "https://registry.npmjs.org/lodash.tostring/-/lodash.tostring-4.1.3.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"
}
}
},
- "lodash.padstart": {
- "version": "4.5.0",
- "from": "lodash.padstart@^4.1.0",
- "resolved": "https://registry.npmjs.org/lodash.padstart/-/lodash.padstart-4.5.0.tgz",
- "dependencies": {
- "lodash._baseslice": {
- "version": "4.0.0",
- "from": "lodash._baseslice@~4.0.0",
- "resolved": "https://registry.npmjs.org/lodash._baseslice/-/lodash._baseslice-4.0.0.tgz"
- },
- "lodash._basetostring": {
- "version": "4.12.0",
- "from": "lodash._basetostring@~4.12.0",
- "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz"
- },
- "lodash.tostring": {
- "version": "4.1.3",
- "from": "lodash.tostring@^4.0.0",
- "resolved": "https://registry.npmjs.org/lodash.tostring/-/lodash.tostring-4.1.3.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"
}
}
+ },
+ "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"
}
}
},
"rc": {
"version": "1.1.6",
- "from": "rc@~1.1.0",
+ "from": "rc@>=1.1.0 <1.2.0",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz",
"dependencies": {
"deep-extend": {
"version": "0.4.1",
- "from": "deep-extend@~0.4.0",
+ "from": "deep-extend@>=0.4.0 <0.5.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz"
},
"ini": {
"version": "1.3.4",
- "from": "ini@~1.3.0",
+ "from": "ini@>=1.3.0 <1.4.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz"
},
"minimist": {
"version": "1.2.0",
- "from": "minimist@^1.2.0",
+ "from": "minimist@>=1.2.0 <2.0.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
},
"strip-json-comments": {
"version": "1.0.4",
- "from": "strip-json-comments@~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"
}
}
},
"rimraf": {
- "version": "2.5.2",
- "from": "rimraf@~2.5.0",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz",
+ "version": "2.5.4",
+ "from": "rimraf@>=2.5.0 <2.6.0",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz",
"dependencies": {
"glob": {
- "version": "7.0.3",
- "from": "glob@^7.0.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz",
+ "version": "7.0.5",
+ "from": "glob@>=7.0.5 <8.0.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz",
"dependencies": {
+ "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"
+ },
"inflight": {
"version": "1.0.5",
- "from": "inflight@^1.0.4",
+ "from": "inflight@>=1.0.4 <2.0.0",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz",
"dependencies": {
"wrappy": {
"version": "1.0.2",
- "from": "wrappy@1",
+ "from": "wrappy@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
}
}
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@2",
+ "from": "inherits@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"minimatch": {
- "version": "3.0.0",
- "from": "minimatch@2 || 3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz",
+ "version": "3.0.3",
+ "from": "minimatch@>=3.0.2 <4.0.0",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz",
"dependencies": {
"brace-expansion": {
- "version": "1.1.4",
- "from": "brace-expansion@^1.0.0",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz",
+ "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",
"dependencies": {
"balanced-match": {
- "version": "0.4.1",
- "from": "balanced-match@^0.4.1",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
+ "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"
},
"concat-map": {
"version": "0.0.1",
@@ -3654,19 +3898,19 @@
},
"once": {
"version": "1.3.3",
- "from": "once@^1.3.0",
+ "from": "once@>=1.3.0 <2.0.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz",
"dependencies": {
"wrappy": {
"version": "1.0.2",
- "from": "wrappy@1",
+ "from": "wrappy@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
}
}
},
"path-is-absolute": {
"version": "1.0.0",
- "from": "path-is-absolute@^1.0.0",
+ "from": "path-is-absolute@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz"
}
}
@@ -3674,13 +3918,13 @@
}
},
"semver": {
- "version": "5.1.0",
- "from": "semver@~5.1.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz"
+ "version": "5.2.0",
+ "from": "semver@>=5.2.0 <5.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.2.0.tgz"
},
"tar": {
"version": "2.2.1",
- "from": "tar@~2.2.0",
+ "from": "tar@>=2.2.0 <2.3.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
"dependencies": {
"block-stream": {
@@ -3689,32 +3933,32 @@
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz"
},
"fstream": {
- "version": "1.0.9",
- "from": "fstream@^1.0.2",
- "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.9.tgz",
+ "version": "1.0.10",
+ "from": "fstream@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.10.tgz",
"dependencies": {
"graceful-fs": {
- "version": "4.1.4",
- "from": "graceful-fs@^4.1.2",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.4.tgz"
+ "version": "4.1.5",
+ "from": "graceful-fs@>=4.1.2 <5.0.0",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.5.tgz"
}
}
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@~2.0.1",
+ "from": "inherits@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
}
}
},
"tar-pack": {
- "version": "3.1.3",
- "from": "tar-pack@~3.1.0",
- "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.1.3.tgz",
+ "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",
+ "from": "debug@>=2.2.0 <2.3.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
"dependencies": {
"ms": {
@@ -3725,46 +3969,46 @@
}
},
"fstream": {
- "version": "1.0.9",
- "from": "fstream@~1.0.8",
- "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.9.tgz",
+ "version": "1.0.10",
+ "from": "fstream@>=1.0.10 <1.1.0",
+ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.10.tgz",
"dependencies": {
"graceful-fs": {
- "version": "4.1.4",
- "from": "graceful-fs@^4.1.2",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.4.tgz"
+ "version": "4.1.5",
+ "from": "graceful-fs@>=4.1.2 <5.0.0",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.5.tgz"
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@~2.0.0",
+ "from": "inherits@>=2.0.0 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
}
}
},
"fstream-ignore": {
"version": "1.0.5",
- "from": "fstream-ignore@~1.0.3",
+ "from": "fstream-ignore@>=1.0.5 <1.1.0",
"resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz",
"dependencies": {
"inherits": {
"version": "2.0.1",
- "from": "inherits@2",
+ "from": "inherits@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"minimatch": {
- "version": "3.0.0",
- "from": "minimatch@^3.0.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz",
+ "version": "3.0.3",
+ "from": "minimatch@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz",
"dependencies": {
"brace-expansion": {
- "version": "1.1.4",
- "from": "brace-expansion@^1.0.0",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz",
+ "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",
"dependencies": {
"balanced-match": {
- "version": "0.4.1",
- "from": "balanced-match@^0.4.1",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
+ "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"
},
"concat-map": {
"version": "0.0.1",
@@ -3779,56 +4023,61 @@
},
"once": {
"version": "1.3.3",
- "from": "once@~1.3.3",
+ "from": "once@>=1.3.3 <1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz",
"dependencies": {
"wrappy": {
"version": "1.0.2",
- "from": "wrappy@1",
+ "from": "wrappy@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
}
}
},
"readable-stream": {
- "version": "2.0.6",
- "from": "readable-stream@~2.0.4",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+ "version": "2.1.4",
+ "from": "readable-stream@>=2.1.4 <2.2.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz",
"dependencies": {
+ "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"
+ },
"core-util-is": {
"version": "1.0.2",
- "from": "core-util-is@~1.0.0",
+ "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"
},
"inherits": {
"version": "2.0.1",
- "from": "inherits@~2.0.1",
+ "from": "inherits@>=2.0.1 <2.1.0",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"isarray": {
"version": "1.0.0",
- "from": "isarray@~1.0.0",
+ "from": "isarray@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
},
"process-nextick-args": {
"version": "1.0.7",
- "from": "process-nextick-args@~1.0.6",
+ "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"
},
"string_decoder": {
"version": "0.10.31",
- "from": "string_decoder@~0.10.x",
+ "from": "string_decoder@>=0.10.0 <0.11.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
},
"util-deprecate": {
"version": "1.0.2",
- "from": "util-deprecate@~1.0.1",
+ "from": "util-deprecate@>=1.0.1 <1.1.0",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
}
}
},
"uid-number": {
"version": "0.0.6",
- "from": "uid-number@~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"
}
}
From 0270d34d0f3b3a22d0037d94cac86b5c0d158179 Mon Sep 17 00:00:00 2001
From: James Allen
Date: Thu, 11 Aug 2016 10:18:58 +0100
Subject: [PATCH 131/378] Use JSONB not JSON column type
---
.../web/app/coffee/Features/Analytics/AnalyticsManager.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee
index df1ba0be7c..f906118b16 100644
--- a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee
+++ b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee
@@ -21,7 +21,7 @@ else
Event = sequelize.define("Event", {
user_id: Sequelize.STRING,
event: Sequelize.STRING,
- segmentation: Sequelize.JSON
+ segmentation: Sequelize.JSONB
})
module.exports =
From 6a210978fe312ce9720e4c937cf998a01beb87c3 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Thu, 11 Aug 2016 12:29:58 +0100
Subject: [PATCH 132/378] Track registration events.
---
.../web/app/coffee/Features/User/UserRegistrationHandler.coffee | 2 ++
1 file changed, 2 insertions(+)
diff --git a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee
index 75a9debba4..f5db2e54a1 100644
--- a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee
+++ b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee
@@ -8,6 +8,7 @@ logger = require("logger-sharelatex")
crypto = require("crypto")
EmailHandler = require("../Email/EmailHandler")
OneTimeTokenHandler = require "../Security/OneTimeTokenHandler"
+Analytics = require "../Analytics/AnalyticsManager"
settings = require "settings-sharelatex"
module.exports = UserRegistrationHandler =
@@ -62,6 +63,7 @@ module.exports = UserRegistrationHandler =
cb() #this can be slow, just fire it off
], (err)->
logger.log user: user, "registered"
+ Analytics.recordEvent user._id, "user-registered"
callback(err, user)
registerNewUserAndSendActivationEmail: (email, callback = (error, user, setNewPasswordUrl) ->) ->
From a862592138b70161e8f6209700974c30432cc5b1 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 11 Aug 2016 13:43:33 +0100
Subject: [PATCH 133/378] added syntax check to beta page, removed mendeley
---
services/web/app/views/beta_program/opt_in.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/beta_program/opt_in.jade b/services/web/app/views/beta_program/opt_in.jade
index c6411cb211..f54766f30d 100644
--- a/services/web/app/views/beta_program/opt_in.jade
+++ b/services/web/app/views/beta_program/opt_in.jade
@@ -22,7 +22,7 @@ block content
ul.list-unstyled.text-center
li
i.fa.fa-fw.fa-book
- | #{translate("mendeley_integration")}
+ | #{translate("syntax_checking")}
.row.text-centered
.col-md-12
if user.betaProgram
From 9a399d3dd3118b6fd173b6f64df2b70eeb833eb6 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 11 Aug 2016 13:55:47 +0100
Subject: [PATCH 134/378] move "run syntax check" compile option out of beta
---
services/web/app/views/project/editor/pdf.jade | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade
index 844f0f51dc..492f75f582 100644
--- a/services/web/app/views/project/editor/pdf.jade
+++ b/services/web/app/views/project/editor/pdf.jade
@@ -36,13 +36,11 @@ div.full-size.pdf(ng-controller="PdfController")
i.fa.fa-fw(ng-class="{'fa-check': draft}")
| #{translate("fast")}
span.subdued [draft]
- if user.betaProgram
- li.dropdown-header #{translate("file_checks")}
- li
- a(href, ng-click="recompile({check:true})")
- i.fa.fa-fw()
- | #{translate("run_syntax_check")}
- span.beta-feature-badge
+ li.dropdown-header #{translate("file_checks")}
+ li
+ a(href, ng-click="recompile({check:true})")
+ i.fa.fa-fw()
+ | #{translate("run_syntax_check")}
a(
href
ng-click="stop()"
From 826295167f4ad7b52e688116771c43dec20d5418 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 11 Aug 2016 14:04:11 +0100
Subject: [PATCH 135/378] Mark Notification as read by key alone
---
.../Notifications/NotificationsBuilder.coffee | 4 +++-
.../Notifications/NotificationsHandler.coffee | 10 ++++++++++
.../NotificationsHandlerTests.coffee | 15 ++++++++++++++-
3 files changed, 27 insertions(+), 2 deletions(-)
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
index da1a6c4957..b9f7500809 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
@@ -3,6 +3,8 @@ NotificationsHandler = require("./NotificationsHandler")
module.exports =
+ # Note: notification keys should be url-safe
+
groupPlan: (user, licence)->
key : "join-sub-#{licence.subscription_id}"
create: (callback = ->)->
@@ -26,4 +28,4 @@ module.exports =
logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user"
NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, callback
read: (callback=()->) ->
- NotificationsHandler.markAsReadWithKey user._id, @key, callback
+ NotificationsHandler.markAsReadByKeyOnly @key, callback
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
index a7cf6a4672..9a7af1bdd1 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
@@ -61,3 +61,13 @@ module.exports =
timeout:oneSecond
logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api"
makeRequest opts, callback
+
+ # removes notification by key, without regard for user_id,
+ # should not be exposed to user via ui/router
+ markAsReadByKeyOnly: (key, callback)->
+ opts =
+ uri: "#{settings.apis.notifications?.url}/notification/key/#{key}"
+ method: "DELETE"
+ timeout: oneSecond
+ logger.log {key:key}, "sending mark notification as read with key-only to notifications api"
+ makeRequest opts, callback
diff --git a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
index cfca6dfebe..ac1d57d1f5 100644
--- a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
@@ -68,4 +68,17 @@ describe 'NotificationsHandler', ->
args.timeout.should.equal 1000
expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts}
assert.deepEqual(args.json, expectedJson)
- done()
\ No newline at end of file
+ done()
+
+ describe "markAsReadByKeyOnly", ->
+ beforeEach ->
+ @key = "some key here"
+
+ it 'should send a delete request when a delete has been received to mark a notification', (done)->
+ @handler.markAsReadByKeyOnly @key, =>
+ opts =
+ uri: "#{notificationUrl}/notification/key/#{@key}"
+ timeout:1000
+ method: "DELETE"
+ @request.calledWith(opts).should.equal true
+ done()
From 9bf9df9a4a822cdc67cf3f63962adb3f2df74b14 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Thu, 11 Aug 2016 14:09:45 +0100
Subject: [PATCH 136/378] Track login events.
---
.../Features/Authentication/AuthenticationController.coffee | 2 ++
1 file changed, 2 insertions(+)
diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index d75bef5207..4972144737 100644
--- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -10,6 +10,7 @@ Settings = require "settings-sharelatex"
basicAuth = require('basic-auth-connect')
UserHandler = require("../User/UserHandler")
UserSessionsManager = require("../User/UserSessionsManager")
+Analytics = require "../Analytics/AnalyticsManager"
module.exports = AuthenticationController =
login: (req, res, next = (error) ->) ->
@@ -37,6 +38,7 @@ module.exports = AuthenticationController =
return next(error) if error?
req.session.justLoggedIn = true
logger.log email: email, user_id: user._id.toString(), "successful log in"
+ Analytics.recordEvent user._id, "user-logged-in"
res.json redir: redir
else
AuthenticationController._recordFailedLogin()
From 228de5332e625c42347452cacdede5f2673edad1 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Thu, 11 Aug 2016 14:09:57 +0100
Subject: [PATCH 137/378] Unit test tracking code.
---
.../Authentication/AuthenticationControllerTests.coffee | 6 ++++++
.../coffee/User/UserRegistrationHandlerTests.coffee | 8 ++++++++
2 files changed, 14 insertions(+)
diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
index 4e9b248f25..86c5e5840f 100644
--- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
@@ -19,6 +19,7 @@ describe "AuthenticationController", ->
"../../infrastructure/Metrics": @Metrics = { inc: sinon.stub() }
"../Security/LoginRateLimiter": @LoginRateLimiter = { processLoginRequest:sinon.stub(), recordSuccessfulLogin:sinon.stub() }
"../User/UserHandler": @UserHandler = {setupLoginData:sinon.stub()}
+ "../Analytics/AnalyticsManager": @AnalyticsManager = { recordEvent: sinon.stub() }
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
"settings-sharelatex": {}
"../User/UserSessionsManager": @UserSessionsManager =
@@ -100,6 +101,11 @@ describe "AuthenticationController", ->
.calledWith(email: @email.toLowerCase(), user_id: @user._id.toString(), "successful log in")
.should.equal true
+ it "should track the login event", ->
+ @AnalyticsManager.recordEvent
+ .calledWith(@user._id, "user-logged-in")
+ .should.equal true
+
describe 'when the user is not authenticated', ->
beforeEach ->
diff --git a/services/web/test/UnitTests/coffee/User/UserRegistrationHandlerTests.coffee b/services/web/test/UnitTests/coffee/User/UserRegistrationHandlerTests.coffee
index ec220e27f8..d0b96da2de 100644
--- a/services/web/test/UnitTests/coffee/User/UserRegistrationHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/User/UserRegistrationHandlerTests.coffee
@@ -33,6 +33,7 @@ describe "UserRegistrationHandler", ->
"crypto": @crypto = {}
"../Email/EmailHandler": @EmailHandler
"../Security/OneTimeTokenHandler": @OneTimeTokenHandler
+ "../Analytics/AnalyticsManager": @AnalyticsManager = { recordEvent: sinon.stub() }
"settings-sharelatex": @settings = {siteUrl: "http://sl.example.com"}
@passingRequest = {email:"something@email.com", password:"123"}
@@ -132,6 +133,13 @@ describe "UserRegistrationHandler", ->
@NewsLetterManager.subscribe.calledWith(@user).should.equal true
done()
+ it "should track the registration event", (done)->
+ @handler.registerNewUser @passingRequest, (err)=>
+ @AnalyticsManager.recordEvent
+ .calledWith(@user._id, "user-registered")
+ .should.equal true
+ done()
+
it "should call the ReferalAllocator", (done)->
done()
From ce039f8cd39fb08eb2ff24f965e4c9d4529efa5b Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 11 Aug 2016 14:17:01 +0100
Subject: [PATCH 138/378] Remove the email when user id is added to project
---
.../Features/Collaborators/CollaboratorsHandler.coffee | 5 -----
.../coffee/Collaborators/CollaboratorsHandlerTests.coffee | 2 --
2 files changed, 7 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
index 822201a83e..e974698b18 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee
@@ -130,11 +130,6 @@ module.exports = CollaboratorsHandler =
else
return callback(new Error("unknown privilegeLevel: #{privilegeLevel}"))
- # Do these in the background
- UserGetter.getUser user_id, {email: 1}, (error, user) ->
- if error?
- logger.error {err: error, project_id, user_id}, "error getting user while adding to project"
- CollaboratorsEmailHandler.notifyUserOfProjectShare project_id, user.email
ContactManager.addContact adding_user_id, user_id
Project.update { _id: project_id }, { $addToSet: level }, (error) ->
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee
index efbf30487e..15c9d7a303 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee
@@ -180,8 +180,6 @@ describe "CollaboratorsHandler", ->
@Project.findOne = sinon.stub().callsArgWith(2, null, @project = {})
@ProjectEntityHandler.flushProjectToThirdPartyDataStore = sinon.stub().callsArg(1)
@CollaboratorHandler.addEmailToProject = sinon.stub().callsArgWith(4, null, @user_id)
- @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user = { _id: @user_id, email: @email })
- @CollaboratorsEmailHandler.notifyUserOfProjectShare = sinon.stub()
@ContactManager.addContact = sinon.stub()
describe "as readOnly", ->
From 276241495b9a66311f7d535388991e942e295c1b Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 11 Aug 2016 14:23:25 +0100
Subject: [PATCH 139/378] Fix tests
---
.../coffee/Collaborators/CollaboratorsHandlerTests.coffee | 5 -----
1 file changed, 5 deletions(-)
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee
index 15c9d7a303..ff6ca6de67 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee
@@ -200,11 +200,6 @@ describe "CollaboratorsHandler", ->
.calledWith(@project_id)
.should.equal true
- it "should send an email to the shared-with user", ->
- @CollaboratorsEmailHandler.notifyUserOfProjectShare
- .calledWith(@project_id, @email)
- .should.equal true
-
it "should add the user as a contact for the adding user", ->
@ContactManager.addContact
.calledWith(@adding_user_id, @user_id)
From a9042ff3245129a28b933d9c949292235759ce5b Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 11 Aug 2016 15:24:35 +0100
Subject: [PATCH 140/378] Enable enter key on share dialog button
---
services/web/app/views/project/editor/share.jade | 1 +
1 file changed, 1 insertion(+)
diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade
index 147803786d..049623c09f 100644
--- a/services/web/app/views/project/editor/share.jade
+++ b/services/web/app/views/project/editor/share.jade
@@ -95,6 +95,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
button.btn.btn-info(
type="submit"
ng-mousedown="addMembers()"
+ ng-keyup="$event.keyCode == 13 ? addMembers() : null"
) #{translate("share")}
div(ng-hide="canAddCollaborators")
p.text-center #{translate("need_to_upgrade_for_more_collabs")}. Also:
From daa1d80865ed4a412cfd8b61a82abf9dfa3d0288 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 11 Aug 2016 16:46:12 +0100
Subject: [PATCH 141/378] add extra delay to gotoLine event
---
services/web/public/coffee/ide/editor/EditorManager.coffee | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee
index 64cbe1d884..be15958c37 100644
--- a/services/web/public/coffee/ide/editor/EditorManager.coffee
+++ b/services/web/public/coffee/ide/editor/EditorManager.coffee
@@ -46,8 +46,11 @@ define [
done = () =>
if options.gotoLine?
- @$scope.$broadcast "editor:gotoLine", options.gotoLine, options.gotoColumn
-
+ # allow Ace to display document before moving, delay until next tick
+ setTimeout () =>
+ @$scope.$broadcast "editor:gotoLine", options.gotoLine, options.gotoColumn
+ ,0
+
if doc.id == @$scope.editor.open_doc_id and !options.forceReopen
@$scope.$apply () =>
done()
From 7863b7cab67d8a6947b591e6c9988fcce854a726 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 11 Aug 2016 16:46:30 +0100
Subject: [PATCH 142/378] when calling gotoLine also scrollToLine to put line
in view
---
.../aceEditor/cursor-position/CursorPositionManager.coffee | 1 +
1 file changed, 1 insertion(+)
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 6e238122e1..8a38c317be 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
@@ -55,4 +55,5 @@ define [], () ->
gotoLine: (line, column) ->
@editor.gotoLine(line, column)
+ @editor.scrollToLine(line,true,true)
@editor.focus()
From a7bc8bffe06f746a7bbfb5512bb81a89df7d7869 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 12 Aug 2016 09:59:25 +0100
Subject: [PATCH 143/378] Update `markAsReadByKeyOnly` url.
---
.../Notifications/NotificationsHandler.coffee | 12 ++++++------
.../Notifications/NotificationsHandlerTests.coffee | 2 +-
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
index 9a7af1bdd1..55ed4e8a9a 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
@@ -10,10 +10,10 @@ makeRequest = (opts, callback)->
else
request(opts, callback)
-module.exports =
+module.exports =
getUserNotifications: (user_id, callback)->
- opts =
+ opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
json: true
timeout: oneSecond
@@ -30,7 +30,7 @@ module.exports =
callback(null, unreadNotifications)
createNotification: (user_id, key, templateKey, messageOpts, callback)->
- opts =
+ opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
timeout: oneSecond
method:"POST"
@@ -43,7 +43,7 @@ module.exports =
makeRequest opts, callback
markAsReadWithKey: (user_id, key, callback)->
- opts =
+ opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
method: "DELETE"
timeout: oneSecond
@@ -52,7 +52,7 @@ module.exports =
}
logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api"
makeRequest opts, callback
-
+
markAsRead: (user_id, notification_id, callback)->
opts =
@@ -66,7 +66,7 @@ module.exports =
# should not be exposed to user via ui/router
markAsReadByKeyOnly: (key, callback)->
opts =
- uri: "#{settings.apis.notifications?.url}/notification/key/#{key}"
+ uri: "#{settings.apis.notifications?.url}/key/#{key}"
method: "DELETE"
timeout: oneSecond
logger.log {key:key}, "sending mark notification as read with key-only to notifications api"
diff --git a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
index ac1d57d1f5..3554836fa5 100644
--- a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
@@ -77,7 +77,7 @@ describe 'NotificationsHandler', ->
it 'should send a delete request when a delete has been received to mark a notification', (done)->
@handler.markAsReadByKeyOnly @key, =>
opts =
- uri: "#{notificationUrl}/notification/key/#{@key}"
+ uri: "#{notificationUrl}/key/#{@key}"
timeout:1000
method: "DELETE"
@request.calledWith(opts).should.equal true
From d547bff4e59455f8101fe5533120434e9831cf3c Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 12 Aug 2016 11:25:03 +0100
Subject: [PATCH 144/378] Blur the `resend` button after response
---
services/web/app/views/project/editor/share.jade | 2 +-
.../ide/share/controllers/ShareProjectModalController.coffee | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade
index 049623c09f..fd13ccb240 100644
--- a/services/web/app/views/project/editor/share.jade
+++ b/services/web/app/views/project/editor/share.jade
@@ -47,7 +47,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
.col-xs-8 {{ invite.email }}
div.small
| #{translate("invite_not_accepted")}.
- a(href="#", ng-click="resendInvite(invite)") #{translate("resend")}
+ a(href="#", ng-click="resendInvite(invite, $event)") #{translate("resend")}
.col-xs-3.text-left
// todo: get invite privileges
span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")}
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index 7cbfcd6a57..9fc7417912 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -143,16 +143,18 @@ define [
$scope.state.inflight = false
$scope.state.error = "Sorry, something went wrong :("
- $scope.resendInvite = (invite) ->
+ $scope.resendInvite = (invite, event) ->
$scope.state.error = null
$scope.state.inflight = true
projectInvites
.resendInvite(invite._id)
.success () ->
$scope.state.inflight = false
+ event.target.blur()
.error () ->
$scope.state.inflight = false
$scope.state.error = "Sorry, something went wrong resending the invite :("
+ event.target.blur()
$scope.openMakePublicModal = () ->
$modal.open {
From e53394919f6dbee905838c7402ce8a3fa0919ded Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 12 Aug 2016 14:40:59 +0100
Subject: [PATCH 145/378] Rework how invite expiry functions.
---
.../Notifications/NotificationsBuilder.coffee | 4 ++--
.../Notifications/NotificationsHandler.coffee | 15 +++++++++------
.../app/coffee/models/ProjectInvite.coffee | 13 ++++++++++---
.../NotificationsHandlerTests.coffee | 19 ++++++++++++++++++-
4 files changed, 39 insertions(+), 12 deletions(-)
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
index b9f7500809..ffb71f70e6 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
@@ -12,7 +12,7 @@ module.exports =
groupName: licence.name
subscription_id: licence.subscription_id
logger.log user_id:user._id, key:key, "creating notification key for user"
- NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, callback
+ NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, callback
read: (callback = ->)->
NotificationsHandler.markAsReadWithKey user._id, @key, callback
@@ -26,6 +26,6 @@ module.exports =
projectId: project._id.toString()
token: invite.token
logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user"
- NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, callback
+ NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback
read: (callback=()->) ->
NotificationsHandler.markAsReadByKeyOnly @key, callback
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
index 55ed4e8a9a..9fc21659c9 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
@@ -29,16 +29,19 @@ module.exports =
unreadNotifications = []
callback(null, unreadNotifications)
- createNotification: (user_id, key, templateKey, messageOpts, callback)->
+ createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)->
+ payload = {
+ key:key
+ messageOpts:messageOpts
+ templateKey:templateKey
+ }
+ if expiryDateTime?
+ payload.expires = expiryDateTime
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
timeout: oneSecond
method:"POST"
- json: {
- key:key
- messageOpts:messageOpts
- templateKey:templateKey
- }
+ json: payload
logger.log opts:opts, "creating notification for user"
makeRequest opts, callback
diff --git a/services/web/app/coffee/models/ProjectInvite.coffee b/services/web/app/coffee/models/ProjectInvite.coffee
index 2e3309397b..9b9e0cb350 100644
--- a/services/web/app/coffee/models/ProjectInvite.coffee
+++ b/services/web/app/coffee/models/ProjectInvite.coffee
@@ -6,7 +6,13 @@ Schema = mongoose.Schema
ObjectId = Schema.ObjectId
-THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30
+EXPIRY_IN_SECONDS = 60 * 60 * 24 * 30
+
+ExpiryDate = () ->
+ timestamp = new Date()
+ timestamp.setSeconds(timestamp.getSeconds() + EXPIRY_IN_SECONDS)
+ return timestamp
+
ProjectInviteSchema = new Schema(
@@ -16,7 +22,8 @@ ProjectInviteSchema = new Schema(
sendingUserId: ObjectId
projectId: ObjectId
privileges: String
- createdAt: {type: Date, default: Date.now, index: {expireAfterSeconds: THIRTY_DAYS_IN_SECONDS}}
+ createdAt: {type: Date, default: Date.now}
+ expires: {type: Date, default: ExpiryDate, index: {expireAfterSeconds: 10}}
},
{
collection: 'projectInvites'
@@ -29,7 +36,7 @@ conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.
ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema)
-
mongoose.model 'ProjectInvite', ProjectInviteSchema
exports.ProjectInvite = ProjectInvite
exports.ProjectInviteSchema = ProjectInviteSchema
+exports.EXPIRY_IN_SECONDS = EXPIRY_IN_SECONDS
diff --git a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
index 3554836fa5..9629bca8c7 100644
--- a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
@@ -60,9 +60,10 @@ describe 'NotificationsHandler', ->
@key = "some key here"
@messageOpts = {value:12344}
@templateKey = "renderThisHtml"
+ @expiry = null
it "should post the message over", (done)->
- @handler.createNotification user_id, @key, @templateKey, @messageOpts, =>
+ @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, =>
args = @request.args[0][0]
args.uri.should.equal "#{notificationUrl}/user/#{user_id}"
args.timeout.should.equal 1000
@@ -70,6 +71,22 @@ describe 'NotificationsHandler', ->
assert.deepEqual(args.json, expectedJson)
done()
+ describe 'when expiry date is supplied', ->
+ beforeEach ->
+ @key = "some key here"
+ @messageOpts = {value:12344}
+ @templateKey = "renderThisHtml"
+ @expiry = new Date()
+
+ it 'should post the message over with expiry field', (done) ->
+ @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, =>
+ args = @request.args[0][0]
+ args.uri.should.equal "#{notificationUrl}/user/#{user_id}"
+ args.timeout.should.equal 1000
+ expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts, expires: @expiry}
+ assert.deepEqual(args.json, expectedJson)
+ done()
+
describe "markAsReadByKeyOnly", ->
beforeEach ->
@key = "some key here"
From f92767f7b5c13008f72adf681815507595333ba5 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 12 Aug 2016 15:26:20 +0100
Subject: [PATCH 146/378] Address feedback, add `?` checks where appropriate
---
.../CollaboratorsInviteController.coffee | 12 ++++++------
.../CollaboratorsInviteControllerTests.coffee | 4 ++--
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 256697ecbd..3f8cbbf5b8 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -26,14 +26,14 @@ module.exports = CollaboratorsInviteController =
if err?
logger.err {projectId, email}, "error checking if user exists"
return callback(err)
- if !existingUser
+ if !existingUser?
logger.log {projectId, email}, "no existing user found, returning"
return callback(null)
- ProjectGetter.getProject projectId, (err, project) ->
+ ProjectGetter.getProject projectId, {_id: 1, name: 1}, (err, project) ->
if err?
logger.err {projectId, email}, "error getting project"
return callback(err)
- if !project
+ if !project?
logger.log {projectId}, "no project found while sending notification, returning"
return callback(null)
NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback)
@@ -109,7 +109,7 @@ module.exports = CollaboratorsInviteController =
logger.err {projectId, token}, "error getting invite by token"
return next(err)
# check if invite is gone, or otherwise non-existent
- if !invite
+ if !invite?
logger.log {projectId, token}, "no invite found for this token"
return _renderInvalidPage()
# check the user who sent the invite exists
@@ -117,7 +117,7 @@ module.exports = CollaboratorsInviteController =
if err?
logger.err {err, projectId}, "error getting project owner"
return next(err)
- if !owner
+ if !owner?
logger.log {projectId}, "no project owner found"
return _renderInvalidPage()
# fetch the project name
@@ -125,7 +125,7 @@ module.exports = CollaboratorsInviteController =
if err?
logger.err {err, projectId}, "error getting project"
return next(err)
- if !project
+ if !project?
logger.log {projectId}, "no project found"
return _renderInvalidPage()
# finally render the invite
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index dc15fbc65f..6ad001f466 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -73,7 +73,7 @@ describe "CollaboratorsInviteController", ->
@fakeProject =
_id: @project_id
name: "some project"
- @ProjectGetter.getProject = sinon.stub().callsArgWith(1, null, @fakeProject)
+ @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @fakeProject)
@notification = {create: sinon.stub().callsArgWith(0, null)}
@NotificationsBuilder.projectInvite = sinon.stub().returns(@notification)
@call = (callback) =>
@@ -109,7 +109,7 @@ describe "CollaboratorsInviteController", ->
describe 'when getProject produces an error', ->
beforeEach ->
- @ProjectGetter.getProject.callsArgWith(1, new Error('woops'))
+ @ProjectGetter.getProject.callsArgWith(2, new Error('woops'))
it 'should produce an error', (done) ->
@call (err) =>
From 492853f284a39badc04ba92bfd5d9ec0dbbaa02b Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 15 Aug 2016 14:56:02 +0100
Subject: [PATCH 147/378] Defend against undefined invites and members
---
.../ide/share/controllers/ShareProjectModalController.coffee | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index 9fc7417912..1f934cb801 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -43,10 +43,10 @@ define [
contact.display = contact.name
getCurrentMemberEmails = () ->
- $scope.project.members.map (u) -> u.email
+ ($scope.project.members || []).map (u) -> u.email
getCurrentInviteEmails = () ->
- $scope.project.invites.map (u) -> u.email
+ ($scope.project.invites || []).map (u) -> u.email
$scope.filterAutocompleteUsers = ($query) ->
currentMemberEmails = getCurrentMemberEmails()
From 40cb7e459091e07c56d7600a9d09721c1938189c Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 15 Aug 2016 15:19:16 +0100
Subject: [PATCH 148/378] defend against undefined property
---
.../ide/share/controllers/ShareProjectModalController.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index 1f934cb801..7874436a3c 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -81,7 +81,7 @@ define [
return addNextMember()
# NOTE: groups aren't really a thing in ShareLaTeX, partially inherited from DJ
- if member.display in currentInviteEmails and inviteId = _.find($scope.project.invites, (invite) -> invite.email == member.display)?._id
+ if member.display in currentInviteEmails and inviteId = _.find(($scope.project.invites || []), (invite) -> invite.email == member.display)?._id
request = projectInvites.resendInvite(inviteId)
else if member.type == "user"
request = projectInvites.sendInvite(member.email, $scope.inputs.privileges)
From 36d969e6e6b0629b452cc8e7fbe4770d8bc05e5d Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 15 Aug 2016 15:22:23 +0100
Subject: [PATCH 149/378] Set invites to be an empty array if missing
---
.../app/coffee/Features/Project/ProjectEditorHandler.coffee | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
index a20cc99fc0..1a10321544 100644
--- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
@@ -15,7 +15,10 @@ module.exports = ProjectEditorHandler =
deletedByExternalDataSource : project.deletedByExternalDataSource || false
deletedDocs: project.deletedDocs
members: []
- invites: invites || []
+ invites: invites
+
+ if !result.invites?
+ result.invites = []
{owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
result.owner = owner
From d40cf6568da6e49b0a4afdd3e9a0d02c7937b983 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 15 Aug 2016 15:40:16 +0100
Subject: [PATCH 150/378] Set invites to empty array
---
.../ide/share/controllers/ShareProjectModalController.coffee | 3 +++
1 file changed, 3 insertions(+)
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index 7874436a3c..409608825e 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -67,6 +67,9 @@ define [
$scope.state.error = null
$scope.state.inflight = true
+ if !$scope.project.invites?
+ $scope.project.invites = []
+
currentMemberEmails = getCurrentMemberEmails()
currentInviteEmails = getCurrentInviteEmails()
do addNextMember = () ->
From 7bec656bc28bb3b5daa2852e64dfff0262173a5b Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Mon, 15 Aug 2016 16:45:33 +0100
Subject: [PATCH 151/378] sort latex output files into order in dropdown
---
.../public/coffee/ide/pdf/controllers/PdfController.coffee | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index fd133e1bc3..418a55f8a5 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -195,12 +195,17 @@ define [
qs.clsiserverid = response.clsiServerId
for file in response.outputFiles
if IGNORE_FILES.indexOf(file.path) == -1
+ isOutputFile = file.path.match(/^output\./)
$scope.pdf.outputFiles.push {
# Turn 'output.blg' into 'blg file'.
- name: if file.path.match(/^output\./) then "#{file.path.replace(/^output\./, "")} file" else file.path
+ name: if isOutputFile then "#{file.path.replace(/^output\./, "")} file" else file.path
url: "/project/#{project_id}/output/#{file.path}" + createQueryString qs
+ main: if isOutputFile then true else false
}
+ # sort the output files into order, main files first, then others
+ $scope.pdf.outputFiles.sort (a,b) -> (b.main - a.main) || a.name.localeCompare(b.name)
+
fetchLogs = (fileByPath, options) ->
From 8d6cdb03e8bc77eed01b7a43d3cc331e5a699f1b Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Mon, 15 Aug 2016 16:46:53 +0100
Subject: [PATCH 152/378] restrict compile check options
allowed options are validate/error/silent
validate = only run chktex, exit status 0
error = run compilation, exit(1) if chktex fails
silent = run chktex, but always do full compilation
---
.../web/app/coffee/Features/Compile/CompileController.coffee | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee
index 000465a83c..00f4a880ff 100755
--- a/services/web/app/coffee/Features/Compile/CompileController.coffee
+++ b/services/web/app/coffee/Features/Compile/CompileController.coffee
@@ -29,8 +29,8 @@ module.exports = CompileController =
options.compiler = req.body.compiler
if req.body?.draft
options.draft = req.body.draft
- if req.body?.check
- options.check = if req.body.check is "validate" then "validate" else undefined
+ if req.body?.check in ['validate', 'error', 'silent']
+ options.check = req.body.check
logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
return next(error) if error?
From 93f69ca0a2d076cae6eeea4df4a1f5af7780ea8f Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Mon, 15 Aug 2016 16:48:48 +0100
Subject: [PATCH 153/378] run chktex silently for all users
---
.../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index 418a55f8a5..a31a336885 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -86,7 +86,7 @@ define [
return $http.post url, {
rootDoc_id: options.rootDocOverride_id or null
draft: $scope.draft
- check: if options.check then "validate" else null
+ check: if options.check then "validate" else "silent"
_csrf: window.csrfToken
}, {params: params}
From d2183738c579e999d64e1e968e718f363db48e00 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 16 Aug 2016 09:04:11 +0100
Subject: [PATCH 154/378] Improve logging for debugging
---
.../Features/Collaborators/CollaboratorsInviteHandler.coffee | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index 681d400205..d57087997d 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -11,11 +11,12 @@ Crypto = require 'crypto'
module.exports = CollaboratorsInviteHandler =
getAllInvites: (projectId, callback=(err, invites)->) ->
- logger.log {projectId}, "fetching invites from mongo"
+ logger.log {projectId}, "fetching invites for project"
ProjectInvite.find {projectId: projectId}, (err, invites) ->
if err?
logger.err {err, projectId}, "error getting invites from mongo"
return callback(err)
+ logger.log {projectId, count: invites.length}, "found invites for project"
callback(null, invites)
getInviteCount: (projectId, callback=(err, count)->) ->
From b68af254ffb3d72624c034f2770566d619a29dac Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 16 Aug 2016 09:59:42 +0100
Subject: [PATCH 155/378] Correct logic for bailing out with no privileges
---
.../Features/Editor/EditorHttpController.coffee | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
index f467d7a9bf..5c53a11348 100644
--- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
+++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
@@ -40,15 +40,14 @@ module.exports = EditorHttpController =
return callback(error) if error?
AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) ->
return callback(error) if error?
+ if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
+ return callback null, null, false
CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) ->
return callback(error) if error?
- if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
- callback null, null, false
- else
- callback(null,
- ProjectEditorHandler.buildProjectModelView(project, members, invites),
- privilegeLevel
- )
+ callback(null,
+ ProjectEditorHandler.buildProjectModelView(project, members, invites),
+ privilegeLevel
+ )
restoreDoc: (req, res, next) ->
project_id = req.params.Project_id
From cb4f6391a285bc9c959c6d2181dcb9be238a1db0 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Tue, 16 Aug 2016 10:59:27 +0100
Subject: [PATCH 156/378] updated comments
---
services/web/public/coffee/ide/editor/EditorManager.coffee | 2 ++
.../aceEditor/cursor-position/CursorPositionManager.coffee | 2 +-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee
index be15958c37..9c6a124f1a 100644
--- a/services/web/public/coffee/ide/editor/EditorManager.coffee
+++ b/services/web/public/coffee/ide/editor/EditorManager.coffee
@@ -47,6 +47,8 @@ define [
done = () =>
if options.gotoLine?
# allow Ace to display document before moving, delay until next tick
+ # added delay to make this happen later that gotoStoredPosition in
+ # CursorPositionManager
setTimeout () =>
@$scope.$broadcast "editor:gotoLine", options.gotoLine, options.gotoColumn
,0
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 8a38c317be..fd1b2d7830 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
@@ -55,5 +55,5 @@ define [], () ->
gotoLine: (line, column) ->
@editor.gotoLine(line, column)
- @editor.scrollToLine(line,true,true)
+ @editor.scrollToLine(line,true,true) # centre and animate
@editor.focus()
From da40f54d55812d5ddf89cfc17cccbbfe0598e7ac Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 16 Aug 2016 11:17:45 +0100
Subject: [PATCH 157/378] Improve logging, add acceptance tests for joinProject
json
---
.../Editor/EditorHttpController.coffee | 3 +++
.../coffee/ProjectInviteTests.coffee | 27 +++++++++++++++++++
2 files changed, 30 insertions(+)
diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
index 5c53a11348..b091c16c8b 100644
--- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
+++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
@@ -31,6 +31,7 @@ module.exports = EditorHttpController =
ProjectDeleter.unmarkAsDeletedByExternalSource project_id
_buildJoinProjectView: (project_id, user_id, callback = (error, project, privilegeLevel) ->) ->
+ logger.log {project_id, user_id}, "building the joinProject view"
ProjectGetter.getProjectWithoutDocLines project_id, (error, project) ->
return callback(error) if error?
return callback(new Error("not found")) if !project?
@@ -41,9 +42,11 @@ module.exports = EditorHttpController =
AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) ->
return callback(error) if error?
if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
+ logger.log {project_id, user_id, privilegeLevel}, "not an acceptable privilege level, returning null"
return callback null, null, false
CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) ->
return callback(error) if error?
+ logger.log {project_id, user_id, privilegeLevel}, "returning project model view"
callback(null,
ProjectEditorHandler.buildProjectModelView(project, members, invites),
privilegeLevel
diff --git a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
index c2614b5eb0..8e207948a1 100644
--- a/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectInviteTests.coffee
@@ -95,6 +95,19 @@ tryGetInviteList = (user, projectId, callback=(err, response, body)->) ->
json: true
}, callback
+tryJoinProject = (user, projectId, callback=(err, response, body)->) ->
+ user.getCsrfToken (error) =>
+ return callback(error) if error?
+ user.request.post {
+ url: "/project/#{projectId}/join"
+ qs: {user_id: user._id}
+ auth:
+ user: settings.apis.web.user
+ pass: settings.apis.web.pass
+ sendImmediately: true
+ json: true
+ jar: false
+ }, callback
# Expectations
expectProjectAccess = (user, projectId, callback=(err,result)->) ->
@@ -185,6 +198,15 @@ expectInviteListCount = (user, projectId, count, callback=(err)->) ->
expect(body.invites.length).to.equal count
callback()
+expectInvitesInJoinProjectCount = (user, projectId, count, callback=(err,result)->) ->
+ tryJoinProject user, projectId, (err, response, body) ->
+ expect(err).to.be.oneOf [null, undefined]
+ expect(response.statusCode).to.equal 200
+ expect(body.project).to.contain.keys ['invites']
+ expect(body.project.invites.length).to.equal count
+ callback()
+
+
describe "ProjectInviteTests", ->
before (done) ->
@@ -245,8 +267,12 @@ describe "ProjectInviteTests", ->
@invite = invite
cb()
(cb) => expectInviteListCount @sendingUser, @projectId, 1, cb
+ # check the joinProject view
+ (cb) => expectInvitesInJoinProjectCount @sendingUser, @projectId, 1, cb
+ # revoke invite
(cb) => revokeInvite @sendingUser, @projectId, @invite._id, cb
(cb) => expectInviteListCount @sendingUser, @projectId, 0, cb
+ (cb) => expectInvitesInJoinProjectCount @sendingUser, @projectId, 0, cb
], done
it 'should allow the project owner to many invites at once', (done) ->
@@ -268,6 +294,7 @@ describe "ProjectInviteTests", ->
cb()
# should have two
(cb) => expectInviteListCount @sendingUser, @projectId, 2, cb
+ (cb) => expectInvitesInJoinProjectCount @sendingUser, @projectId, 2, cb
# revoke first
(cb) => revokeInvite @sendingUser, @projectId, @inviteOne._id, cb
(cb) => expectInviteListCount @sendingUser, @projectId, 1, cb
From ce78b855a3f840519407f23a0365e7961337646c Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 16 Aug 2016 11:33:14 +0100
Subject: [PATCH 158/378] Add counts to log message
---
.../web/app/coffee/Features/Editor/EditorHttpController.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
index b091c16c8b..0c547e53ba 100644
--- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
+++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
@@ -46,7 +46,7 @@ module.exports = EditorHttpController =
return callback null, null, false
CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) ->
return callback(error) if error?
- logger.log {project_id, user_id, privilegeLevel}, "returning project model view"
+ logger.log {project_id, user_id, memberCount: members.length, inviteCount: invites.length, privilegeLevel}, "returning project model view"
callback(null,
ProjectEditorHandler.buildProjectModelView(project, members, invites),
privilegeLevel
From 81d0edf71672dca20852acf3b00adf5bb777d7b4 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 16 Aug 2016 15:19:36 +0100
Subject: [PATCH 159/378] Improve error handling
---
.../SubscriptionController.coffee | 45 ++++++++++++-------
1 file changed, 30 insertions(+), 15 deletions(-)
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
index ca932ffab3..fbc72a277e 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
@@ -22,6 +22,7 @@ module.exports = SubscriptionController =
viewName = "#{viewName}_#{req.query.v}"
logger.log viewName:viewName, "showing plans page"
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)->
+ return next(err) if err?
res.render viewName,
title: "plans_and_pricing"
plans: plans
@@ -71,12 +72,13 @@ module.exports = SubscriptionController =
AuthenticationController.getLoggedInUser req, (error, user) =>
return next(error) if error?
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
+ return next(err) if err?
groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user)
if subscription?.customAccount
logger.log user: user, "redirecting to custom account page"
res.redirect "/user/subscription/custom_account"
else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember
- logger.log user:user, "redirecting to group subscription invite page"
+ logger.log user:user, "redirecting to group subscription invite page"
res.redirect groupLicenceInviteUrl
else if !hasSubOrIsGroupMember
logger.log user: user, "redirecting to plans"
@@ -99,7 +101,9 @@ module.exports = SubscriptionController =
userCustomSubscriptionPage: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
+ return next(error) if error?
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
+ return next(err) if err?
if !subscription?
err = new Error("subscription null for custom account, user:#{user?._id}")
logger.warn err:err, "subscription is null for custom accounts page"
@@ -113,6 +117,7 @@ module.exports = SubscriptionController =
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
+ return next(err) if err?
if !hasSubscription
res.redirect "/user/subscription"
else
@@ -142,23 +147,26 @@ module.exports = SubscriptionController =
return res.sendStatus 500
res.sendStatus 201
- successful_subscription: (req, res)->
+ successful_subscription: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) =>
+ return next(error) if error?
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
+ return next(error) if error?
res.render "subscriptions/successful_subscription",
title: "thank_you"
subscription:subscription
cancelSubscription: (req, res, next) ->
AuthenticationController.getLoggedInUser req, (error, user) ->
- logger.log user_id:user._id, "canceling subscription"
return next(error) if error?
+ logger.log user_id:user._id, "canceling subscription"
SubscriptionHandler.cancelSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong canceling subscription"
+ return next(err)
res.redirect "/user/subscription"
-
- updateSubscription: (req, res)->
+
+ updateSubscription: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
planCode = req.body.plan_code
@@ -166,30 +174,35 @@ module.exports = SubscriptionController =
SubscriptionHandler.updateSubscription user, planCode, null, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong updating subscription"
+ return next(err)
res.redirect "/user/subscription"
- reactivateSubscription: (req, res)->
+ reactivateSubscription: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
- logger.log user_id:user._id, "reactivating subscription"
return next(error) if error?
+ logger.log user_id:user._id, "reactivating subscription"
SubscriptionHandler.reactivateSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong reactivating subscription"
+ return next(err)
res.redirect "/user/subscription"
- recurlyCallback: (req, res)->
+ recurlyCallback: (req, res, next)->
logger.log data: req.body, "received recurly callback"
# we only care if a subscription has exipired
if req.body? and req.body["expired_subscription_notification"]?
recurlySubscription = req.body["expired_subscription_notification"].subscription
- SubscriptionHandler.recurlyCallback recurlySubscription, ->
+ SubscriptionHandler.recurlyCallback recurlySubscription, (err)->
+ return next(err) if err?
res.sendStatus 200
else
res.sendStatus 200
- renderUpgradeToAnnualPlanPage: (req, res)->
+ renderUpgradeToAnnualPlanPage: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
+ return next(error) if error?
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
+ return next(err) if err?
planCode = subscription?.planCode.toLowerCase()
if planCode?.indexOf("annual") != -1
planName = "annual"
@@ -204,8 +217,9 @@ module.exports = SubscriptionController =
title: "Upgrade to annual"
planName: planName
- processUpgradeToAnnualPlan: (req, res)->
+ processUpgradeToAnnualPlan: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
+ return next(error) if error?
{planName} = req.body
coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName]
annualPlanName = "#{planName}-annual"
@@ -213,13 +227,14 @@ module.exports = SubscriptionController =
SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)->
if err?
logger.err err:err, user_id:user._id, "error updating subscription"
- res.sendStatus 500
- else
- res.sendStatus 200
+ return next(err)
+ res.sendStatus 200
- extendTrial: (req, res)->
+ extendTrial: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
+ return next(error) if error?
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
+ return next(err) if err?
SubscriptionHandler.extendTrial subscription, 14, (err)->
if err?
res.send 500
From fc068b62a26a67f1255b9181f604c340502a689b Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 17 Aug 2016 08:51:35 +0100
Subject: [PATCH 160/378] defend against undefined plan_code
---
.../Features/Subscription/SubscriptionController.coffee | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
index fbc72a277e..d4521981e4 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
@@ -170,6 +170,10 @@ module.exports = SubscriptionController =
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
planCode = req.body.plan_code
+ if !planCode?
+ err = new Error('plan_code not defined')
+ logger.err {user_id: user._id, err}, "error updating subscription"
+ return next(err)
logger.log planCode: planCode, user_id:user._id, "updating subscription"
SubscriptionHandler.updateSubscription user, planCode, null, (err)->
if err?
From c98e473bc3d0e1807f5f35fb34ad81221f881024 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 17 Aug 2016 10:31:05 +0100
Subject: [PATCH 161/378] Fix layout of notifications
---
.../app/views/project/list/notifications.jade | 7 +++----
.../public/stylesheets/app/project-list.less | 20 ++++++++++++++++---
2 files changed, 20 insertions(+), 7 deletions(-)
diff --git a/services/web/app/views/project/list/notifications.jade b/services/web/app/views/project/list/notifications.jade
index dd6886fba6..8dba0852ff 100644
--- a/services/web/app/views/project/list/notifications.jade
+++ b/services/web/app/views/project/list/notifications.jade
@@ -9,10 +9,9 @@ span(ng-controller="NotificationsController").userNotifications
.row(ng-hide="unreadNotification.hide")
.col-xs-12
.alert.alert-info
- .row
- .col-xs-11
- span(ng-bind-html="unreadNotification.html")
- .col-xs-1
+ div.notification_inner
+ span(ng-bind-html="unreadNotification.html").notification_body
+ span().notification_close
button(ng-click="dismiss(unreadNotification)").close.pull-right
span(aria-hidden="true") ×
span.sr-only #{translate("close")}
diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less
index 53c780dbe4..d3131f136c 100644
--- a/services/web/public/stylesheets/app/project-list.less
+++ b/services/web/public/stylesheets/app/project-list.less
@@ -32,10 +32,24 @@
ul {
margin-bottom:0px;
}
- .alert {
- .box-shadow(2px 4px 6px rgba(0, 0, 0, 0.25));
+ .notification_entry {
+ .alert {
+ .box-shadow(2px 4px 6px rgba(0, 0, 0, 0.25));
+ .notification_inner {
+ display: table-row;
+ .notification_body {
+ display: table-cell;
+ width: 99%;
+ padding-right: 15px;
+ vertical-align: middle;
+ }
+ .notification_close {
+ display: table-cell;
+ vertical-align: middle;
+ }
+ }
+ }
}
-
}
ul.folders-menu {
From 85f49d6c9cdd63aa48afc17e9ee0e4f9ee10b14a Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 17 Aug 2016 10:37:44 +0100
Subject: [PATCH 162/378] Make whole 'red button' in email a link
---
services/web/app/coffee/Features/Email/EmailBuilder.coffee | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee
index ae3ee28253..f7a7a78a05 100644
--- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee
+++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee
@@ -94,15 +94,11 @@ templates.projectInvite =
compiledTemplate: _.template """
Hi, <%= owner.email %> wants to share '<%= project.name %>' with you
-
Thank you
#{settings.appName}
From 672d4f8767454faf31e4d1532a5ab57a963c85dd Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Wed, 17 Aug 2016 12:11:17 +0100
Subject: [PATCH 163/378] Setup AB test (alternative is still empty).
---
services/web/app/views/subscriptions/new.jade | 233 +++++++++---------
1 file changed, 117 insertions(+), 116 deletions(-)
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade
index e84dc56eec..7613ec463e 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.jade
@@ -51,131 +51,132 @@ block content
div(ng-if="normalPrice")
span.small Normally {{price.currency.symbol}}{{normalPrice}}
.row
- .col-md-12
- form(ng-show="planName")
-
-
- .row
- .col-md-12
- .form-group
- .row
- .col-md-6
- label.radio-inline
- input.paymentTypeOption(type="radio",value="credit_card", ng-model="paymentMethod")
- i.fa.fa-cc-mastercard.fa-3x
- span
- i.fa.fa-cc-visa.fa-3x
- .col-md-6
- label.radio-inline
- input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod")
- i.fa.fa-cc-paypal.fa-3x
-
-
-
- .alert.alert-warning.small(ng-show="genericError")
- strong {{genericError}}
-
- span(ng-hide="paymentMethod == 'paypal'")
+ div(sixpack-switch="subscription-form")
+ .col-md-12(sixpack-default)
+ form(ng-show="planName")
.row
.col-md-12
.form-group
- div.alert.alert-warning.small(ng-hide="validation.correctCvv") #{translate("invalid")} CVV
- div.alert.alert-warning.small(ng-hide="validation.correctCardNumber") #{translate("invalid")} #{translate("credit_card_number")}
+ .row
+ .col-md-6
+ label.radio-inline
+ input.paymentTypeOption(type="radio",value="credit_card", ng-model="paymentMethod")
+ i.fa.fa-cc-mastercard.fa-3x
+ span
+ i.fa.fa-cc-visa.fa-3x
+ .col-md-6
+ label.radio-inline
+ input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod")
+ i.fa.fa-cc-paypal.fa-3x
+
+
+
+ .alert.alert-warning.small(ng-show="genericError")
+ strong {{genericError}}
+
+ span(ng-hide="paymentMethod == 'paypal'")
+ .row
+ .col-md-12
+ .form-group
+ div.alert.alert-warning.small(ng-hide="validation.correctCvv") #{translate("invalid")} CVV
+ div.alert.alert-warning.small(ng-hide="validation.correctCardNumber") #{translate("invalid")} #{translate("credit_card_number")}
+ .row
+ .col-md-6
+ .form-group(ng-class="validation.number == false || validation.errorFields.number ? 'has-error' : ''")
+ input.form-control(ng-model='data.number', ng-blur="validateCardNumber()", placeholder="#{translate('credit_card_number')}")
+ .col-md-3
+ .form-group(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
+ input.form-control(ng-model='data.cvv', ng-blur="validateCvv()", placeholder="CVV")
+ .row
+ .col-md-12
+ div.alert.alert-warning.small(ng-hide="validation.correctExpiry") #{translate("invalid")} #{translate("expiry")}
+ .row
+ .col-md-3
+ .form-group(ng-class="validation.correctExpiry == false || validation.errorFields.month ? 'has-error' : ''")
+ select.form-control(data-recurly='month', ng-change="validateExpiry()", ng-model='data.month')
+ option(value="", disabled, selected) Month
+ option(value="01") 01
+ option(value="02") 02
+ option(value="03") 03
+ option(value="04") 04
+ option(value="05") 05
+ option(value="06") 06
+ option(value="07") 07
+ option(value="08") 08
+ option(value="09") 09
+ option(value="10") 10
+ option(value="11") 11
+ option(value="12") 12
+ .col-md-4
+ .form-group(ng-class="validation.correctExpiry == false || validation.errorFields.year ? 'has-error' : ''")
+ select.form-control(data-recurly='year', ng-change="validateExpiry()", ng-model='data.year')
+ option(value="", disabled, selected) Year
+ option(value="2016") 2016
+ option(value="2017") 2017
+ option(value="2018") 2018
+ option(value="2019") 2019
+ option(value="2020") 2020
+ option(value="2021") 2021
+ option(value="2022") 2022
+ option(value="2023") 2023
+ option(value="2024") 2024
+ option(value="2025") 2025
+ option(value="2026") 2026
.row
.col-md-6
- .form-group(ng-class="validation.number == false || validation.errorFields.number ? 'has-error' : ''")
- input.form-control(ng-model='data.number', ng-blur="validateCardNumber()", placeholder="#{translate('credit_card_number')}")
- .col-md-3
- .form-group(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
- input.form-control(ng-model='data.cvv', ng-blur="validateCvv()", placeholder="CVV")
+ .form-group(ng-class="validation.errorFields.first_name ? 'has-error' : ''")
+ input.form-control(type='text', value='', maxlength='255', , onkeyup='', data-recurly="first_name", ng-model="data.first_name", required, placeholder="#{translate('first_name')}")
+ .col-md-6
+ .form-group(ng-class="validation.errorFields.last_name ? 'has-error' : ''")
+ input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="last_name", ng-model="data.last_name", required, placeholder="#{translate('last_name')}")
+ hr
.row
.col-md-12
- div.alert.alert-warning.small(ng-hide="validation.correctExpiry") #{translate("invalid")} #{translate("expiry")}
+ .form-group(ng-class="validation.errorFields.address1 ? 'has-error' : ''")
+ label #{translate("billing_address")}
+ input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address1", placeholder="#{translate('address')}")
+ .form-group(ng-class="validation.errorFields.address2 ? 'has-error' : ''")
+ input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address2", placeholder="#{translate('address')}")
+ .form-group(ng-class="validation.errorFields.state ? 'has-error' : ''")
+ input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.state", placeholder="#{translate('state')}")
.row
- .col-md-3
- .form-group(ng-class="validation.correctExpiry == false || validation.errorFields.month ? 'has-error' : ''")
- select.form-control(data-recurly='month', ng-change="validateExpiry()", ng-model='data.month')
- option(value="", disabled, selected) Month
- option(value="01") 01
- option(value="02") 02
- option(value="03") 03
- option(value="04") 04
- option(value="05") 05
- option(value="06") 06
- option(value="07") 07
- option(value="08") 08
- option(value="09") 09
- option(value="10") 10
- option(value="11") 11
- option(value="12") 12
- .col-md-4
- .form-group(ng-class="validation.correctExpiry == false || validation.errorFields.year ? 'has-error' : ''")
- select.form-control(data-recurly='year', ng-change="validateExpiry()", ng-model='data.year')
- option(value="", disabled, selected) Year
- option(value="2016") 2016
- option(value="2017") 2017
- option(value="2018") 2018
- option(value="2019") 2019
- option(value="2020") 2020
- option(value="2021") 2021
- option(value="2022") 2022
- option(value="2023") 2023
- option(value="2024") 2024
- option(value="2025") 2025
- option(value="2026") 2026
- .row
- .col-md-6
- .form-group(ng-class="validation.errorFields.first_name ? 'has-error' : ''")
- input.form-control(type='text', value='', maxlength='255', , onkeyup='', data-recurly="first_name", ng-model="data.first_name", required, placeholder="#{translate('first_name')}")
- .col-md-6
- .form-group(ng-class="validation.errorFields.last_name ? 'has-error' : ''")
- input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="last_name", ng-model="data.last_name", required, placeholder="#{translate('last_name')}")
- hr
- .row
- .col-md-12
- .form-group(ng-class="validation.errorFields.address1 ? 'has-error' : ''")
- label #{translate("billing_address")}
- input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address1", placeholder="#{translate('address')}")
- .form-group(ng-class="validation.errorFields.address2 ? 'has-error' : ''")
- input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address2", placeholder="#{translate('address')}")
- .form-group(ng-class="validation.errorFields.state ? 'has-error' : ''")
- input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.state", placeholder="#{translate('state')}")
- .row
- .col-md-7
- .form-group(ng-class="validation.errorFields.city ? 'has-error' : ''")
- input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="city", ng-model="data.city", placeholder="#{translate('city')}")
- .col-md-5(ng-class="validation.errorFields.postal_code ? 'has-error' : ''")
- input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="postal_code", ng-model="data.postal_code", placeholder="#{translate('zip_post_code')}")
- .row
- .col-md-7
- .form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
- select.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required)
- mixin countries_options()
- .row
- .col-md-8
- if (showCouponField)
+ .col-md-7
+ .form-group(ng-class="validation.errorFields.city ? 'has-error' : ''")
+ input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="city", ng-model="data.city", placeholder="#{translate('city')}")
+ .col-md-5(ng-class="validation.errorFields.postal_code ? 'has-error' : ''")
+ input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="postal_code", ng-model="data.postal_code", placeholder="#{translate('zip_post_code')}")
+ .row
+ .col-md-7
+ .form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
+ select.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required)
+ mixin countries_options()
+ .row
+ .col-md-8
+ if (showCouponField)
+ .form-group
+ input.form-control(type='text', ng-blur="applyCoupon()", ng-model="data.coupon", placeholder="#{translate('coupon')}")
+ .row
+ .col-md-8
+ if (showVatField)
+ .form-group
+ input.form-control(type='text', ng-blur="applyVatNumber()", ng-model="data.vat_number", placeholder="#{translate('vat_number')}")
+ .row
+ .col-xs-7
.form-group
- input.form-control(type='text', ng-blur="applyCoupon()", ng-model="data.coupon", placeholder="#{translate('coupon')}")
- .row
- .col-md-8
- if (showVatField)
- .form-group
- input.form-control(type='text', ng-blur="applyVatNumber()", ng-model="data.vat_number", placeholder="#{translate('vat_number')}")
- .row
- .col-xs-7
- .form-group
- button.btn.btn-success(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") #{translate("upgrade_now")}
-
- .col-xs-3.pricingBreakdown
- div(ng-if="price.next.subtotal != price.next.total") Subtotal
- div(ng-if="price.next.tax!='0.00'") Tax
- div
- strong Total
- .col-xs-2
- div(ng-if="price.next.subtotal != price.next.total").pull-right {{price.currency.symbol}}{{price.next.subtotal}}
- div(ng-if="price.next.tax!='0.00'").pull-right {{price.currency.symbol}}{{price.next.tax}}
- div.pull-right
- strong {{price.currency.symbol}}{{price.next.total}}
+ button.btn.btn-success(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") #{translate("upgrade_now")}
+
+ .col-xs-3.pricingBreakdown
+ div(ng-if="price.next.subtotal != price.next.total") Subtotal
+ div(ng-if="price.next.tax!='0.00'") Tax
+ div
+ strong Total
+ .col-xs-2
+ div(ng-if="price.next.subtotal != price.next.total").pull-right {{price.currency.symbol}}{{price.next.subtotal}}
+ div(ng-if="price.next.tax!='0.00'").pull-right {{price.currency.symbol}}{{price.next.tax}}
+ div.pull-right
+ strong {{price.currency.symbol}}{{price.next.total}}
+ .col-md-12(sixpack-when="simple")
+ span Lorem ipsum!
span(sixpack-switch="payment-left-menu-bottom")
From ece0491e3d7522a2c30870675652c64975710917 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 17 Aug 2016 16:27:15 +0100
Subject: [PATCH 164/378] Refactor. Handle republishing of notifications on
resend.
---
.../CollaboratorsInviteController.coffee | 31 +--
.../CollaboratorsInviteHandler.coffee | 59 +++-
.../Notifications/NotificationsBuilder.coffee | 4 +-
.../Notifications/NotificationsHandler.coffee | 4 +-
.../CollaboratorsInviteControllerTests.coffee | 185 +------------
.../CollaboratorsInviteHandlerTests.coffee | 257 +++++++++++++++++-
.../NotificationsHandlerTests.coffee | 23 +-
7 files changed, 331 insertions(+), 232 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 3f8cbbf5b8..216e2f8a01 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -20,27 +20,6 @@ module.exports = CollaboratorsInviteController =
return next(err)
res.json({invites: invites})
- _trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) ->
- email = invite.email
- UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) ->
- if err?
- logger.err {projectId, email}, "error checking if user exists"
- return callback(err)
- if !existingUser?
- logger.log {projectId, email}, "no existing user found, returning"
- return callback(null)
- ProjectGetter.getProject projectId, {_id: 1, name: 1}, (err, project) ->
- if err?
- logger.err {projectId, email}, "error getting project"
- return callback(err)
- if !project?
- logger.log {projectId}, "no project found while sending notification, returning"
- return callback(null)
- NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback)
-
- _tryCancelInviteNotification: (inviteId, currentUser, callback=()->) ->
- NotificationsBuilder.projectInvite({_id: inviteId}, null, null, currentUser).read(callback)
-
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
email = req.body.email
@@ -57,14 +36,12 @@ module.exports = CollaboratorsInviteController =
if !email? or email == ""
logger.log {projectId, email, sendingUserId}, "invalid email address"
return res.sendStatus(400)
- CollaboratorsInviteHandler.inviteToProject projectId, sendingUserId, email, privileges, (err, invite) ->
+ 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}
- # async check if email is for an existing user, send a notification
- CollaboratorsInviteController._trySendInviteNotification(projectId, sendingUser, invite, ()->)
+ EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
return res.json {invite: invite}
revokeInvite: (req, res, next) ->
@@ -81,8 +58,9 @@ module.exports = CollaboratorsInviteController =
resendInvite: (req, res, next) ->
projectId = req.params.Project_id
inviteId = req.params.invite_id
+ sendingUser = req.session.user
logger.log {projectId, inviteId}, "resending invite"
- CollaboratorsInviteHandler.resendInvite projectId, inviteId, (err) ->
+ CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) ->
if err?
logger.err {projectId, inviteId}, "error resending invite"
return next(err)
@@ -142,5 +120,4 @@ module.exports = CollaboratorsInviteController =
logger.err {projectId, inviteId}, "error accepting invite by token"
return next(err)
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
- CollaboratorsInviteController._tryCancelInviteNotification inviteId, currentUser, () ->
res.redirect "/project/#{projectId}"
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
index d57087997d..bf03fe242d 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -2,10 +2,13 @@ ProjectInvite = require("../../models/ProjectInvite").ProjectInvite
logger = require('logger-sharelatex')
CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
CollaboratorsHandler = require "./CollaboratorsHandler"
+UserGetter = require "../User/UserGetter"
+ProjectGetter = require "../Project/ProjectGetter"
Async = require "async"
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
Errors = require "../Errors/Errors"
Crypto = require 'crypto'
+NotificationsBuilder = require("../Notifications/NotificationsBuilder")
module.exports = CollaboratorsInviteHandler =
@@ -27,26 +30,57 @@ module.exports = CollaboratorsInviteHandler =
return callback(err)
callback(null, count)
- inviteToProject: (projectId, sendingUserId, email, privileges, callback=(err,invite)->) ->
- logger.log {projectId, sendingUserId, email, privileges}, "adding invite"
+ _trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) ->
+ email = invite.email
+ UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) ->
+ if err?
+ logger.err {projectId, email}, "error checking if user exists"
+ return callback(err)
+ if !existingUser?
+ logger.log {projectId, email}, "no existing user found, returning"
+ return callback(null)
+ ProjectGetter.getProject projectId, {_id: 1, name: 1}, (err, project) ->
+ if err?
+ logger.err {projectId, email}, "error getting project"
+ return callback(err)
+ if !project?
+ logger.log {projectId}, "no project found while sending notification, returning"
+ return callback(null)
+ NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback)
+
+ _tryCancelInviteNotification: (inviteId, callback=()->) ->
+ NotificationsBuilder.projectInvite({_id: inviteId}, null, null, null).read(callback)
+
+ _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)->
+ return callback(err) if err?
+ CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
+ return callback(err) if err?
+ callback()
+
+ inviteToProject: (projectId, sendingUser, email, privileges, callback=(err,invite)->) ->
+ logger.log {projectId, sendingUserId: sendingUser._id, email, privileges}, "adding invite"
Crypto.randomBytes 24, (err, buffer) ->
if err?
- logger.err {err, projectId, sendingUserId, email}, "error generating random token"
+ logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error generating random token"
return callback(err)
token = buffer.toString('hex')
invite = new ProjectInvite {
email: email
token: token
- sendingUserId: sendingUserId
+ sendingUserId: sendingUser._id
projectId: projectId
privileges: privileges
}
invite.save (err, invite) ->
if err?
- logger.err {err, projectId, sendingUserId, email}, "error saving token"
+ logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error saving token"
return callback(err)
- CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, email, invite
- callback(null, invite)
+ CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
+ if err?
+ logger.err {projectId, email}, "error sending messages for invite"
+ callback(err, invite)
revokeInvite: (projectId, inviteId, callback=(err)->) ->
logger.log {projectId, inviteId}, "removing invite"
@@ -54,9 +88,10 @@ module.exports = CollaboratorsInviteHandler =
if err?
logger.err {err, projectId, inviteId}, "error removing invite"
return callback(err)
+ CollaboratorsInviteHandler._tryCancelInviteNotification(inviteId, ()->)
callback(null)
- resendInvite: (projectId, inviteId, callback=(err)->) ->
+ resendInvite: (projectId, sendingUser, inviteId, callback=(err)->) ->
logger.log {projectId, inviteId}, "resending invite email"
ProjectInvite.findOne {_id: inviteId, projectId: projectId}, (err, invite) ->
if err?
@@ -65,8 +100,11 @@ module.exports = CollaboratorsInviteHandler =
if !invite?
logger.err {err, projectId, inviteId}, "no invite found, nothing to resend"
return callback(null)
- CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite
- callback(null)
+ CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
+ if err?
+ logger.err {projectid, inviteId}, "error resending invite messages"
+ return callback(err)
+ callback(null)
getInviteByToken: (projectId, tokenString, callback=(err,invite)->) ->
logger.log {projectId, tokenString}, "fetching invite by token"
@@ -100,4 +138,5 @@ module.exports = CollaboratorsInviteHandler =
if err?
logger.err {err, projectId, inviteId}, "error removing invite"
return callback(err)
+ CollaboratorsInviteHandler._tryCancelInviteNotification inviteId, ()->
callback()
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
index ffb71f70e6..646a520a1c 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
@@ -12,7 +12,7 @@ module.exports =
groupName: licence.name
subscription_id: licence.subscription_id
logger.log user_id:user._id, key:key, "creating notification key for user"
- NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, callback
+ NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, false, callback
read: (callback = ->)->
NotificationsHandler.markAsReadWithKey user._id, @key, callback
@@ -26,6 +26,6 @@ module.exports =
projectId: project._id.toString()
token: invite.token
logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user"
- NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback
+ NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, true, callback
read: (callback=()->) ->
NotificationsHandler.markAsReadByKeyOnly @key, callback
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
index 9fc21659c9..2ee97dae49 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
@@ -29,7 +29,7 @@ module.exports =
unreadNotifications = []
callback(null, unreadNotifications)
- createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)->
+ createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)->
payload = {
key:key
messageOpts:messageOpts
@@ -37,6 +37,8 @@ module.exports =
}
if expiryDateTime?
payload.expires = expiryDateTime
+ if forceCreate
+ payload.forceCreate = true
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
timeout: oneSecond
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 6ad001f466..346ac3a3c1 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -26,166 +26,6 @@ describe "CollaboratorsInviteController", ->
@project_id = "project-id-123"
@callback = sinon.stub()
- describe '_tryCancelInviteNotification', ->
- beforeEach ->
- @inviteId = ObjectId()
- @currentUser = {_id: ObjectId()}
- @notification = {read: sinon.stub().callsArgWith(0, null)}
- @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification)
- @call = (callback) =>
- @CollaboratorsInviteController._tryCancelInviteNotification @inviteId, @currentUser, callback
-
- it 'should not produce an error', (done) ->
- @call (err) =>
- expect(err).to.be.oneOf [null, undefined]
- done()
-
- it 'should call notification.read', (done) ->
- @call (err) =>
- @notification.read.callCount.should.equal 1
- done()
-
- describe 'when notification.read produces an error', ->
- beforeEach ->
- @notification = {read: sinon.stub().callsArgWith(0, new Error('woops'))}
- @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification)
-
- it 'should produce an error', (done) ->
- @call (err) =>
- expect(err).to.be.instanceof Error
- done()
-
- describe "_trySendInviteNotification", ->
-
- beforeEach ->
- @invite =
- _id: ObjectId(),
- token: "some_token",
- sendingUserId: ObjectId(),
- projectId: @project_id,
- targetEmail: 'user@example.com'
- createdAt: new Date(),
- @sendingUser =
- _id: ObjectId()
- first_name: "jim"
- @existingUser = {_id: ObjectId()}
- @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @existingUser)
- @fakeProject =
- _id: @project_id
- name: "some project"
- @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @fakeProject)
- @notification = {create: sinon.stub().callsArgWith(0, null)}
- @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification)
- @call = (callback) =>
- @CollaboratorsInviteController._trySendInviteNotification @project_id, @sendingUser, @invite, callback
-
- describe 'when the user exists', ->
-
- beforeEach ->
-
- it 'should not produce an error', (done) ->
- @call (err) =>
- expect(err).to.be.oneOf [null, undefined]
- done()
-
- it 'should call getUser', (done) ->
- @call (err) =>
- @UserGetter.getUser.callCount.should.equal 1
- @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
- done()
-
- it 'should call getProject', (done) ->
- @call (err) =>
- @ProjectGetter.getProject.callCount.should.equal 1
- @ProjectGetter.getProject.calledWith(@project_id).should.equal true
- done()
-
- it 'should call NotificationsBuilder.projectInvite.create', (done) ->
- @call (err) =>
- @NotificationsBuilder.projectInvite.callCount.should.equal 1
- @notification.create.callCount.should.equal 1
- done()
-
- describe 'when getProject produces an error', ->
-
- beforeEach ->
- @ProjectGetter.getProject.callsArgWith(2, new Error('woops'))
-
- it 'should produce an error', (done) ->
- @call (err) =>
- expect(err).to.be.instanceof Error
- done()
-
- it 'should not call NotificationsBuilder.projectInvite.create', (done) ->
- @call (err) =>
- @NotificationsBuilder.projectInvite.callCount.should.equal 0
- @notification.create.callCount.should.equal 0
- done()
-
- describe 'when projectInvite.create produces an error', ->
-
- beforeEach ->
- @notification.create.callsArgWith(0, new Error('woops'))
-
- it 'should produce an error', (done) ->
- @call (err) =>
- expect(err).to.be.instanceof Error
- done()
-
- describe 'when the user does not exist', ->
-
- beforeEach ->
- @UserGetter.getUser = sinon.stub().callsArgWith(2, null, null)
-
- it 'should not produce an error', (done) ->
- @call (err) =>
- expect(err).to.be.oneOf [null, undefined]
- done()
-
- it 'should call getUser', (done) ->
- @call (err) =>
- @UserGetter.getUser.callCount.should.equal 1
- @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
- done()
-
- it 'should not call getProject', (done) ->
- @call (err) =>
- @ProjectGetter.getProject.callCount.should.equal 0
- done()
-
- it 'should not call NotificationsBuilder.projectInvite.create', (done) ->
- @call (err) =>
- @NotificationsBuilder.projectInvite.callCount.should.equal 0
- @notification.create.callCount.should.equal 0
- done()
-
- describe 'when the getUser produces an error', ->
-
- beforeEach ->
- @UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops'))
-
- it 'should produce an error', (done) ->
- @call (err) =>
- expect(err).to.be.instanceof Error
- done()
-
- it 'should call getUser', (done) ->
- @call (err) =>
- @UserGetter.getUser.callCount.should.equal 1
- @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
- done()
-
- it 'should not call getProject', (done) ->
- @call (err) =>
- @ProjectGetter.getProject.callCount.should.equal 0
- done()
-
- it 'should not call NotificationsBuilder.projectInvite.create', (done) ->
- @call (err) =>
- @NotificationsBuilder.projectInvite.callCount.should.equal 0
- @notification.create.callCount.should.equal 0
- done()
-
describe 'getAllInvites', ->
beforeEach ->
@@ -231,8 +71,10 @@ describe "CollaboratorsInviteController", ->
@targetEmail = "user@example.com"
@req.params =
Project_id: @project_id
+ @current_user =
+ _id: @current_user_id = "current-user-id"
@req.session =
- user: _id: @current_user_id = "current-user-id"
+ user: @current_user
@req.body =
email: @targetEmail
privileges: @privileges = "readAndWrite"
@@ -248,7 +90,6 @@ describe "CollaboratorsInviteController", ->
}
@LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true)
@CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, null, @invite)
- @CollaboratorsInviteController._trySendInviteNotification = sinon.stub()
@callback = sinon.stub()
@next = sinon.stub()
@@ -268,15 +109,12 @@ describe "CollaboratorsInviteController", ->
it 'should have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
- @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true
+ @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true
it 'should have called emitToRoom', ->
@EditorRealTimeController.emitToRoom.callCount.should.equal 1
@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true
- it 'should call _trySendInviteNotification', ->
- @CollaboratorsInviteController._trySendInviteNotification.callCount.should.equal 1
-
describe 'when the user is not allowed to add more collaborators', ->
beforeEach ->
@@ -321,7 +159,7 @@ describe "CollaboratorsInviteController", ->
it 'should have called inviteToProject', ->
@CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1
- @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user_id,@targetEmail,@privileges).should.equal true
+ @CollaboratorsInviteHandler.inviteToProject.calledWith(@project_id,@current_user,@targetEmail,@privileges).should.equal true
describe "viewInvite", ->
@@ -604,7 +442,7 @@ describe "CollaboratorsInviteController", ->
user: _id: @current_user_id = "current-user-id"
@res.render = sinon.stub()
@res.sendStatus = sinon.stub()
- @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(2, null)
+ @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(3, null)
@callback = sinon.stub()
@next = sinon.stub()
@@ -624,7 +462,7 @@ describe "CollaboratorsInviteController", ->
beforeEach ->
@err = new Error('woops')
- @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(2, @err)
+ @CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(3, @err)
@CollaboratorsInviteController.resendInvite @req, @res, @next
it 'should not produce a 201 response', ->
@@ -637,15 +475,16 @@ describe "CollaboratorsInviteController", ->
it 'should have called resendInvite', ->
@CollaboratorsInviteHandler.resendInvite.callCount.should.equal 1
-
describe "revokeInvite", ->
beforeEach ->
@req.params =
Project_id: @project_id
invite_id: @invite_id = "thuseoautoh"
+ @current_user =
+ _id: @current_user_id = "current-user-id"
@req.session =
- user: _id: @current_user_id = "current-user-id"
+ user: @current_user
@res.render = sinon.stub()
@res.sendStatus = sinon.stub()
@CollaboratorsInviteHandler.revokeInvite = sinon.stub().callsArgWith(2, null)
@@ -698,7 +537,6 @@ describe "CollaboratorsInviteController", ->
@res.render = sinon.stub()
@res.redirect = sinon.stub()
@CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, null)
- @CollaboratorsInviteController._tryCancelInviteNotification = sinon.stub()
@callback = sinon.stub()
@next = sinon.stub()
@@ -718,9 +556,6 @@ describe "CollaboratorsInviteController", ->
@EditorRealTimeController.emitToRoom.callCount.should.equal 1
@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'project:membership:changed').should.equal true
- it 'should call _tryCancelInviteNotification', ->
- @CollaboratorsInviteController._tryCancelInviteNotification.callCount.should.equal 1
-
describe 'when revokeInvite produces an error', ->
beforeEach ->
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
index 0e8ceef0b3..d4ae9a4229 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee
@@ -24,14 +24,20 @@ describe "CollaboratorsInviteHandler", ->
@Crypto = Crypto
@CollaboratorsInviteHandler = SandboxedModule.require modulePath, requires:
'settings-sharelatex': @settings = {}
+ '../../models/ProjectInvite': {ProjectInvite: @ProjectInvite}
'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
'./CollaboratorsEmailHandler': @CollaboratorsEmailHandler = {}
"./CollaboratorsHandler": @CollaboratorsHandler = {addUserIdToProject: sinon.stub()}
- '../../models/ProjectInvite': {ProjectInvite: @ProjectInvite}
+ '../User/UserGetter': @UserGetter = {getUser: sinon.stub()}
+ "../Project/ProjectGetter": @ProjectGetter = {}
+ "../Notifications/NotificationsBuilder": @NotificationsBuilder = {}
'crypto': @Crypto
@projectId = ObjectId()
@sendingUserId = ObjectId()
+ @sendingUser =
+ _id: @sendingUserId
+ name: "Bob"
@email = "user@example.com"
@userId = ObjectId()
@user =
@@ -125,9 +131,9 @@ describe "CollaboratorsInviteHandler", ->
beforeEach ->
@ProjectInvite::save = sinon.spy (cb) -> cb(null, this)
@randomBytesSpy = sinon.spy(@Crypto, 'randomBytes')
- @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub()
+ @CollaboratorsInviteHandler._sendMessages = sinon.stub().callsArgWith(3, null)
@call = (callback) =>
- @CollaboratorsInviteHandler.inviteToProject @projectId, @sendingUserId, @email, @privileges, callback
+ @CollaboratorsInviteHandler.inviteToProject @projectId, @sendingUser, @email, @privileges, callback
afterEach ->
@randomBytesSpy.restore()
@@ -160,10 +166,10 @@ describe "CollaboratorsInviteHandler", ->
@ProjectInvite::save.callCount.should.equal 1
done()
- it 'should have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) ->
+ it 'should have called _sendMessages', (done) ->
@call (err, invite) =>
- @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1
- @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @email).should.equal true
+ @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 1
+ @CollaboratorsInviteHandler._sendMessages.calledWith(@projectId, @sendingUser).should.equal true
done()
describe 'when saving model produces an error', ->
@@ -176,10 +182,64 @@ describe "CollaboratorsInviteHandler", ->
expect(err).to.be.instanceof Error
done()
+ describe '_sendMessages', ->
+
+ beforeEach ->
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(3, null)
+ @CollaboratorsInviteHandler._trySendInviteNotification = sinon.stub().callsArgWith(3, null)
+ @call = (callback) =>
+ @CollaboratorsInviteHandler._sendMessages @projectId, @sendingUser, @fakeInvite, callback
+
+ describe 'when all goes well', ->
+
+ it 'should not produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.not.be.instanceof Error
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should call CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) ->
+ @call (err) =>
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @fakeInvite.email, @fakeInvite).should.equal true
+ done()
+
+ it 'should call _trySendInviteNotification', (done) ->
+ @call (err) =>
+ @CollaboratorsInviteHandler._trySendInviteNotification.callCount.should.equal 1
+ @CollaboratorsInviteHandler._trySendInviteNotification.calledWith(@projectId, @sendingUser, @fakeInvite).should.equal true
+ done()
+
+ describe 'when CollaboratorsEmailHandler.notifyUserOfProjectInvite produces an error', ->
+
+ beforeEach ->
+ @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub().callsArgWith(3, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err, invite) =>
+ expect(err).to.be.instanceof Error
+ done()
+
+ it 'should not call _trySendInviteNotification', (done) ->
+ @call (err) =>
+ @CollaboratorsInviteHandler._trySendInviteNotification.callCount.should.equal 0
+ done()
+
+ describe 'when _trySendInviteNotification produces an error', ->
+
+ beforeEach ->
+ @CollaboratorsInviteHandler._trySendInviteNotification = sinon.stub().callsArgWith(3, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err, invite) =>
+ expect(err).to.be.instanceof Error
+ done()
+
describe 'revokeInvite', ->
beforeEach ->
@ProjectInvite.remove.callsArgWith(1, null)
+ @CollaboratorsInviteHandler._tryCancelInviteNotification = sinon.stub().callsArgWith(1, null)
@call = (callback) =>
@CollaboratorsInviteHandler.revokeInvite @projectId, @inviteId, callback
@@ -199,6 +259,12 @@ describe "CollaboratorsInviteHandler", ->
@ProjectInvite.remove.calledWith({projectId: @projectId, _id: @inviteId}).should.equal true
done()
+ it 'should call _tryCancelInviteNotification', (done) ->
+ @call (err) =>
+ @CollaboratorsInviteHandler._tryCancelInviteNotification.callCount.should.equal 1
+ @CollaboratorsInviteHandler._tryCancelInviteNotification.calledWith(@inviteId).should.equal true
+ done()
+
describe 'when remove produces an error', ->
beforeEach ->
@@ -213,9 +279,9 @@ describe "CollaboratorsInviteHandler", ->
beforeEach ->
@ProjectInvite.findOne.callsArgWith(1, null, @fakeInvite)
- @CollaboratorsEmailHandler.notifyUserOfProjectInvite = sinon.stub()
+ @CollaboratorsInviteHandler._sendMessages = sinon.stub().callsArgWith(3, null)
@call = (callback) =>
- @CollaboratorsInviteHandler.resendInvite @projectId, @inviteId, callback
+ @CollaboratorsInviteHandler.resendInvite @projectId, @sendingUser, @inviteId, callback
describe 'when all goes well', ->
@@ -233,10 +299,10 @@ describe "CollaboratorsInviteHandler", ->
@ProjectInvite.findOne.calledWith({_id: @inviteId, projectId: @projectId}).should.equal true
done()
- it 'should have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) ->
+ it 'should have called _sendMessages', (done) ->
@call (err, invite) =>
- @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 1
- @CollaboratorsEmailHandler.notifyUserOfProjectInvite.calledWith(@projectId, @email).should.equal true
+ @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 1
+ @CollaboratorsInviteHandler._sendMessages.calledWith(@projectId, @sendingUser, @fakeInvite).should.equal true
done()
describe 'when findOne produces an error', ->
@@ -249,9 +315,9 @@ describe "CollaboratorsInviteHandler", ->
expect(err).to.be.instanceof Error
done()
- it 'should not have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) ->
+ it 'should not have called _sendMessages', (done) ->
@call (err, invite) =>
- @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 0
+ @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 0
done()
describe 'when findOne does not find an invite', ->
@@ -265,9 +331,9 @@ describe "CollaboratorsInviteHandler", ->
expect(err).to.be.oneOf [null, undefined]
done()
- it 'should not have called CollaboratorsEmailHandler.notifyUserOfProjectInvite', (done) ->
+ it 'should not have called _sendMessages', (done) ->
@call (err, invite) =>
- @CollaboratorsEmailHandler.notifyUserOfProjectInvite.callCount.should.equal 0
+ @CollaboratorsInviteHandler._sendMessages.callCount.should.equal 0
done()
describe 'getInviteByToken', ->
@@ -335,6 +401,7 @@ describe "CollaboratorsInviteHandler", ->
@CollaboratorsHandler.addUserIdToProject.callsArgWith(4, null)
@_getInviteByToken = sinon.stub(@CollaboratorsInviteHandler, 'getInviteByToken')
@_getInviteByToken.callsArgWith(2, null, @fakeInvite)
+ @CollaboratorsInviteHandler._tryCancelInviteNotification = sinon.stub().callsArgWith(1, null)
@ProjectInvite.remove.callsArgWith(1, null)
@call = (callback) =>
@CollaboratorsInviteHandler.acceptInvite @projectId, @inviteId, @token, @user, callback
@@ -494,3 +561,163 @@ describe "CollaboratorsInviteHandler", ->
@call (err) =>
@ProjectInvite.remove.callCount.should.equal 1
done()
+
+ describe '_tryCancelInviteNotification', ->
+ beforeEach ->
+ @inviteId = ObjectId()
+ @currentUser = {_id: ObjectId()}
+ @notification = {read: sinon.stub().callsArgWith(0, null)}
+ @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification)
+ @call = (callback) =>
+ @CollaboratorsInviteHandler._tryCancelInviteNotification @inviteId, callback
+
+ it 'should not produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should call notification.read', (done) ->
+ @call (err) =>
+ @notification.read.callCount.should.equal 1
+ done()
+
+ describe 'when notification.read produces an error', ->
+ beforeEach ->
+ @notification = {read: sinon.stub().callsArgWith(0, new Error('woops'))}
+ @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification)
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ done()
+
+ describe "_trySendInviteNotification", ->
+
+ beforeEach ->
+ @invite =
+ _id: ObjectId(),
+ token: "some_token",
+ sendingUserId: ObjectId(),
+ projectId: @project_id,
+ targetEmail: 'user@example.com'
+ createdAt: new Date(),
+ @sendingUser =
+ _id: ObjectId()
+ first_name: "jim"
+ @existingUser = {_id: ObjectId()}
+ @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @existingUser)
+ @fakeProject =
+ _id: @project_id
+ name: "some project"
+ @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @fakeProject)
+ @notification = {create: sinon.stub().callsArgWith(0, null)}
+ @NotificationsBuilder.projectInvite = sinon.stub().returns(@notification)
+ @call = (callback) =>
+ @CollaboratorsInviteHandler._trySendInviteNotification @project_id, @sendingUser, @invite, callback
+
+ describe 'when the user exists', ->
+
+ beforeEach ->
+
+ it 'should not produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should call getUser', (done) ->
+ @call (err) =>
+ @UserGetter.getUser.callCount.should.equal 1
+ @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
+ done()
+
+ it 'should call getProject', (done) ->
+ @call (err) =>
+ @ProjectGetter.getProject.callCount.should.equal 1
+ @ProjectGetter.getProject.calledWith(@project_id).should.equal true
+ done()
+
+ it 'should call NotificationsBuilder.projectInvite.create', (done) ->
+ @call (err) =>
+ @NotificationsBuilder.projectInvite.callCount.should.equal 1
+ @notification.create.callCount.should.equal 1
+ done()
+
+ describe 'when getProject produces an error', ->
+
+ beforeEach ->
+ @ProjectGetter.getProject.callsArgWith(2, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ done()
+
+ it 'should not call NotificationsBuilder.projectInvite.create', (done) ->
+ @call (err) =>
+ @NotificationsBuilder.projectInvite.callCount.should.equal 0
+ @notification.create.callCount.should.equal 0
+ done()
+
+ describe 'when projectInvite.create produces an error', ->
+
+ beforeEach ->
+ @notification.create.callsArgWith(0, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ done()
+
+ describe 'when the user does not exist', ->
+
+ beforeEach ->
+ @UserGetter.getUser = sinon.stub().callsArgWith(2, null, null)
+
+ it 'should not produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.oneOf [null, undefined]
+ done()
+
+ it 'should call getUser', (done) ->
+ @call (err) =>
+ @UserGetter.getUser.callCount.should.equal 1
+ @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
+ done()
+
+ it 'should not call getProject', (done) ->
+ @call (err) =>
+ @ProjectGetter.getProject.callCount.should.equal 0
+ done()
+
+ it 'should not call NotificationsBuilder.projectInvite.create', (done) ->
+ @call (err) =>
+ @NotificationsBuilder.projectInvite.callCount.should.equal 0
+ @notification.create.callCount.should.equal 0
+ done()
+
+ describe 'when the getUser produces an error', ->
+
+ beforeEach ->
+ @UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops'))
+
+ it 'should produce an error', (done) ->
+ @call (err) =>
+ expect(err).to.be.instanceof Error
+ done()
+
+ it 'should call getUser', (done) ->
+ @call (err) =>
+ @UserGetter.getUser.callCount.should.equal 1
+ @UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
+ done()
+
+ it 'should not call getProject', (done) ->
+ @call (err) =>
+ @ProjectGetter.getProject.callCount.should.equal 0
+ done()
+
+ it 'should not call NotificationsBuilder.projectInvite.create', (done) ->
+ @call (err) =>
+ @NotificationsBuilder.projectInvite.callCount.should.equal 0
+ @notification.create.callCount.should.equal 0
+ done()
diff --git a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
index 9629bca8c7..0aafe6e5fc 100644
--- a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
@@ -61,9 +61,10 @@ describe 'NotificationsHandler', ->
@messageOpts = {value:12344}
@templateKey = "renderThisHtml"
@expiry = null
+ @forceCreate = false
it "should post the message over", (done)->
- @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, =>
+ @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, @forceCreate, =>
args = @request.args[0][0]
args.uri.should.equal "#{notificationUrl}/user/#{user_id}"
args.timeout.should.equal 1000
@@ -77,9 +78,10 @@ describe 'NotificationsHandler', ->
@messageOpts = {value:12344}
@templateKey = "renderThisHtml"
@expiry = new Date()
+ @forceCreate = false
it 'should post the message over with expiry field', (done) ->
- @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, =>
+ @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, @forceCreate, =>
args = @request.args[0][0]
args.uri.should.equal "#{notificationUrl}/user/#{user_id}"
args.timeout.should.equal 1000
@@ -87,6 +89,23 @@ describe 'NotificationsHandler', ->
assert.deepEqual(args.json, expectedJson)
done()
+ describe 'when forceCreate is true', ->
+ beforeEach ->
+ @key = "some key here"
+ @messageOpts = {value:12344}
+ @templateKey = "renderThisHtml"
+ @expiry = null
+ @forceCreate = true
+
+ it 'should add a forceCreate=true flag to the payload', (done) ->
+ @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, @forceCreate, =>
+ args = @request.args[0][0]
+ args.uri.should.equal "#{notificationUrl}/user/#{user_id}"
+ args.timeout.should.equal 1000
+ expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts, forceCreate: @forceCreate}
+ assert.deepEqual(args.json, expectedJson)
+ done()
+
describe "markAsReadByKeyOnly", ->
beforeEach ->
@key = "some key here"
From 79d9e5445899225fb9b6aae6d84dafcde9bed2f2 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Wed, 17 Aug 2016 17:34:04 +0100
Subject: [PATCH 165/378] Basic port of Stripe JS credit card validation and
formatting lib.
---
.../coffee/directives/creditCards.coffee | 395 ++++++++++++++++++
1 file changed, 395 insertions(+)
create mode 100644 services/web/public/coffee/directives/creditCards.coffee
diff --git a/services/web/public/coffee/directives/creditCards.coffee b/services/web/public/coffee/directives/creditCards.coffee
new file mode 100644
index 0000000000..ae9df139a9
--- /dev/null
+++ b/services/web/public/coffee/directives/creditCards.coffee
@@ -0,0 +1,395 @@
+define [
+ "base"
+], (App) ->
+ App.factory 'ccUtils', () ->
+ defaultFormat = /(\d{1,4})/g;
+ defaultInputFormat = /(?:^|\s)(\d{4})$/
+
+ cards = [
+ # Credit cards
+ {
+ type: 'visa'
+ patterns: [4]
+ format: defaultFormat
+ length: [13, 16]
+ cvcLength: [3]
+ luhn: true
+ }
+ {
+ type: 'mastercard'
+ patterns: [
+ 51, 52, 53, 54, 55,
+ 22, 23, 24, 25, 26, 27
+ ]
+ format: defaultFormat
+ length: [16]
+ cvcLength: [3]
+ luhn: true
+ }
+ {
+ type: 'amex'
+ patterns: [34, 37]
+ format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/
+ length: [15]
+ cvcLength: [3..4]
+ luhn: true
+ }
+ {
+ type: 'dinersclub'
+ patterns: [30, 36, 38, 39]
+ format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/
+ length: [14]
+ cvcLength: [3]
+ luhn: true
+ }
+ {
+ type: 'discover'
+ patterns: [60, 64, 65, 622]
+ format: defaultFormat
+ length: [16]
+ cvcLength: [3]
+ luhn: true
+ }
+ {
+ type: 'unionpay'
+ patterns: [62, 88]
+ format: defaultFormat
+ length: [16..19]
+ cvcLength: [3]
+ luhn: false
+ }
+ {
+ type: 'jcb'
+ patterns: [35]
+ format: defaultFormat
+ length: [16]
+ cvcLength: [3]
+ luhn: true
+ }
+ ]
+
+ cardFromNumber = (num) ->
+ num = (num + '').replace(/\D/g, "")
+ for card in cards
+ for pattern in card.patterns
+ p = pattern + ""
+ return card if num.substr(0, p.length) == p
+
+ cardFromType = (type) ->
+ return card for card in cards when card.type is type
+
+ parseExpiry = (value = "") ->
+ [month, year] = value.split(/[\s\/]+/, 2)
+
+ # Allow for year shortcut
+ if year?.length is 2 and /^\d+$/.test(year)
+ prefix = (new Date).getFullYear()
+ prefix = prefix.toString()[0..1]
+ year = prefix + year
+
+ month = parseInt(month, 10)
+ year = parseInt(year, 10)
+
+ month: month, year: year
+
+ return {
+ fromNumber: cardFromNumber
+ fromType: cardFromType
+ defaultFormat: defaultFormat
+ defaultInputFormat: defaultInputFormat
+ parseExpiry: parseExpiry
+ }
+
+ App.factory 'ccFormat', (ccUtils, $filter) ->
+ hasTextSelected = ($target) ->
+ # If some text is selected
+ return true if $target.prop('selectionStart')? and
+ $target.prop('selectionStart') isnt $target.prop('selectionEnd')
+
+ # If some text is selected in IE
+ if document?.selection?.createRange?
+ return true if document.selection.createRange().text
+
+ false
+
+ # Replace Full-Width Chars
+ replaceFullWidthChars = (str = '') ->
+ fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'
+ halfWidth = '0123456789'
+
+ value = ''
+ chars = str.split('')
+
+ # Avoid using reserved word `char`
+ for chr in chars
+ idx = fullWidth.indexOf(chr)
+ chr = halfWidth[idx] if idx > -1
+ value += chr
+
+ value
+
+ # Format Numeric
+ reFormatNumeric = (e) ->
+ $target = $(e.currentTarget)
+ setTimeout ->
+ value = $target.val()
+ value = replaceFullWidthChars(value)
+ value = value.replace(/\D/g, '')
+ safeVal(value, $target)
+
+ # Format Card Number
+ reFormatCardNumber = (e) ->
+ $target = $(e.currentTarget)
+ setTimeout ->
+ value = $target.val()
+ value = replaceFullWidthChars(value)
+ value = $.payment.formatCardNumber(value)
+ #safeVal(value, $target)
+
+ formatCardNumber = (e) ->
+ # Only format if input is a number
+ digit = String.fromCharCode(e.which)
+ return unless /^\d+$/.test(digit)
+
+ $target = $(e.currentTarget)
+ value = $target.val()
+ card = ccUtils.cardFromNumber(value + digit)
+ length = (value.replace(/\D/g, '') + digit).length
+
+ upperLength = 16
+ upperLength = card.length[card.length.length - 1] if card
+ return if length >= upperLength
+
+ # Return if focus isn't at the end of the text
+ return if $target.prop('selectionStart')? and
+ $target.prop('selectionStart') isnt value.length
+
+ if card && card.type is 'amex'
+ # AMEX cards are formatted differently
+ re = /^(\d{4}|\d{4}\s\d{6})$/
+ else
+ re = /(?:^|\s)(\d{4})$/
+
+ # If '4242' + 4
+ if re.test(value)
+ e.preventDefault()
+ setTimeout -> $target.val(value + ' ' + digit)
+
+ # If '424' + 2
+ else if re.test(value + digit)
+ e.preventDefault()
+ setTimeout -> $target.val(value + digit + ' ')
+
+ formatBackCardNumber = (e) ->
+ $target = $(e.currentTarget)
+ value = $target.val()
+
+ # Return unless backspacing
+ return unless e.which is 8
+
+ # Return if focus isn't at the end of the text
+ return if $target.prop('selectionStart')? and
+ $target.prop('selectionStart') isnt value.length
+
+ # Remove the digit + trailing space
+ if /\d\s$/.test(value)
+ e.preventDefault()
+ setTimeout -> $target.val(value.replace(/\d\s$/, ''))
+ # Remove digit if ends in space + digit
+ else if /\s\d?$/.test(value)
+ e.preventDefault()
+ setTimeout -> $target.val(value.replace(/\d$/, ''))
+
+ # Format Expiry
+ reFormatExpiry = (e) ->
+ $target = $(e.currentTarget)
+ setTimeout ->
+ value = $target.val()
+ value = replaceFullWidthChars(value)
+ value = $.payment.formatExpiry(value)
+ safeVal(value, $target)
+
+ formatExpiry = (e) ->
+ # Only format if input is a number
+ digit = String.fromCharCode(e.which)
+ return unless /^\d+$/.test(digit)
+
+ $target = $(e.currentTarget)
+ val = $target.val() + digit
+
+ if /^\d$/.test(val) and val not in ['0', '1']
+ e.preventDefault()
+ setTimeout -> $target.val("0#{val} / ")
+
+ else if /^\d\d$/.test(val)
+ e.preventDefault()
+ setTimeout ->
+ # Split for months where we have the second digit > 2 (past 12) and turn
+ # that into (m1)(m2) => 0(m1) / (m2)
+ m1 = parseInt(val[0], 10)
+ m2 = parseInt(val[1], 10)
+ if m2 > 2 and m1 != 0
+ $target.val("0#{m1} / #{m2}")
+ else
+ $target.val("#{val} / ")
+
+ formatForwardExpiry = (e) ->
+ digit = String.fromCharCode(e.which)
+ return unless /^\d+$/.test(digit)
+
+ $target = $(e.currentTarget)
+ val = $target.val()
+
+ if /^\d\d$/.test(val)
+ $target.val("#{val} / ")
+
+ formatForwardSlash = (e) ->
+ which = String.fromCharCode(e.which)
+ return unless which is '/' or which is ' '
+
+ $target = $(e.currentTarget)
+ val = $target.val()
+
+ if /^\d$/.test(val) and val isnt '0'
+ $target.val("0#{val} / ")
+
+ formatBackExpiry = (e) ->
+ $target = $(e.currentTarget)
+ value = $target.val()
+
+ # Return unless backspacing
+ return unless e.which is 8
+
+ # Return if focus isn't at the end of the text
+ return if $target.prop('selectionStart')? and
+ $target.prop('selectionStart') isnt value.length
+
+ # Remove the trailing space + last digit
+ if /\d\s\/\s$/.test(value)
+ e.preventDefault()
+ setTimeout -> $target.val(value.replace(/\d\s\/\s$/, ''))
+
+ parseExpiry = (value) ->
+ if value?
+ dateAsObj = ccUtils.parseExpiry(value);
+ expiry = new Date dateAsObj.year, dateAsObj.month - 1
+ return $filter('date')(expiry, 'MM/yyyy')
+
+ # Format CVC
+ reFormatCVC = (e) ->
+ $target = $(e.currentTarget)
+ setTimeout ->
+ value = $target.val()
+ value = replaceFullWidthChars(value)
+ value = value.replace(/\D/g, '')[0...4]
+ safeVal(value, $target)
+
+ # Restrictions
+ restrictNumeric = (e) ->
+ # Key event is for a browser shortcut
+ return true if e.metaKey or e.ctrlKey
+
+ # If keycode is a space
+ return false if e.which is 32
+
+ # If keycode is a special char (WebKit)
+ return true if e.which is 0
+
+ # If char is a special char (Firefox)
+ return true if e.which < 33
+
+ input = String.fromCharCode(e.which)
+
+ # Char is a number or a space
+ !!/[\d\s]/.test(input)
+
+ restrictCardNumber = (e) ->
+ $target = $(e.currentTarget)
+ digit = String.fromCharCode(e.which)
+ return unless /^\d+$/.test(digit)
+
+ return if hasTextSelected($target)
+
+ # Restrict number of digits
+ value = ($target.val() + digit).replace(/\D/g, '')
+ card = cardFromNumber(value)
+
+ if card
+ value.length <= card.length[card.length.length - 1]
+ else
+ # All other cards are 16 digits long
+ value.length <= 16
+
+ restrictExpiry = (e) ->
+ $target = $(e.currentTarget)
+ digit = String.fromCharCode(e.which)
+ return unless /^\d+$/.test(digit)
+
+ return if hasTextSelected($target)
+
+ value = $target.val() + digit
+ value = value.replace(/\D/g, '')
+
+ return false if value.length > 6
+
+ restrictCVC = (e) ->
+ $target = $(e.currentTarget)
+ digit = String.fromCharCode(e.which)
+ return unless /^\d+$/.test(digit)
+
+ return if hasTextSelected($target)
+
+ val = $target.val() + digit
+ val.length <= 4
+
+ setCardType = (e) ->
+ $target = $(e.currentTarget)
+ val = $target.val()
+ cardType = $.payment.cardType(val) or 'unknown'
+
+ unless $target.hasClass(cardType)
+ allTypes = (card.type for card in cards)
+
+ $target.removeClass('unknown')
+ $target.removeClass(allTypes.join(' '))
+
+ $target.addClass(cardType)
+ $target.toggleClass('identified', cardType isnt 'unknown')
+ $target.trigger('payment.cardType', cardType)
+
+ return {
+ hasTextSelected
+ replaceFullWidthChars
+ reFormatNumeric
+ reFormatCardNumber
+ formatCardNumber
+ formatBackCardNumber
+ reFormatExpiry
+ formatExpiry
+ formatForwardExpiry
+ formatForwardSlash
+ formatBackExpiry
+ parseExpiry
+ reFormatCVC
+ restrictNumeric
+ restrictCardNumber
+ restrictExpiry
+ restrictCVC
+ setCardType
+ }
+
+ App.directive 'ccFormatExpiry', (ccFormat) ->
+ restrict: 'A'
+ require: 'ngModel'
+ link: (scope, el, attrs, ngModel) ->
+ el.on 'keypress', ccFormat.restrictExpiry
+ el.on 'keypress', ccFormat.formatExpiry
+ el.on 'keypress', ccFormat.formatForwardSlash
+ el.on 'keypress', ccFormat.formatForwardExpiry
+ el.on 'keydown', ccFormat.formatBackExpiry
+
+ ngModel.$parsers.push ccFormat.parseExpiry
+ ngModel.$formatters.push ccFormat.parseExpiry
+
+
+
\ No newline at end of file
From 4814fc2606b0fea6de44e728ea913154fc7e8a9b Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Wed, 17 Aug 2016 17:34:17 +0100
Subject: [PATCH 166/378] Simple layout for subscription form.
---
services/web/app/views/subscriptions/new.jade | 56 ++++++++++++++++++-
services/web/public/coffee/main.coffee | 1 +
.../coffee/main/new-subscription.coffee | 5 +-
services/web/public/stylesheets/style.less | 1 +
4 files changed, 61 insertions(+), 2 deletions(-)
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade
index 7613ec463e..d62e8e0de2 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.jade
@@ -176,7 +176,54 @@ block content
div.pull-right
strong {{price.currency.symbol}}{{price.next.total}}
.col-md-12(sixpack-when="simple")
- span Lorem ipsum!
+ form(ng-show="planName")
+ .row
+ .col-md-12
+ .form-group
+ .row
+ .col-md-6
+ label.radio-inline
+ input.paymentTypeOption(type="radio", value="credit_card", ng-model="paymentMethod")
+ i.fa.fa-cc-mastercard.fa-3x
+ span
+ i.fa.fa-cc-visa.fa-3x
+ .col-md-6
+ label.radio-inline
+ input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod")
+ i.fa.fa-cc-paypal.fa-3x
+ //- TODO Validation messages
+ .form-group
+ label(for="card-name") Name on card
+ input#card-name.form-control(type="text")
+ .form-group(ng-class="validation.number == false || validation.errorFields.number ? 'has-error' : ''")
+ label(for="card-no") Card number
+ input#card-no.form-control(type="credit card", ng-model='data.number', ng-blur="validateCardNumber()")
+ .row
+ .col-xs-6
+ label Valid thru {{ data.mmYY }}
+ input.form-control(
+ type="text"
+ ng-model="data.mmYY"
+ cc-format-expiry
+ )
+ .col-xs-6
+ .form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
+ label Security code
+ input.form-control(type="cvc", ng-model='data.cvv', ng-blur="validateCvv()")
+ .form-control-feedback
+ a.form-helper(
+ href
+ tooltip-template="'cvv-tooltip-tpl.html'"
+ tooltip-trigger="mouseenter"
+ tooltip-append-to-body="true"
+ ) ?
+ .form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
+ label(for="country") Country
+ select#country.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required)
+ mixin countries_options()
+ .row
+ .col-xs-12
+ button.btn.btn-success.btn-block(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") #{translate("upgrade_now")}
span(sixpack-switch="payment-left-menu-bottom")
@@ -272,6 +319,13 @@ block content
script(type="text/javascript").
ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
+ script(
+ type="text/ng-template"
+ id="cvv-tooltip-tpl.html"
+ )
+ p For #[strong Visa, MasterCard and Discover], the #[strong 3 digits] on the #[strong back] of your card.
+ p For #[strong American Express], the #[strong 4 digits] on the #[strong front] of your card.
+
mixin countries_options()
option(value='', disabled, selected) #{translate("country")}
option(value='-') --------------
diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee
index d85d89cfe8..c723031016 100644
--- a/services/web/public/coffee/main.coffee
+++ b/services/web/public/coffee/main.coffee
@@ -26,6 +26,7 @@ define [
"directives/onEnter"
"directives/selectAll"
"directives/maxHeight"
+ "directives/creditCards"
"services/queued-http"
"filters/formatDate"
"__MAIN_CLIENTSIDE_INCLUDES__"
diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee
index c153392c15..47c1c28a34 100644
--- a/services/web/public/coffee/main/new-subscription.coffee
+++ b/services/web/public/coffee/main/new-subscription.coffee
@@ -1,5 +1,6 @@
define [
- "base"
+ "base",
+ "directives/creditCards"
], (App)->
App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking)->
@@ -14,6 +15,7 @@ define [
$scope.paymentMethod = "credit_card"
+
$scope.data =
number: ""
month: ""
@@ -28,6 +30,7 @@ define [
city:""
country:window.countryCode
coupon: window.couponCode
+ mmYY: ""
$scope.validation =
diff --git a/services/web/public/stylesheets/style.less b/services/web/public/stylesheets/style.less
index b02891425c..2abb57388f 100755
--- a/services/web/public/stylesheets/style.less
+++ b/services/web/public/stylesheets/style.less
@@ -75,6 +75,7 @@
@import "app/wiki.less";
@import "app/translations.less";
@import "app/contact-us.less";
+@import "app/subscription.less";
@import "app/sprites.less";
@import "../js/libs/pdfListView/TextLayer.css";
From 090f10e3be90555a5a770a43566b88547191f4d1 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 18 Aug 2016 09:47:57 +0100
Subject: [PATCH 167/378] add log hints for new chktex messages
---
.../HumanReadableLogsRules.coffee | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
index c61c61506b..7d31bf3f43 100644
--- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
+++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
@@ -102,5 +102,22 @@ define -> [
humanReadableHint: """
You have used an open bracket without a corresponding close bracket.
"""
-
+ , ruleId: "hint_mismatched_environment2"
+ regexToMatch: /Error: `\\end\{([^\}]+)\})' expected but found `\\end\{([^\}]+)\}'.*/
+ newMessage: "Error: environment does not match \\begin{$1} ... \\end{$2}"
+ humanReadableHint: """
+ You have used \\begin{...} without a corresponding \\end{...}.
+ """
+ , ruleId: "hint_mismatched_environment3"
+ regexToMatch: /Error: No matching \\end found for `\\begin\{([^\}]+)\}'.*/
+ newMessage: "Error: No matching \\end found for \\begin{$1}"
+ humanReadableHint: """
+ You have used \\begin{...} without a corresponding \\end{...}.
+ """
+ , ruleId: "hint_mismatched_environment3"
+ regexToMatch: /Error: Found `\\end\{([^\}]+)\}' without corresponding \\begin.*/
+ newMessage: "Error: Found \\end{$1} without a corresponding \\begin{$1}"
+ humanReadableHint: """
+ You have used \\begin{...} without a corresponding \\end{...}.
+ """
]
From 1af5017e49e2d2cc0c076aee183d4e0e013666de Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Thu, 18 Aug 2016 10:39:55 +0100
Subject: [PATCH 168/378] Better integration with Angular models.
---
.../web/public/coffee/directives/creditCards.coffee | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/services/web/public/coffee/directives/creditCards.coffee b/services/web/public/coffee/directives/creditCards.coffee
index ae9df139a9..70b485252a 100644
--- a/services/web/public/coffee/directives/creditCards.coffee
+++ b/services/web/public/coffee/directives/creditCards.coffee
@@ -90,6 +90,8 @@ define [
month = parseInt(month, 10)
year = parseInt(year, 10)
+ return unless !isNaN(month) and !isNaN(year)
+
month: month, year: year
return {
@@ -271,8 +273,14 @@ define [
parseExpiry = (value) ->
if value?
- dateAsObj = ccUtils.parseExpiry(value);
+ dateAsObj = ccUtils.parseExpiry(value)
+
+ console.log dateAsObj
+
+ return unless dateAsObj?
+
expiry = new Date dateAsObj.year, dateAsObj.month - 1
+
return $filter('date')(expiry, 'MM/yyyy')
# Format CVC
From 109e79db9930ba32e8a7f86a4da9483380ed8f87 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 18 Aug 2016 13:21:27 +0100
Subject: [PATCH 169/378] track cascading errors in Human Readable Log Hints
---
.../human-readable-logs/HumanReadableLogs.coffee | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee
index e59e35a40c..61cc1b7df7 100644
--- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee
+++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee
@@ -11,6 +11,8 @@ define [
_getRule = (logMessage) ->
return rule for rule in ruleset when rule.regexToMatch.test logMessage
+ seenErrorTypes = {} # keep track of types of errors seen
+
for entry in parsedLogEntries.all
ruleDetails = _getRule entry.message
@@ -21,8 +23,20 @@ define [
entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1)
if ruleDetails.newMessage?
entry.message = entry.message.replace ruleDetails.regexToMatch, ruleDetails.newMessage
+ # suppress any entries that are known to cascade from previous error types
+ if ruleDetails.cascadesFrom?
+ for type in ruleDetails.cascadesFrom
+ entry.suppressed = true if seenErrorTypes[type]
+ # record the types of errors seen
+ if ruleDetails.types?
+ for type in ruleDetails.types
+ seenErrorTypes[type] = true
entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint?
entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL?
-
+
+ # filter out the suppressed errors (from the array entries in parsedLogEntries)
+ for key, errors of parsedLogEntries when typeof errors is 'object' and errors.length > 0
+ parsedLogEntries[key] = (err for err in errors when not err.suppressed)
+
return parsedLogEntries
From 133250c15092be0ca8c4935a950530a299c2bb60 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 18 Aug 2016 13:28:47 +0100
Subject: [PATCH 170/378] extend log hints for more chktex errors
---
.../HumanReadableLogsRules.coffee | 33 ++++++++++++-------
1 file changed, 21 insertions(+), 12 deletions(-)
diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
index 7d31bf3f43..9436d0705d 100644
--- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
+++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
@@ -90,6 +90,7 @@ define -> [
"""
,
ruleId: "hint_mismatched_environment"
+ types: ['environment']
regexToMatch: /Error: `([^']{2,})' expected, found `([^']{2,})'.*/
newMessage: "Error: environment does not match \\begin{$1} ... \\end{$2}"
humanReadableHint: """
@@ -97,27 +98,35 @@ define -> [
"""
,
ruleId: "hint_mismatched_brackets"
+ types: ['environment']
regexToMatch: /Error: `([^a-zA-Z0-9])' expected, found `([^a-zA-Z0-9])'.*/
newMessage: "Error: brackets do not match, found '$2' instead of '$1'"
humanReadableHint: """
You have used an open bracket without a corresponding close bracket.
"""
- , ruleId: "hint_mismatched_environment2"
- regexToMatch: /Error: `\\end\{([^\}]+)\})' expected but found `\\end\{([^\}]+)\}'.*/
- newMessage: "Error: environment does not match \\begin{$1} ... \\end{$2}"
+ ,
+ ruleId: "hint_mismatched_environment2"
+ types: ['environment']
+ regexToMatch: /Error: `\\end\{([^\}]+)\}' expected but found `\\end\{([^\}]+)\}'.*/
+ newMessage: "Error: environments do not match: \\begin{$1} ... \\end{$2}"
humanReadableHint: """
- You have used \\begin{...} without a corresponding \\end{...}.
+ You have used \\begin{} without a corresponding \\end{}.
"""
- , ruleId: "hint_mismatched_environment3"
- regexToMatch: /Error: No matching \\end found for `\\begin\{([^\}]+)\}'.*/
- newMessage: "Error: No matching \\end found for \\begin{$1}"
+ ,
+ ruleId: "hint_mismatched_environment3"
+ types: ['environment']
+ regexToMatch: /Warning: No matching \\end found for `\\begin\{([^\}]+)\}'.*/
+ newMessage: "Warning: No matching \\end found for \\begin{$1}"
humanReadableHint: """
- You have used \\begin{...} without a corresponding \\end{...}.
+ You have used \\begin{} without a corresponding \\end{}.
"""
- , ruleId: "hint_mismatched_environment3"
- regexToMatch: /Error: Found `\\end\{([^\}]+)\}' without corresponding \\begin.*/
- newMessage: "Error: Found \\end{$1} without a corresponding \\begin{$1}"
+ ,
+ ruleId: "hint_mismatched_environment4"
+ types: ['environment']
+ cascadesFrom: ['environment']
+ regexToMatch: /Error: Found `\\end\{([^\}]+)\}' without corresponding \\begin.*/
+ newMessage: "Error: found \\end{$1} without a corresponding \\begin{$1}"
humanReadableHint: """
- You have used \\begin{...} without a corresponding \\end{...}.
+ You have used \\begin{} without a corresponding \\end{}.
"""
]
From 097df3a771d37902ff26cfa227f23f985be7ba17 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Thu, 18 Aug 2016 16:55:54 +0100
Subject: [PATCH 171/378] Minor fixes.
---
.../coffee/directives/creditCards.coffee | 71 ++++++++++++++-----
1 file changed, 55 insertions(+), 16 deletions(-)
diff --git a/services/web/public/coffee/directives/creditCards.coffee b/services/web/public/coffee/directives/creditCards.coffee
index 70b485252a..26c8dc1122 100644
--- a/services/web/public/coffee/directives/creditCards.coffee
+++ b/services/web/public/coffee/directives/creditCards.coffee
@@ -137,7 +137,6 @@ define [
value = $target.val()
value = replaceFullWidthChars(value)
value = value.replace(/\D/g, '')
- safeVal(value, $target)
# Format Card Number
reFormatCardNumber = (e) ->
@@ -146,7 +145,6 @@ define [
value = $target.val()
value = replaceFullWidthChars(value)
value = $.payment.formatCardNumber(value)
- #safeVal(value, $target)
formatCardNumber = (e) ->
# Only format if input is a number
@@ -155,7 +153,7 @@ define [
$target = $(e.currentTarget)
value = $target.val()
- card = ccUtils.cardFromNumber(value + digit)
+ card = ccUtils.fromNumber(value + digit)
length = (value.replace(/\D/g, '') + digit).length
upperLength = 16
@@ -202,6 +200,26 @@ define [
e.preventDefault()
setTimeout -> $target.val(value.replace(/\d$/, ''))
+ getFormattedCardNumber = (num) ->
+ num = num.replace(/\D/g, '')
+ card = ccUtils.fromNumber(num)
+ return num unless card
+
+ upperLength = card.length[card.length.length - 1]
+ num = num[0...upperLength]
+
+ if card.format.global
+ num.match(card.format)?.join(' ')
+ else
+ groups = card.format.exec(num)
+ return unless groups?
+ groups.shift()
+ groups = $.grep(groups, (n) -> n) # Filter empty groups
+ groups.join(' ')
+
+ parseCardNumber = (value) ->
+ if value? then value.replace(/\s/g, '') else value
+
# Format Expiry
reFormatExpiry = (e) ->
$target = $(e.currentTarget)
@@ -209,7 +227,6 @@ define [
value = $target.val()
value = replaceFullWidthChars(value)
value = $.payment.formatExpiry(value)
- safeVal(value, $target)
formatExpiry = (e) ->
# Only format if input is a number
@@ -275,8 +292,6 @@ define [
if value?
dateAsObj = ccUtils.parseExpiry(value)
- console.log dateAsObj
-
return unless dateAsObj?
expiry = new Date dateAsObj.year, dateAsObj.month - 1
@@ -290,7 +305,6 @@ define [
value = $target.val()
value = replaceFullWidthChars(value)
value = value.replace(/\D/g, '')[0...4]
- safeVal(value, $target)
# Restrictions
restrictNumeric = (e) ->
@@ -320,7 +334,7 @@ define [
# Restrict number of digits
value = ($target.val() + digit).replace(/\D/g, '')
- card = cardFromNumber(value)
+ card = ccUtils.fromNumber(value)
if card
value.length <= card.length[card.length.length - 1]
@@ -372,6 +386,8 @@ define [
reFormatCardNumber
formatCardNumber
formatBackCardNumber
+ getFormattedCardNumber
+ parseCardNumber
reFormatExpiry
formatExpiry
formatForwardExpiry
@@ -386,18 +402,41 @@ define [
setCardType
}
- App.directive 'ccFormatExpiry', (ccFormat) ->
- restrict: 'A'
- require: 'ngModel'
+ App.directive "ccFormatExpiry", (ccFormat) ->
+ restrict: "A"
+ require: "ngModel"
link: (scope, el, attrs, ngModel) ->
- el.on 'keypress', ccFormat.restrictExpiry
- el.on 'keypress', ccFormat.formatExpiry
- el.on 'keypress', ccFormat.formatForwardSlash
- el.on 'keypress', ccFormat.formatForwardExpiry
- el.on 'keydown', ccFormat.formatBackExpiry
+ el.on "keypress", ccFormat.restrictExpiry
+ el.on "keypress", ccFormat.formatExpiry
+ el.on "keypress", ccFormat.formatForwardSlash
+ el.on "keypress", ccFormat.formatForwardExpiry
+ el.on "keydown", ccFormat.formatBackExpiry
ngModel.$parsers.push ccFormat.parseExpiry
ngModel.$formatters.push ccFormat.parseExpiry
+ App.directive "ccFormatCardNumber", (ccFormat) ->
+ restrict: "A"
+ require: "ngModel"
+ link: (scope, el, attrs, ngModel) ->
+ el.on "keypress", ccFormat.restrictCardNumber
+ el.on "keypress", ccFormat.formatCardNumber
+ el.on "keydown", ccFormat.formatBackCardNumber
+ el.on "paste", ccFormat.reFormatCardNumber
+
+ ngModel.$parsers.push ccFormat.parseCardNumber
+ ngModel.$formatters.push ccFormat.getFormattedCardNumber
+
+ App.directive "ccFormatSecCode", (ccFormat) ->
+ restrict: "A"
+ require: "ngModel"
+ link: (scope, el, attrs, ngModel) ->
+ el.on "keypress", ccFormat.restrictNumeric
+ el.on "keypress", ccFormat.restrictCVC
+ el.on "paste", ccFormat.reFormatCVC
+ el.on "change", ccFormat.reFormatCVC
+ el.on "input", ccFormat.reFormatCVC
+
+
\ No newline at end of file
From 43b832965fbb84a5a1479101dc1b149f373c3530 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Thu, 18 Aug 2016 16:56:23 +0100
Subject: [PATCH 172/378] Layout and styling for simple subscription form.
---
services/web/app/views/subscriptions/new.jade | 156 ++++++++++--------
.../public/stylesheets/app/subscription.less | 62 +++++++
2 files changed, 150 insertions(+), 68 deletions(-)
create mode 100644 services/web/public/stylesheets/app/subscription.less
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade
index d62e8e0de2..5bbda1aa95 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.jade
@@ -176,54 +176,73 @@ block content
div.pull-right
strong {{price.currency.symbol}}{{price.next.total}}
.col-md-12(sixpack-when="simple")
- form(ng-show="planName")
- .row
- .col-md-12
- .form-group
- .row
- .col-md-6
- label.radio-inline
- input.paymentTypeOption(type="radio", value="credit_card", ng-model="paymentMethod")
- i.fa.fa-cc-mastercard.fa-3x
- span
- i.fa.fa-cc-visa.fa-3x
- .col-md-6
- label.radio-inline
- input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod")
- i.fa.fa-cc-paypal.fa-3x
+ form(ng-if="planName")
+ div.payment-method-toggle
+ a.payment-method-toggle-switch(
+ href
+ ng-click="paymentMethod = 'credit_card'"
+ ng-class="paymentMethod === 'credit_card' ? 'payment-method-toggle-switch-selected' : ''"
+ )
+ i.fa.fa-cc-mastercard.fa-2x
+ span
+ i.fa.fa-cc-visa.fa-2x
+ span
+ i.fa.fa-cc-amex.fa-2x
+ a.payment-method-toggle-switch(
+ href
+ ng-click="paymentMethod = 'paypal'"
+ ng-class="paymentMethod === 'paypal' ? 'payment-method-toggle-switch-selected' : ''"
+ )
+ i.fa.fa-cc-paypal.fa-2x
//- TODO Validation messages
- .form-group
- label(for="card-name") Name on card
- input#card-name.form-control(type="text")
- .form-group(ng-class="validation.number == false || validation.errorFields.number ? 'has-error' : ''")
- label(for="card-no") Card number
- input#card-no.form-control(type="credit card", ng-model='data.number', ng-blur="validateCardNumber()")
- .row
- .col-xs-6
- label Valid thru {{ data.mmYY }}
- input.form-control(
+ div(ng-if="paymentMethod === 'credit_card'")
+ .form-group
+ label(for="card-name") Name on card
+ input#card-name.form-control(type="text")
+ .form-group(ng-class="validation.number == false || validation.errorFields.number ? 'has-error' : ''")
+ label(for="card-no") Card number
+ input#card-no.form-control(
type="text"
- ng-model="data.mmYY"
- cc-format-expiry
- )
- .col-xs-6
- .form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
- label Security code
- input.form-control(type="cvc", ng-model='data.cvv', ng-blur="validateCvv()")
- .form-control-feedback
- a.form-helper(
- href
- tooltip-template="'cvv-tooltip-tpl.html'"
- tooltip-trigger="mouseenter"
- tooltip-append-to-body="true"
- ) ?
- .form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
- label(for="country") Country
- select#country.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required)
- mixin countries_options()
- .row
- .col-xs-12
- button.btn.btn-success.btn-block(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") #{translate("upgrade_now")}
+ ng-model="data.number"
+ ng-blur="validateCardNumber()"
+ cc-format-card-number)
+ .row
+ .col-xs-6
+ label Expiry date
+ input.form-control(
+ type="text"
+ ng-model="data.mmYY"
+ placeholder="MM // YY"
+ cc-format-expiry
+ )
+ .col-xs-6
+ .form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
+ label Security code
+ input.form-control(
+ type="text"
+ ng-model="data.cvv"
+ ng-blur="validateCvv()"
+ cc-format-sec-code)
+ .form-control-feedback
+ a.form-helper(
+ href
+ tabindex="-1"
+ tooltip-template="'cvv-tooltip-tpl.html'"
+ tooltip-trigger="mouseenter"
+ tooltip-append-to-body="true"
+ ) ?
+ .form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
+ label(for="country") Country
+ select#country.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required)
+ mixin countries_options()
+ .row.payment-form-submit
+ .col-xs-12
+ button.btn.btn-success.btn-block(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") Upgrade now, pay after 7 days
+ div(ng-if="paymentMethod === 'paypal'")
+ p Click the button below to login with PayPal and upgrade.
+ .row.payment-form-submit
+ .col-xs-12
+ button.btn.btn-success.btn-block(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") Continue
span(sixpack-switch="payment-left-menu-bottom")
@@ -279,32 +298,33 @@ block content
.paymentPageFeatures
.page-header
h2 #{translate("features")}
- h3
- i.fa.fa-check
- | #{translate("unlimited_projects")}
+ h3.feature
+ .features-check: i.fa.fa-check
+ .features-copy #{translate("unlimited_projects")}
- h3
- i.fa.fa-check
- if plan.features.collaborators == -1
- - var collaboratorCount = 'Unlimited'
- else
- - var collaboratorCount = plan.features.collaborators
- | #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
+ h3.feature
+ .features-check: i.fa.fa-check
+ .features-copy
+ if plan.features.collaborators == -1
+ - var collaboratorCount = 'Unlimited'
+ else
+ - var collaboratorCount = plan.features.collaborators
+ | #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
- h3
- i.fa.fa-check
- | #{translate("full_doc_history")}
+ h3.feature
+ .features-check: i.fa.fa-check
+ .features-copy #{translate("full_doc_history")}
- h3
- i.fa.fa-check
- | #{translate("sync_to_dropbox")}
+ h3.feature
+ .features-check: i.fa.fa-check
+ .features-copy #{translate("sync_to_dropbox")}
- h3
- i.fa.fa-check
- | #{translate("sync_to_github")}
- h3
- i.fa.fa-check
- | #{translate("Compile Larger Projects")}
+ h3.feature
+ .features-check: i.fa.fa-check
+ .features-copy #{translate("sync_to_github")}
+ h3.feature
+ .features-check: i.fa.fa-check
+ .features-copy #{translate("compile_larger_projects")}
hr
h2.text-center 30 Day Guarantee
diff --git a/services/web/public/stylesheets/app/subscription.less b/services/web/public/stylesheets/app/subscription.less
new file mode 100644
index 0000000000..1bafe10700
--- /dev/null
+++ b/services/web/public/stylesheets/app/subscription.less
@@ -0,0 +1,62 @@
+.form-helper {
+ display: inline-block;
+ width: 1.3em;
+ height: 1.3em;
+ line-height: 1.3;
+ vertical-align: initial;
+ background-color: @gray;
+ color: #FFF;
+ font-weight: bolder;
+ border-radius: 50%;
+
+ &:hover,
+ &:focus {
+ color: #FFF;
+ text-decoration: none;
+ }
+}
+
+.payment-form {
+ &-submit {
+ padding-top: (@line-height-computed / 2);
+ }
+}
+
+.payment-method-toggle {
+ margin-bottom: (@line-height-computed / 2);
+
+ &-switch {
+ display: inline-block;
+ width: 50%;
+ text-align: center;
+ border: solid 1px @gray-lighter;
+ border-radius: @border-radius-large 0 0 @border-radius-large;
+ padding: (@line-height-computed / 2);
+ color: @gray;
+
+ &:hover,
+ &:focus {
+ color: @gray;
+ text-decoration: none;
+ }
+
+ &:hover {
+ color: @gray-dark;
+ }
+
+ & + & {
+ border-left-width: 0;
+ border-radius: 0 @border-radius-large @border-radius-large 0;
+ }
+
+ &-selected {
+ color: @link-color;
+ box-shadow: inset 0 -2px 0 0;
+
+ &:hover,
+ &:focus {
+ color: @link-color;
+ }
+ }
+ }
+}
\ No newline at end of file
From 82a6cd82a6a1cb81af532d09c04a32a82b162f2b Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Thu, 18 Aug 2016 16:56:36 +0100
Subject: [PATCH 173/378] Minor tweaks in the features list.
---
.../web/public/stylesheets/app/plans.less | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/services/web/public/stylesheets/app/plans.less b/services/web/public/stylesheets/app/plans.less
index c375052394..b830db9b23 100644
--- a/services/web/public/stylesheets/app/plans.less
+++ b/services/web/public/stylesheets/app/plans.less
@@ -89,8 +89,27 @@
.small {
font-size: 12px;
}
+
}
+.feature {
+ margin-top: (@line-height-computed / 2);
+ margin-bottom: (@line-height-computed / 1.5);
+}
+
+.features-check,
+.features-copy {
+ display: inline-block;
+ width: 12%;
+ line-height: 1.4;
+ vertical-align: top;
+}
+
+.features-copy {
+ width: 88%;
+}
+
+
.plansPageStudentLink {
margin-left: 20px;
margin-top: 20px;
From 26e21732f88bc0a84763bbafd44fb5910bf8ded8 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Thu, 18 Aug 2016 17:41:55 +0100
Subject: [PATCH 174/378] Added validations + minor fixes.
---
services/web/app/views/subscriptions/new.jade | 27 +++++++++++++------
.../coffee/directives/creditCards.coffee | 27 +++++++++++++++++--
.../coffee/main/new-subscription.coffee | 11 +++++---
3 files changed, 52 insertions(+), 13 deletions(-)
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade
index 5bbda1aa95..8e8dcb8b23 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.jade
@@ -175,6 +175,7 @@ block content
div(ng-if="price.next.tax!='0.00'").pull-right {{price.currency.symbol}}{{price.next.tax}}
div.pull-right
strong {{price.currency.symbol}}{{price.next.total}}
+
.col-md-12(sixpack-when="simple")
form(ng-if="planName")
div.payment-method-toggle
@@ -196,10 +197,18 @@ block content
i.fa.fa-cc-paypal.fa-2x
//- TODO Validation messages
div(ng-if="paymentMethod === 'credit_card'")
+ ol
+ li validation.correctCardNumber : {{ validation.correctCardNumber }}
+ li validation.correctExpiry : {{ validation.correctExpiry }}
+ li validation.correctCvv : {{ validation.correctCvv }}
+ li data.month : {{ data.month }}
+ li data.year : {{ data.year }}
+ li data.mmYY : {{ data.mmYY }}
+
.form-group
label(for="card-name") Name on card
input#card-name.form-control(type="text")
- .form-group(ng-class="validation.number == false || validation.errorFields.number ? 'has-error' : ''")
+ .form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number ? 'has-error' : ''")
label(for="card-no") Card number
input#card-no.form-control(
type="text"
@@ -208,13 +217,15 @@ block content
cc-format-card-number)
.row
.col-xs-6
- label Expiry date
- input.form-control(
- type="text"
- ng-model="data.mmYY"
- placeholder="MM // YY"
- cc-format-expiry
- )
+ .form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry ? 'has-error' : ''")
+ label Expiry date
+ input.form-control(
+ type="text"
+ ng-model="data.mmYY"
+ placeholder="MM / YY"
+ ng-blur="validateExpiry()"
+ cc-format-expiry
+ )
.col-xs-6
.form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
label Security code
diff --git a/services/web/public/coffee/directives/creditCards.coffee b/services/web/public/coffee/directives/creditCards.coffee
index 26c8dc1122..bfac125f6a 100644
--- a/services/web/public/coffee/directives/creditCards.coffee
+++ b/services/web/public/coffee/directives/creditCards.coffee
@@ -78,6 +78,27 @@ define [
cardFromType = (type) ->
return card for card in cards when card.type is type
+ cardType = (num) ->
+ return null unless num
+ cardFromNumber(num)?.type or null
+
+ formatCardNumber = (num) ->
+ num = num.replace(/\D/g, '')
+ card = cardFromNumber(num)
+ return num unless card
+
+ upperLength = card.length[card.length.length - 1]
+ num = num[0...upperLength]
+
+ if card.format.global
+ num.match(card.format)?.join(' ')
+ else
+ groups = card.format.exec(num)
+ return unless groups?
+ groups.shift()
+ groups = $.grep(groups, (n) -> n) # Filter empty groups
+ groups.join(' ')
+
parseExpiry = (value = "") ->
[month, year] = value.split(/[\s\/]+/, 2)
@@ -97,6 +118,8 @@ define [
return {
fromNumber: cardFromNumber
fromType: cardFromType
+ cardType: cardType
+ formatCardNumber: formatCardNumber
defaultFormat: defaultFormat
defaultInputFormat: defaultInputFormat
parseExpiry: parseExpiry
@@ -144,7 +167,7 @@ define [
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
- value = $.payment.formatCardNumber(value)
+ value = ccUtils.formatCardNumber(value)
formatCardNumber = (e) ->
# Only format if input is a number
@@ -367,7 +390,7 @@ define [
setCardType = (e) ->
$target = $(e.currentTarget)
val = $target.val()
- cardType = $.payment.cardType(val) or 'unknown'
+ cardType = ccUtils.cardType(val) or 'unknown'
unless $target.hasClass(cardType)
allTypes = (card.type for card in cards)
diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee
index 47c1c28a34..0d3b9f1728 100644
--- a/services/web/public/coffee/main/new-subscription.coffee
+++ b/services/web/public/coffee/main/new-subscription.coffee
@@ -3,7 +3,7 @@ define [
"directives/creditCards"
], (App)->
- App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking)->
+ App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils)->
throw new Error("Recurly API Library Missing.") if typeof recurly is "undefined"
$scope.currencyCode = MultiCurrencyPricing.currencyCode
@@ -32,11 +32,16 @@ define [
coupon: window.couponCode
mmYY: ""
-
+ $scope.$watch 'data.mmYY', (newVal) ->
+ parsedDateObj = ccUtils.parseExpiry newVal
+ if parsedDateObj?
+ $scope.data.month = parsedDateObj.month
+ $scope.data.year = parsedDateObj.year
+
$scope.validation =
correctCardNumber : true
correctExpiry: true
- correctCvv:true
+ correctCvv: true
$scope.processing = false
From c653f59705355af0b30d35c5631ae7f3ab9f5cfd Mon Sep 17 00:00:00 2001
From: James Allen
Date: Thu, 18 Aug 2016 17:48:33 +0100
Subject: [PATCH 175/378] Add error handling to mkdir_p
---
.../web/app/coffee/Features/Project/ProjectEntityHandler.coffee | 2 ++
1 file changed, 2 insertions(+)
diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
index c68f732f16..ca783a2cbf 100644
--- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
@@ -258,6 +258,7 @@ module.exports = ProjectEntityHandler =
if !foundFolder?
logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp"
@addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)->
+ return callback(err) if err?
newFolder.parentFolder_id = parentFolder_id
previousFolders.push newFolder
callback null, previousFolders
@@ -268,6 +269,7 @@ module.exports = ProjectEntityHandler =
async.reduce folders, [], procesFolder, (err, folders)->
+ return callback(err) if err?
lastFolder = folders[folders.length-1]
folders = _.select folders, (folder)->
!folder.filterOut
From 67290fd6a7c0cf733d1ac7d9b4ea78430e05702a Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Thu, 18 Aug 2016 17:56:53 +0100
Subject: [PATCH 176/378] Fix formatting on change and paste events.
---
.../coffee/directives/creditCards.coffee | 76 ++++++++++++++++++-
1 file changed, 75 insertions(+), 1 deletion(-)
diff --git a/services/web/public/coffee/directives/creditCards.coffee b/services/web/public/coffee/directives/creditCards.coffee
index bfac125f6a..3ee89a610a 100644
--- a/services/web/public/coffee/directives/creditCards.coffee
+++ b/services/web/public/coffee/directives/creditCards.coffee
@@ -99,6 +99,30 @@ define [
groups = $.grep(groups, (n) -> n) # Filter empty groups
groups.join(' ')
+ formatExpiry = (expiry) ->
+ parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/)
+ return '' unless parts
+
+ mon = parts[1] || ''
+ sep = parts[2] || ''
+ year = parts[3] || ''
+
+ if year.length > 0
+ sep = ' / '
+
+ else if sep is ' /'
+ mon = mon.substring(0, 1)
+ sep = ''
+
+ else if mon.length == 2 or sep.length > 0
+ sep = ' / '
+
+ else if mon.length == 1 and mon not in ['0', '1']
+ mon = "0#{mon}"
+ sep = ' / '
+
+ return mon + sep + year
+
parseExpiry = (value = "") ->
[month, year] = value.split(/[\s\/]+/, 2)
@@ -119,6 +143,7 @@ define [
fromNumber: cardFromNumber
fromType: cardFromType
cardType: cardType
+ formatExpiry: formatExpiry
formatCardNumber: formatCardNumber
defaultFormat: defaultFormat
defaultInputFormat: defaultInputFormat
@@ -137,6 +162,45 @@ define [
false
+ safeVal = (value, $target) ->
+ try
+ cursor = $target.prop('selectionStart')
+ catch error
+ cursor = null
+
+ last = $target.val()
+ $target.val(value)
+
+ if cursor != null && $target.is(":focus")
+ cursor = value.length if cursor is last.length
+
+ # This hack looks for scenarios where we are changing an input's value such
+ # that "X| " is replaced with " |X" (where "|" is the cursor). In those
+ # scenarios, we want " X|".
+ #
+ # For example:
+ # 1. Input field has value "4444| "
+ # 2. User types "1"
+ # 3. Input field has value "44441| "
+ # 4. Reformatter changes it to "4444 |1"
+ # 5. By incrementing the cursor, we make it "4444 1|"
+ #
+ # This is awful, and ideally doesn't go here, but given the current design
+ # of the system there does not appear to be a better solution.
+ #
+ # Note that we can't just detect when the cursor-1 is " ", because that
+ # would incorrectly increment the cursor when backspacing, e.g. pressing
+ # backspace in this scenario: "4444 1|234 5".
+ if last != value
+ prevPair = last[cursor-1..cursor]
+ currPair = value[cursor-1..cursor]
+ digit = value[cursor]
+ cursor = cursor + 1 if /\d/.test(digit) and
+ prevPair == "#{digit} " and currPair == " #{digit}"
+
+ $target.prop('selectionStart', cursor)
+ $target.prop('selectionEnd', cursor)
+
# Replace Full-Width Chars
replaceFullWidthChars = (str = '') ->
fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'
@@ -160,6 +224,7 @@ define [
value = $target.val()
value = replaceFullWidthChars(value)
value = value.replace(/\D/g, '')
+ safeVal(value, $target)
# Format Card Number
reFormatCardNumber = (e) ->
@@ -168,6 +233,7 @@ define [
value = $target.val()
value = replaceFullWidthChars(value)
value = ccUtils.formatCardNumber(value)
+ safeVal(value, $target)
formatCardNumber = (e) ->
# Only format if input is a number
@@ -249,7 +315,9 @@ define [
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
- value = $.payment.formatExpiry(value)
+ value = ccUtils.formatExpiry(value)
+ safeVal(value, $target)
+
formatExpiry = (e) ->
# Only format if input is a number
@@ -328,6 +396,7 @@ define [
value = $target.val()
value = replaceFullWidthChars(value)
value = value.replace(/\D/g, '')[0...4]
+ safeVal(value, $target)
# Restrictions
restrictNumeric = (e) ->
@@ -429,11 +498,15 @@ define [
restrict: "A"
require: "ngModel"
link: (scope, el, attrs, ngModel) ->
+ el.on "keypress", ccFormat.restrictNumeric
el.on "keypress", ccFormat.restrictExpiry
el.on "keypress", ccFormat.formatExpiry
el.on "keypress", ccFormat.formatForwardSlash
el.on "keypress", ccFormat.formatForwardExpiry
el.on "keydown", ccFormat.formatBackExpiry
+ el.on "change", ccFormat.reFormatExpiry
+ el.on "input", ccFormat.reFormatExpiry
+ el.on "paste", ccFormat.reFormatExpiry
ngModel.$parsers.push ccFormat.parseExpiry
ngModel.$formatters.push ccFormat.parseExpiry
@@ -442,6 +515,7 @@ define [
restrict: "A"
require: "ngModel"
link: (scope, el, attrs, ngModel) ->
+ el.on "keypress", ccFormat.restrictNumeric
el.on "keypress", ccFormat.restrictCardNumber
el.on "keypress", ccFormat.formatCardNumber
el.on "keydown", ccFormat.formatBackCardNumber
From 3d36dc7d6c6cad7e5365a5f944288586f93c6103 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Fri, 19 Aug 2016 11:05:35 +0100
Subject: [PATCH 177/378] mvp for not using cdn when blocked
---
.../app/coffee/infrastructure/ExpressLocals.coffee | 9 +++++++--
services/web/app/views/layout.jade | 11 ++++++++++-
services/web/config/settings.defaults.coffee | 6 +++---
3 files changed, 20 insertions(+), 6 deletions(-)
diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index ee7418dacc..155b6ac3c0 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -61,11 +61,16 @@ module.exports = (app, webRouter, apiRouter)->
webRouter.use (req, res, next)->
+ cdnBlocked = req.query.nocdn == 'true' or req.session.cdnBlocked
+
+ if cdnBlocked and !req.session.cdnBlocked?
+ req.session.cdnBlocked = true
+
isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark"
isSmoke = req.headers?.host?.slice(0,5)?.toLowerCase() == "smoke"
isLive = !isDark and !isSmoke
-
- if cdnAvailable and isLive
+
+ if cdnAvailable and isLive and !cdnBlocked
staticFilesBase = Settings.cdn?.web?.host
else if darkCdnAvailable and isDark
staticFilesBase = Settings.cdn?.web?.darkHost
diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade
index 3c781a4b9f..f1bfeedfee 100644
--- a/services/web/app/views/layout.jade
+++ b/services/web/app/views/layout.jade
@@ -8,6 +8,7 @@ html(itemscope, itemtype='http://schema.org/Product')
window.similarproducts = true
style [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {display: none !important; display: none; }
+
-if (typeof(gaExperiments) != "undefined")
|!{gaExperiments}
@@ -51,8 +52,16 @@ html(itemscope, itemtype='http://schema.org/Product')
window.csrfToken = "#{csrfToken}";
block scripts
- script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
+ script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
+ script(type="text/javascript").
+ var noCdnKey = "nocdn=true"
+ var cdnBlocked = typeof jQuery === 'undefined'
+ var noCdnAlreadyInUrl = window.location.href.indexOf(noCdnKey) != -1 //prevent loops
+ if (cdnBlocked && !noCdnAlreadyInUrl) {
+ window.location.search += '&'+noCdnKey;
+ }
+
script.
window.sharelatex = {
siteUrl: '#{settings.siteUrl}',
diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee
index a326c3b07d..56f3ee2b81 100644
--- a/services/web/config/settings.defaults.coffee
+++ b/services/web/config/settings.defaults.coffee
@@ -114,9 +114,9 @@ module.exports = settings =
showSocialButtons: false
showComments: false
- # cdn:
- # web:
- # host:"http://cdn.sharelatex.dev:3000"
+ cdn:
+ web:
+ host:"http://nowhere.sharelatex.dev"
# darkHost:"http://cdn.sharelatex.dev:3000"
# Where your instance of ShareLaTeX can be found publically. Used in emails
From 07cd75cd64cf9b8973adb3e48ec45d48386ac55f Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 19 Aug 2016 11:52:04 +0100
Subject: [PATCH 178/378] Add an `expect404` option to apiRequest.
Suppress error generation when 404 response is encountered.
---
.../Features/Subscription/RecurlyWrapper.coffee | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee
index 059c5fb02c..2be1fc35c7 100644
--- a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee
+++ b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee
@@ -29,13 +29,15 @@ module.exports = RecurlyWrapper =
RecurlyWrapper.apiRequest({
url: "accounts/#{user._id}"
method: "GET"
+ expect404: true
}, (error, response, responseBody) ->
if error
- if response.statusCode == 404 # actually not an error in this case, just no existing account
- cache.userExists = false
- return next(null, cache)
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while checking account"
return next(error)
+ if response.statusCode == 404 # actually not an error in this case, just no existing account
+ logger.log {user_id: user._id, recurly_token_id}, "user does not currently exist in recurly, proceed"
+ cache.userExists = false
+ return next(null, cache)
logger.log {user_id: user._id, recurly_token_id}, "user appears to exist in recurly"
RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
if err
@@ -236,10 +238,14 @@ module.exports = RecurlyWrapper =
"Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64")
"Accept" : "application/xml"
"Content-Type" : "application/xml; charset=utf-8"
+ expect404 = options.expect404
+ delete options.expect404
request options, (error, response, body) ->
- unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204
+ unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204 or (response.statusCode == 404 and expect404)
logger.err err:error, body:body, options:options, statusCode:response?.statusCode, "error returned from recurly"
error = "Recurly API returned with status code: #{response.statusCode}"
+ if response.statusCode == 404 and expect404
+ logger.log {url: options.url, method: options.method}, "got 404 response from recurly, expected as valid response"
callback(error, response, body)
sign : (parameters, callback) ->
From c02854c9d8fc2852b654d9734f6b6c950c27e48a Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 19 Aug 2016 11:52:50 +0100
Subject: [PATCH 179/378] Improve log messages
---
.../Features/Subscription/SubscriptionController.coffee | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
index d4521981e4..8b36e28e1d 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
@@ -171,8 +171,8 @@ module.exports = SubscriptionController =
return next(error) if error?
planCode = req.body.plan_code
if !planCode?
- err = new Error('plan_code not defined')
- logger.err {user_id: user._id, err}, "error updating subscription"
+ err = new Error('plan_code is not defined')
+ logger.err {user_id: user._id, err, planCode}, "[Subscription] error in updateSubscription form"
return next(err)
logger.log planCode: planCode, user_id:user._id, "updating subscription"
SubscriptionHandler.updateSubscription user, planCode, null, (err)->
From d8e7bacec4f82ea46912bf63df010ef19d716304 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Fri, 19 Aug 2016 11:53:40 +0100
Subject: [PATCH 180/378] added logging in
---
services/web/app/coffee/infrastructure/ExpressLocals.coffee | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index 155b6ac3c0..60f1b682e5 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -64,12 +64,14 @@ module.exports = (app, webRouter, apiRouter)->
cdnBlocked = req.query.nocdn == 'true' or req.session.cdnBlocked
if cdnBlocked and !req.session.cdnBlocked?
+ ip = req.ip || req.socket?.socket?.remoteAddress || req.socket?.remoteAddress
+ logger.log user_id:req?.session?.user?._id, ip:ip, "cdnBlocked for user, not using it"
req.session.cdnBlocked = true
isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark"
isSmoke = req.headers?.host?.slice(0,5)?.toLowerCase() == "smoke"
isLive = !isDark and !isSmoke
-
+
if cdnAvailable and isLive and !cdnBlocked
staticFilesBase = Settings.cdn?.web?.host
else if darkCdnAvailable and isDark
From a904427531b2ab16b9f8b428792bdf04bd221203 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 19 Aug 2016 11:57:44 +0100
Subject: [PATCH 181/378] Fix broken test
---
.../UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/test/UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee b/services/web/test/UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee
index 5a5e9fc40c..eda02ccf72 100644
--- a/services/web/test/UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee
+++ b/services/web/test/UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee
@@ -728,7 +728,7 @@ describe "RecurlyWrapper", ->
describe 'when the account does not exist', ->
beforeEach ->
- @apiRequest.callsArgWith(1, new Error('not found'), {statusCode: 404}, '')
+ @apiRequest.callsArgWith(1, null, {statusCode: 404}, '')
it 'should not produce an error', (done) ->
@call (err, result) =>
From 452abe94be3137e78acd3422756e7463dfcab326 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Fri, 19 Aug 2016 14:51:07 +0100
Subject: [PATCH 182/378] Add coupon code, VAT and pricing breakdown. Styling
adjustments.
---
services/web/app/views/subscriptions/new.jade | 74 ++++++++++++++-----
.../coffee/main/new-subscription.coffee | 4 +
.../public/stylesheets/app/subscription.less | 11 ++-
3 files changed, 65 insertions(+), 24 deletions(-)
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade
index 8e8dcb8b23..b3ffdbc8b1 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.jade
@@ -69,8 +69,6 @@ block content
input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod")
i.fa.fa-cc-paypal.fa-3x
-
-
.alert.alert-warning.small(ng-show="genericError")
strong {{genericError}}
@@ -195,16 +193,18 @@ block content
ng-class="paymentMethod === 'paypal' ? 'payment-method-toggle-switch-selected' : ''"
)
i.fa.fa-cc-paypal.fa-2x
- //- TODO Validation messages
- div(ng-if="paymentMethod === 'credit_card'")
- ol
- li validation.correctCardNumber : {{ validation.correctCardNumber }}
- li validation.correctExpiry : {{ validation.correctExpiry }}
- li validation.correctCvv : {{ validation.correctCvv }}
- li data.month : {{ data.month }}
- li data.year : {{ data.year }}
- li data.mmYY : {{ data.mmYY }}
+
+ .alert.alert-warning.small(ng-show="genericError")
+ strong {{genericError}}
+ div(ng-if="paymentMethod === 'credit_card'")
+ //- ol
+ //- li validation.correctCardNumber : {{ validation.correctCardNumber }}
+ //- li validation.correctExpiry : {{ validation.correctExpiry }}
+ //- li validation.correctCvv : {{ validation.correctCvv }}
+ //- li data.month : {{ data.month }}
+ //- li data.year : {{ data.year }}
+ //- li data.mmYY : {{ data.mmYY }}
.form-group
label(for="card-name") Name on card
input#card-name.form-control(type="text")
@@ -244,16 +244,50 @@ block content
) ?
.form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
label(for="country") Country
- select#country.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required)
+ select#country.form-control(
+ data-recurly="country"
+ ng-model="data.country"
+ ng-change="updateCountry()"
+ required
+ )
mixin countries_options()
- .row.payment-form-submit
- .col-xs-12
- button.btn.btn-success.btn-block(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") Upgrade now, pay after 7 days
- div(ng-if="paymentMethod === 'paypal'")
- p Click the button below to login with PayPal and upgrade.
- .row.payment-form-submit
- .col-xs-12
- button.btn.btn-success.btn-block(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") Continue
+
+ div
+ if (showVatField)
+ .form-group
+ label(for="vat-no") #{translate('vat_number')}
+ input#vat-no.form-control(
+ type='text'
+ ng-blur="applyVatNumber()"
+ ng-model="data.vat_number"
+ )
+ if (showCouponField)
+ .form-group
+ label(for="coupon-code") #{translate('coupon_code')}
+ input#coupon-code.form-control(
+ type='text'
+ ng-blur="applyCoupon()"
+ ng-model="data.coupon"
+ )
+
+ //- PRicing stuff
+
+ p(ng-if="paymentMethod === 'paypal'") To upgrade, click on the button below and log on to PayPal using your email and password.
+ div.price-breakdown(ng-if="price.next.tax !== '0.00'")
+ hr.thin
+ span Total:
+ strong {{price.currency.symbol}}{{price.next.total}}
+ span ({{price.currency.symbol}}{{price.next.subtotal}} + {{price.currency.symbol}}{{price.next.tax}} tax)
+ span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")}
+ span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
+ hr.thin
+
+ div.payment-submit
+ button.btn.btn-success.btn-block(
+ ng-click="submit()"
+ ng-disabled="processing"
+ sixpack-convert="payment-left-menu-bottom"
+ ) {{ paymentMethod === 'credit_card' ? 'Upgrade now, pay after 7 days' : 'Continue' }}
span(sixpack-switch="payment-left-menu-bottom")
diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee
index 0d3b9f1728..ae81d73bb0 100644
--- a/services/web/public/coffee/main/new-subscription.coffee
+++ b/services/web/public/coffee/main/new-subscription.coffee
@@ -65,6 +65,7 @@ define [
$scope.price = pricing.price
$scope.trialLength = pricing.items.plan.trial?.length
$scope.monthlyBilling = pricing.items.plan.period.length == 1
+
if pricing.items?.coupon?.discount?.type == "percent"
basePrice = parseInt(pricing.price.base.plan.unit)
$scope.normalPrice = basePrice
@@ -100,6 +101,7 @@ define [
$scope.validation.correctCvv = recurly.validate.cvv($scope.data.cvv)
$scope.updateCountry = ->
+ console.log 'update country'
pricing.address({country:$scope.data.country}).done()
$scope.changePaymentMethod = (paymentMethod)->
@@ -140,6 +142,8 @@ define [
isPaypal : postData.subscriptionDetails.isPaypal
}
+ sixpack.convert "subscription-form"
+
$http.post("/user/subscription/create", postData)
.success (data, status, headers)->
sixpack.convert "in-editor-free-trial-plan", pricing.items.plan.code, (err)->
diff --git a/services/web/public/stylesheets/app/subscription.less b/services/web/public/stylesheets/app/subscription.less
index 1bafe10700..5e7656adc7 100644
--- a/services/web/public/stylesheets/app/subscription.less
+++ b/services/web/public/stylesheets/app/subscription.less
@@ -16,10 +16,13 @@
}
}
-.payment-form {
- &-submit {
- padding-top: (@line-height-computed / 2);
- }
+.price-breakdown {
+ text-align: center;
+ margin-bottom: -10px;
+}
+
+.payment-submit {
+ padding-top: (@line-height-computed / 2);
}
.payment-method-toggle {
From 6a1f0de91218eafa9f0c7fe775de73731294f31c Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Fri, 19 Aug 2016 15:12:05 +0100
Subject: [PATCH 183/378] Add i18n keys.
---
services/web/app/views/subscriptions/new.jade | 20 +++++++++----------
1 file changed, 9 insertions(+), 11 deletions(-)
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade
index b3ffdbc8b1..03aa58c052 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.jade
@@ -206,10 +206,10 @@ block content
//- li data.year : {{ data.year }}
//- li data.mmYY : {{ data.mmYY }}
.form-group
- label(for="card-name") Name on card
+ label(for="card-name") #{translate("name_on_card")}
input#card-name.form-control(type="text")
.form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number ? 'has-error' : ''")
- label(for="card-no") Card number
+ label(for="card-no") #{translate("credit_card_number")}
input#card-no.form-control(
type="text"
ng-model="data.number"
@@ -218,7 +218,7 @@ block content
.row
.col-xs-6
.form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry ? 'has-error' : ''")
- label Expiry date
+ label #{translate("expiry")}
input.form-control(
type="text"
ng-model="data.mmYY"
@@ -228,7 +228,7 @@ block content
)
.col-xs-6
.form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
- label Security code
+ label #{translate("security_code")}
input.form-control(
type="text"
ng-model="data.cvv"
@@ -243,7 +243,7 @@ block content
tooltip-append-to-body="true"
) ?
.form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
- label(for="country") Country
+ label(for="country") #{translate('country')}
select#country.form-control(
data-recurly="country"
ng-model="data.country"
@@ -270,16 +270,14 @@ block content
ng-model="data.coupon"
)
- //- PRicing stuff
-
- p(ng-if="paymentMethod === 'paypal'") To upgrade, click on the button below and log on to PayPal using your email and password.
+ p(ng-if="paymentMethod === 'paypal'") #{translate("paypal_upgrade")}
div.price-breakdown(ng-if="price.next.tax !== '0.00'")
hr.thin
span Total:
strong {{price.currency.symbol}}{{price.next.total}}
span ({{price.currency.symbol}}{{price.next.subtotal}} + {{price.currency.symbol}}{{price.next.tax}} tax)
- span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")}
- span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
+ span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")}
+ span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
hr.thin
div.payment-submit
@@ -287,7 +285,7 @@ block content
ng-click="submit()"
ng-disabled="processing"
sixpack-convert="payment-left-menu-bottom"
- ) {{ paymentMethod === 'credit_card' ? 'Upgrade now, pay after 7 days' : 'Continue' }}
+ ) {{ paymentMethod === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }}
span(sixpack-switch="payment-left-menu-bottom")
From 50b340398365ea1516eac90cb10f8269839e22f0 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Fri, 19 Aug 2016 15:39:58 +0100
Subject: [PATCH 184/378] use url.resolve to build url for freegeoip lookups
---
services/web/app/coffee/infrastructure/GeoIpLookup.coffee | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/infrastructure/GeoIpLookup.coffee b/services/web/app/coffee/infrastructure/GeoIpLookup.coffee
index 5b64b8e6f4..88f3ed4bfa 100644
--- a/services/web/app/coffee/infrastructure/GeoIpLookup.coffee
+++ b/services/web/app/coffee/infrastructure/GeoIpLookup.coffee
@@ -2,6 +2,7 @@ request = require("request")
settings = require("settings-sharelatex")
_ = require("underscore")
logger = require("logger-sharelatex")
+URL = require("url")
currencyMappings = {
"GB":"GBP"
@@ -31,7 +32,7 @@ module.exports = GeoIpLookup =
return callback(e)
ip = ip.trim().split(" ")[0]
opts =
- url: "#{settings.apis.geoIpLookup.url}/#{ip}"
+ url: URL.resolve(settings.apis.geoIpLookup.url,ip)
timeout: 1000
json:true
logger.log ip:ip, opts:opts, "getting geo ip details"
From 110820face184d07212f50cb5359df90da90ab91 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Fri, 19 Aug 2016 15:39:58 +0100
Subject: [PATCH 185/378] use url.resolve to build url for freegeoip lookups
---
services/web/app/coffee/infrastructure/GeoIpLookup.coffee | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/infrastructure/GeoIpLookup.coffee b/services/web/app/coffee/infrastructure/GeoIpLookup.coffee
index 5b64b8e6f4..88f3ed4bfa 100644
--- a/services/web/app/coffee/infrastructure/GeoIpLookup.coffee
+++ b/services/web/app/coffee/infrastructure/GeoIpLookup.coffee
@@ -2,6 +2,7 @@ request = require("request")
settings = require("settings-sharelatex")
_ = require("underscore")
logger = require("logger-sharelatex")
+URL = require("url")
currencyMappings = {
"GB":"GBP"
@@ -31,7 +32,7 @@ module.exports = GeoIpLookup =
return callback(e)
ip = ip.trim().split(" ")[0]
opts =
- url: "#{settings.apis.geoIpLookup.url}/#{ip}"
+ url: URL.resolve(settings.apis.geoIpLookup.url,ip)
timeout: 1000
json:true
logger.log ip:ip, opts:opts, "getting geo ip details"
From 53c6e48da98827c24917062052c7556586c68674 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Fri, 19 Aug 2016 16:03:15 +0100
Subject: [PATCH 186/378] Remove left-hand panel experiment.
---
services/web/app/views/subscriptions/new.jade | 120 ++++++------------
1 file changed, 36 insertions(+), 84 deletions(-)
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade
index 03aa58c052..94493c303d 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.jade
@@ -287,96 +287,48 @@ block content
sixpack-convert="payment-left-menu-bottom"
) {{ paymentMethod === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }}
- span(sixpack-switch="payment-left-menu-bottom")
- .col-md-3.col-md-pull-4(sixpack-default)
- if showStudentPlan == 'true'
- a.btn-primary.btn.plansPageStudentLink(
- href,
- ng-click="switchToStudent()"
- ) #{translate("half_price_student")}
+ .col-md-3.col-md-pull-4
+ if showStudentPlan == 'true'
+ a.btn-primary.btn.plansPageStudentLink(
+ href,
+ ng-click="switchToStudent()"
+ ) #{translate("half_price_student")}
- .card.card-first
- .paymentPageFeatures
- h3 #{translate("unlimited_projects")}
- p #{translate("create_unlimited_projects")}
+ .card.card-first
+ .paymentPageFeatures
+ h3 #{translate("unlimited_projects")}
+ p #{translate("create_unlimited_projects")}
+
+ h3
+ if plan.features.collaborators == -1
+ - var collaboratorCount = 'Unlimited'
+ else
+ - var collaboratorCount = plan.features.collaborators
+ | #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
+ p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time.
- h3
- if plan.features.collaborators == -1
- - var collaboratorCount = 'Unlimited'
- else
- - var collaboratorCount = plan.features.collaborators
- | #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
- p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time.
-
- h3 #{translate("full_doc_history")}
- p #{translate("see_what_has_been")}
- span.added #{translate("added")}
- | #{translate("and")}
- span.removed #{translate("removed")}.
- | #{translate("restore_to_any_older_version")}.
-
- h3 #{translate("sync_to_dropbox")}
- p
- | #{translate("acces_work_from_anywhere")}.
- | #{translate("work_offline_and_sync_with_dropbox")}.
-
- hr
-
- p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
- hr
- span
- a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
- img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
- div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
- a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
- .col-md-3.col-md-pull-4(sixpack-when="bolder")
- if showStudentPlan == 'true'
- a.btn-primary.btn.plansPageStudentLink(
- href,
- ng-click="switchToStudent()"
- ) #{translate("half_price_student")}
-
- .card.card-first
- .paymentPageFeatures
- .page-header
- h2 #{translate("features")}
- h3.feature
- .features-check: i.fa.fa-check
- .features-copy #{translate("unlimited_projects")}
+ h3 #{translate("full_doc_history")}
+ p #{translate("see_what_has_been")}
+ span.added #{translate("added")}
+ | #{translate("and")}
+ span.removed #{translate("removed")}.
+ | #{translate("restore_to_any_older_version")}.
- h3.feature
- .features-check: i.fa.fa-check
- .features-copy
- if plan.features.collaborators == -1
- - var collaboratorCount = 'Unlimited'
- else
- - var collaboratorCount = plan.features.collaborators
- | #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
+ h3 #{translate("sync_to_dropbox")}
+ p
+ | #{translate("acces_work_from_anywhere")}.
+ | #{translate("work_offline_and_sync_with_dropbox")}.
- h3.feature
- .features-check: i.fa.fa-check
- .features-copy #{translate("full_doc_history")}
+ hr
- h3.feature
- .features-check: i.fa.fa-check
- .features-copy #{translate("sync_to_dropbox")}
-
- h3.feature
- .features-check: i.fa.fa-check
- .features-copy #{translate("sync_to_github")}
- h3.feature
- .features-check: i.fa.fa-check
- .features-copy #{translate("compile_larger_projects")}
- hr
-
- h2.text-center 30 Day Guarantee
- hr
- span
- a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
- img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
- div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
- a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
+ p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
+ hr
+ span
+ a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
+ img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
+ div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
+ a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
script(type="text/javascript").
From f4b8cc7fc813c0527d3bd2caef1826de8ee66c14 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Fri, 19 Aug 2016 16:46:44 +0100
Subject: [PATCH 187/378] Use first and last name instead of full name as
input.
---
services/web/app/views/subscriptions/new.jade | 24 +++++++++----------
.../coffee/main/new-subscription.coffee | 1 -
2 files changed, 12 insertions(+), 13 deletions(-)
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade
index 94493c303d..852a2f5ab3 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.jade
@@ -53,7 +53,7 @@ block content
.row
div(sixpack-switch="subscription-form")
.col-md-12(sixpack-default)
- form(ng-show="planName")
+ form(ng-show="planName",novalidate)
.row
.col-md-12
.form-group
@@ -175,7 +175,7 @@ block content
strong {{price.currency.symbol}}{{price.next.total}}
.col-md-12(sixpack-when="simple")
- form(ng-if="planName")
+ form(ng-if="planName",novalidate)
div.payment-method-toggle
a.payment-method-toggle-switch(
href
@@ -198,16 +198,16 @@ block content
strong {{genericError}}
div(ng-if="paymentMethod === 'credit_card'")
- //- ol
- //- li validation.correctCardNumber : {{ validation.correctCardNumber }}
- //- li validation.correctExpiry : {{ validation.correctExpiry }}
- //- li validation.correctCvv : {{ validation.correctCvv }}
- //- li data.month : {{ data.month }}
- //- li data.year : {{ data.year }}
- //- li data.mmYY : {{ data.mmYY }}
- .form-group
- label(for="card-name") #{translate("name_on_card")}
- input#card-name.form-control(type="text")
+ .row
+ .col-xs-6
+ .form-group(ng-class="validation.errorFields.first_name ? 'has-error' : ''")
+ label(for="first-name") #{translate('first_name')}
+ input#first-name.form-control(type='text', value='', maxlength='255', , onkeyup='', data-recurly="first_name", ng-model="data.first_name", required)
+ .col-xs-6
+ .form-group(for="last-name",ng-class="validation.errorFields.last_name ? 'has-error' : ''")
+ label(for="last-name") #{translate('last_name')}
+ input#last-name.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="last_name", ng-model="data.last_name", required)
+
.form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number ? 'has-error' : ''")
label(for="card-no") #{translate("credit_card_number")}
input#card-no.form-control(
diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee
index ae81d73bb0..859d4710ef 100644
--- a/services/web/public/coffee/main/new-subscription.coffee
+++ b/services/web/public/coffee/main/new-subscription.coffee
@@ -101,7 +101,6 @@ define [
$scope.validation.correctCvv = recurly.validate.cvv($scope.data.cvv)
$scope.updateCountry = ->
- console.log 'update country'
pricing.address({country:$scope.data.country}).done()
$scope.changePaymentMethod = (paymentMethod)->
From b3db66e12b47428da83fe5d8cff83dd883dd4d8c Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Fri, 19 Aug 2016 17:10:07 +0100
Subject: [PATCH 188/378] Place tracking event on controller load.
---
services/web/public/coffee/main/new-subscription.coffee | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee
index 859d4710ef..96575eb25a 100644
--- a/services/web/public/coffee/main/new-subscription.coffee
+++ b/services/web/public/coffee/main/new-subscription.coffee
@@ -12,10 +12,10 @@ define [
$scope.switchToStudent = ()->
window.location = "/user/subscription/new?planCode=student_free_trial_7_days¤cy=#{$scope.currencyCode}&cc=#{$scope.data.coupon}"
+ event_tracking.sendMB "subscription-form", { plan : window.plan_code }
$scope.paymentMethod = "credit_card"
-
$scope.data =
number: ""
month: ""
@@ -59,8 +59,6 @@ define [
.done()
pricing.on "change", =>
- event_tracking.sendMB "subscription-form", { plan : pricing.items.plan.code }
-
$scope.planName = pricing.items.plan.name
$scope.price = pricing.price
$scope.trialLength = pricing.items.plan.trial?.length
From 130fece0f6fd3461c5761a24ffe83dbeec5de53a Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Fri, 19 Aug 2016 18:33:03 +0100
Subject: [PATCH 189/378] track when users accept invites
---
.../Collaborators/CollaboratorsInviteController.coffee | 3 ++-
.../Collaborators/CollaboratorsInviteControllerTests.coffee | 2 ++
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 216e2f8a01..4f8daf4bdb 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -7,7 +7,7 @@ logger = require('logger-sharelatex')
EmailHelper = require "../Helpers/EmailHelper"
EditorRealTimeController = require("../Editor/EditorRealTimeController")
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
-
+AnalyticsManger = require("../Analytics/AnalyticsManager")
module.exports = CollaboratorsInviteController =
@@ -120,4 +120,5 @@ module.exports = CollaboratorsInviteController =
logger.err {projectId, inviteId}, "error accepting invite by token"
return next(err)
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
+ AnalyticsManger.recordEvent(currentUser._id, "project-invite-accept", {inviteId:inviteId, projectId:projectId})
res.redirect "/project/#{projectId}"
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 346ac3a3c1..0144583f05 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -11,6 +11,7 @@ ObjectId = require("mongojs").ObjectId
describe "CollaboratorsInviteController", ->
beforeEach ->
+ @AnalyticsManger = recordEvent: sinon.stub()
@CollaboratorsInviteController = SandboxedModule.require modulePath, requires:
"../Project/ProjectGetter": @ProjectGetter = {}
'../Subscription/LimitationsManager' : @LimitationsManager = {}
@@ -20,6 +21,7 @@ describe "CollaboratorsInviteController", ->
'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
"../Editor/EditorRealTimeController": @EditorRealTimeController = {emitToRoom: sinon.stub()}
"../Notifications/NotificationsBuilder": @NotificationsBuilder = {}
+ "../Analytics/AnalyticsManager": @AnalyticsManger
@res = new MockResponse()
@req = new MockRequest()
From 03aa9b87f14115f0355156d92b5855601c11ddb5 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 22 Aug 2016 10:09:54 +0100
Subject: [PATCH 190/378] Add debug query string `origin` to invocations of the
updateSubscription endpoint.
---
.../Features/Subscription/SubscriptionController.coffee | 3 ++-
.../web/app/views/subscriptions/edit-billing-details.jade | 2 +-
.../web/public/coffee/main/subscription-dashboard.coffee | 5 ++---
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
index 8b36e28e1d..3d2ba910d0 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
@@ -167,12 +167,13 @@ module.exports = SubscriptionController =
res.redirect "/user/subscription"
updateSubscription: (req, res, next)->
+ _origin = req?.query?.origin || null
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
planCode = req.body.plan_code
if !planCode?
err = new Error('plan_code is not defined')
- logger.err {user_id: user._id, err, planCode}, "[Subscription] error in updateSubscription form"
+ logger.err {user_id: user._id, err, planCode, origin: _origin, body: req.body}, "[Subscription] error in updateSubscription form"
return next(err)
logger.log planCode: planCode, user_id:user._id, "updating subscription"
SubscriptionHandler.updateSubscription user, planCode, null, (err)->
diff --git a/services/web/app/views/subscriptions/edit-billing-details.jade b/services/web/app/views/subscriptions/edit-billing-details.jade
index f0b6671bcd..caf204b79d 100644
--- a/services/web/app/views/subscriptions/edit-billing-details.jade
+++ b/services/web/app/views/subscriptions/edit-billing-details.jade
@@ -19,7 +19,7 @@ block content
Recurly.config(!{recurlyConfig})
Recurly.buildBillingInfoUpdateForm({
target : "#billingDetailsForm",
- successURL : "#{successURL}?_csrf=#{csrfToken}",
+ successURL : "#{successURL}?_csrf=#{csrfToken}&origin=editBillingDetails",
signature : "!{signature}",
accountCode : "#{user.id}"
});
diff --git a/services/web/public/coffee/main/subscription-dashboard.coffee b/services/web/public/coffee/main/subscription-dashboard.coffee
index 63eec0d65a..69d030ed3b 100644
--- a/services/web/public/coffee/main/subscription-dashboard.coffee
+++ b/services/web/public/coffee/main/subscription-dashboard.coffee
@@ -62,8 +62,7 @@ define [
$scope.inflight = true
-
- $http.post(SUBSCRIPTION_URL, body)
+ $http.post("#{SUBSCRIPTION_URL}?origin=confirmChangePlan", body)
.success ->
location.reload()
.error ->
@@ -124,7 +123,7 @@ define [
plan_code: 'student'
_csrf : window.csrfToken
$scope.inflight = true
- $http.post(SUBSCRIPTION_URL, body)
+ $http.post("#{SUBSCRIPTION_URL}?origin=downgradeToStudent", body)
.success ->
location.reload()
.error ->
From a9095ccde8dff48dd090a1bf9f1096a9d7919c6f Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Mon, 22 Aug 2016 14:54:07 +0100
Subject: [PATCH 191/378] Disable filter which wraps long words, still buggy.
---
services/web/app/views/project/editor/chat.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/project/editor/chat.jade b/services/web/app/views/project/editor/chat.jade
index 86842c36de..538a5b9d37 100644
--- a/services/web/app/views/project/editor/chat.jade
+++ b/services/web/app/views/project/editor/chat.jade
@@ -45,7 +45,7 @@ aside.chat(
mathjax,
ng-repeat="content in message.contents track by $index"
)
- span(ng-bind-html="content | linky:'_blank' | wrapLongWords")
+ span(ng-bind-html="content | linky:'_blank'")
.new-message
textarea(
From 2f93a102fde5a0eca893003aca0115c14f569e55 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Mon, 22 Aug 2016 16:12:29 +0100
Subject: [PATCH 192/378] Fix layout to support scrollable messages.
---
services/web/app/views/project/editor/chat.jade | 11 ++++++-----
services/web/public/stylesheets/app/editor/chat.less | 10 ++++++++--
2 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/services/web/app/views/project/editor/chat.jade b/services/web/app/views/project/editor/chat.jade
index 538a5b9d37..47a1752834 100644
--- a/services/web/app/views/project/editor/chat.jade
+++ b/services/web/app/views/project/editor/chat.jade
@@ -41,11 +41,12 @@ aside.chat(
}"
)
.arrow(ng-style="{'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)'}")
- p(
- mathjax,
- ng-repeat="content in message.contents track by $index"
- )
- span(ng-bind-html="content | linky:'_blank'")
+ .message-content
+ p(
+ mathjax,
+ ng-repeat="content in message.contents track by $index"
+ )
+ span(ng-bind-html="content | linky:'_blank'")
.new-message
textarea(
diff --git a/services/web/public/stylesheets/app/editor/chat.less b/services/web/public/stylesheets/app/editor/chat.less
index 592d39ecf4..0a35fdc1c2 100644
--- a/services/web/public/stylesheets/app/editor/chat.less
+++ b/services/web/public/stylesheets/app/editor/chat.less
@@ -47,6 +47,7 @@
}
.message-wrapper {
margin-left: 50px + @line-height-computed/2;
+
.name {
font-size: 12px;
color: @gray-light;
@@ -54,12 +55,17 @@
min-height: 16px;
}
.message {
- padding: @line-height-computed / 2;
border-left: 3px solid transparent;
- position: relative;
font-size: 14px;
box-shadow: -1px 2px 3px #ddd;
border-raduis: @border-radius-base;
+ position: relative;
+
+ .message-content {
+ padding: @line-height-computed / 2;
+ overflow-x: scroll;
+ }
+
.arrow {
right: 100%;
top: @line-height-computed / 4;
From 861022aff06eb8854370e619c6f7217afa53766d Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Mon, 22 Aug 2016 16:32:59 +0100
Subject: [PATCH 193/378] Make scrollbar only visible when needed.
---
services/web/public/stylesheets/app/editor/chat.less | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/public/stylesheets/app/editor/chat.less b/services/web/public/stylesheets/app/editor/chat.less
index 0a35fdc1c2..d702a225fe 100644
--- a/services/web/public/stylesheets/app/editor/chat.less
+++ b/services/web/public/stylesheets/app/editor/chat.less
@@ -63,7 +63,7 @@
.message-content {
padding: @line-height-computed / 2;
- overflow-x: scroll;
+ overflow-x: auto;
}
.arrow {
From 290b1ad13443c94e94803c11200c0481e5c525c3 Mon Sep 17 00:00:00 2001
From: MCribbin
Date: Mon, 22 Aug 2016 16:33:07 +0100
Subject: [PATCH 194/378] Update HumanReadableLogsRules.coffee
Added corrections to new hints:
-Double subscript
-Double superscript
-LaTeX Error: Something's wrong--perhaps a missing \item
-Misplaced \noalign
---
.../HumanReadableLogsRules.coffee | 49 ++++++++++++++++++-
1 file changed, 48 insertions(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
index c61c61506b..6882d2814d 100644
--- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
+++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
@@ -102,5 +102,52 @@ define -> [
humanReadableHint: """
You have used an open bracket without a corresponding close bracket.
"""
-
+ ,
+ regexToMatch: /LaTeX Error: Can be used only in preamble/
+ extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Can_be_used_only_in_preamble"
+ humanReadableHint: """
+ You have used a command in the main body of your document which should be used in the preamble. Make sure that \\documentclass[\u2026]{\u2026} and all \\usepackage{\u2026} commands are written before \\begin{document}.
+ """
+ ,
+ regexToMatch: /Missing \\right inserted/
+ extraInfoURL: "https://www.sharelatex.com/learn/Errors/Missing_%5Cright_insertede"
+ humanReadableHint: """
+ You have started an expression with a \\left command, but have not included a corresponding \\right command. Make sure that your \\left and \\right commands balance everywhere, or else try using \\Biggl and \\Biggr commands instead as shown here.
+ """
+ ,
+ regexToMatch: /Double superscript/
+ extraInfoURL: "https://www.sharelatex.com/learn/Errors/Double_superscript"
+ humanReadableHint: """
+ You have written a double superscript incorrectly as a^b^c, or else you have written a prime with a superscript. Remember to include { and } when using multiple superscripts. Try a^{b^c} instead.
+ """
+ ,
+ regexToMatch: /Double subscript/
+ extraInfoURL: "https://www.sharelatex.com/learn/Errors/Double_subscript"
+ humanReadableHint: """
+ You have written a double subscript incorrectly as a_b_c. Remember to include { and } when using multiple subscripts. Try a_{b_c} instead.
+ """
+ ,
+ regexToMatch: /No \\author given/
+ extraInfoURL: "https://www.sharelatex.com/learn/Errors/No_%5Cauthor_given"
+ humanReadableHint: """
+ You have used the \\maketitle command, but have not specified any \\author. To fix this, include an author in your preamble using the \\author{\u2026} command.
+ """
+ ,
+ regexToMatch: /LaTeX Error: Environment .+ undefined/
+ extraInfoURL: "https://www.sharelatex.com/learn/Errors%2FLaTeX%20Error%3A%20Environment%20XXX%20undefined"
+ humanReadableHint: """
+ You have created an environment (using \\begin{\u2026} and \\end{\u2026} commands) which is not recognized. Make sure you have included the required package for that environment in your preamble, and that the environment is spelled correctly.
+ """
+ ,
+ regexToMatch: /LaTeX Error: Something's wrong--perhaps a missing \\item/
+ extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Something%27s_wrong--perhaps_a_missing_%5Citem"
+ humanReadableHint: """
+ There are no entries found in a list you have created. Make sure you label list entries using the \\item command, and that you have not used a list inside a table.
+ """
+ ,
+ regexToMatch: /Misplaced \\noalign/
+ extraInfoURL: "https://www.sharelatex.com/learn/Errors/Misplaced_%5Cnoalign"
+ humanReadableHint: """
+ You have used a \\hline command in the wrong place, probably outside a table. If the \\hline command is written inside a table, try including \\\ before it.
+ """
]
From 23ed1e2aa33462c42df5a97b5650611d9518e27a Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Mon, 22 Aug 2016 17:03:29 +0100
Subject: [PATCH 195/378] Add form validation.
---
services/web/app/views/subscriptions/new.jade | 74 ++++++++++++++-----
.../coffee/main/new-subscription.coffee | 35 ++++++---
.../public/stylesheets/app/subscription.less | 9 +++
3 files changed, 87 insertions(+), 31 deletions(-)
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade
index 852a2f5ab3..d26a760e69 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.jade
@@ -175,11 +175,15 @@ block content
strong {{price.currency.symbol}}{{price.next.total}}
.col-md-12(sixpack-when="simple")
- form(ng-if="planName",novalidate)
+ form(
+ ng-if="planName"
+ name="simpleCCForm"
+ novalidate
+ )
div.payment-method-toggle
a.payment-method-toggle-switch(
href
- ng-click="paymentMethod = 'credit_card'"
+ ng-click="setPaymentMethod('credit_card');"
ng-class="paymentMethod === 'credit_card' ? 'payment-method-toggle-switch-selected' : ''"
)
i.fa.fa-cc-mastercard.fa-2x
@@ -189,7 +193,7 @@ block content
i.fa.fa-cc-amex.fa-2x
a.payment-method-toggle-switch(
href
- ng-click="paymentMethod = 'paypal'"
+ ng-click="setPaymentMethod('paypal');"
ng-class="paymentMethod === 'paypal' ? 'payment-method-toggle-switch-selected' : ''"
)
i.fa.fa-cc-paypal.fa-2x
@@ -200,40 +204,68 @@ block content
div(ng-if="paymentMethod === 'credit_card'")
.row
.col-xs-6
- .form-group(ng-class="validation.errorFields.first_name ? 'has-error' : ''")
+ .form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''")
label(for="first-name") #{translate('first_name')}
- input#first-name.form-control(type='text', value='', maxlength='255', , onkeyup='', data-recurly="first_name", ng-model="data.first_name", required)
+ input#first-name.form-control(
+ type="text"
+ maxlength='255'
+ data-recurly="first_name"
+ name="firstName"
+ ng-model="data.first_name"
+ required
+ )
+ span.input-feedback-message {{ simpleCCForm.firstName.$error.required ? 'This field is required' : '' }}
.col-xs-6
- .form-group(for="last-name",ng-class="validation.errorFields.last_name ? 'has-error' : ''")
+ .form-group(for="last-name",ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-error' : ''")
label(for="last-name") #{translate('last_name')}
- input#last-name.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="last_name", ng-model="data.last_name", required)
+ input#last-name.form-control(
+ type="text"
+ maxlength='255'
+ data-recurly="last_name"
+ name="lastName"
+ ng-model="data.last_name"
+ required
+ )
+ span.input-feedback-message {{ simpleCCForm.lastName.$error.required ? 'This field is required' : '' }}
- .form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number ? 'has-error' : ''")
+ .form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number || inputHasError(simpleCCForm.ccNumber) ? 'has-error' : ''")
label(for="card-no") #{translate("credit_card_number")}
input#card-no.form-control(
type="text"
ng-model="data.number"
- ng-blur="validateCardNumber()"
- cc-format-card-number)
+ name="ccNumber"
+ required
+ ng-change="validateCardNumber()"
+ cc-format-card-number
+ )
+ span.input-feedback-message {{ simpleCCForm.ccNumber.$error.required ? 'This field is required' : 'Please re-check the card number' }}
+
.row
.col-xs-6
- .form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry ? 'has-error' : ''")
+ .form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''")
label #{translate("expiry")}
input.form-control(
type="text"
ng-model="data.mmYY"
+ name="expiry"
+ required
placeholder="MM / YY"
- ng-blur="validateExpiry()"
+ ng-change="updateExpiry(); validateExpiry()"
cc-format-expiry
)
+ span.input-feedback-message {{ simpleCCForm.expiry.$error.required ? 'This field is required' : 'Please re-check the expiry date' }}
+
.col-xs-6
- .form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
+ .form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv || inputHasError(simpleCCForm.cvv) ? 'has-error' : ''")
label #{translate("security_code")}
input.form-control(
type="text"
ng-model="data.cvv"
- ng-blur="validateCvv()"
- cc-format-sec-code)
+ ng-change="validateCvv()"
+ name="cvv"
+ required
+ cc-format-sec-code
+ )
.form-control-feedback
a.form-helper(
href
@@ -242,22 +274,26 @@ block content
tooltip-trigger="mouseenter"
tooltip-append-to-body="true"
) ?
- .form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
+ span.input-feedback-message {{ simpleCCForm.cvv.$error.required ? 'This field is required' : 'Please re-check the security code' }}
+
+ .form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''")
label(for="country") #{translate('country')}
select#country.form-control(
data-recurly="country"
ng-model="data.country"
+ name="country"
ng-change="updateCountry()"
required
)
mixin countries_options()
+ span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
div
if (showVatField)
.form-group
label(for="vat-no") #{translate('vat_number')}
input#vat-no.form-control(
- type='text'
+ type="text"
ng-blur="applyVatNumber()"
ng-model="data.vat_number"
)
@@ -265,7 +301,7 @@ block content
.form-group
label(for="coupon-code") #{translate('coupon_code')}
input#coupon-code.form-control(
- type='text'
+ type="text"
ng-blur="applyCoupon()"
ng-model="data.coupon"
)
@@ -283,7 +319,7 @@ block content
div.payment-submit
button.btn.btn-success.btn-block(
ng-click="submit()"
- ng-disabled="processing"
+ ng-disabled="processing || !isFormValid(simpleCCForm);"
sixpack-convert="payment-left-menu-bottom"
) {{ paymentMethod === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }}
diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee
index 96575eb25a..f1c81458ef 100644
--- a/services/web/public/coffee/main/new-subscription.coffee
+++ b/services/web/public/coffee/main/new-subscription.coffee
@@ -32,12 +32,6 @@ define [
coupon: window.couponCode
mmYY: ""
- $scope.$watch 'data.mmYY', (newVal) ->
- parsedDateObj = ccUtils.parseExpiry newVal
- if parsedDateObj?
- $scope.data.month = parsedDateObj.month
- $scope.data.year = parsedDateObj.year
-
$scope.validation =
correctCardNumber : true
correctExpiry: true
@@ -81,11 +75,16 @@ define [
$scope.applyVatNumber = ->
pricing.tax({tax_code: 'digital', vat_number: $scope.data.vat_number}).done()
-
$scope.changeCurrency = (newCurrency)->
$scope.currencyCode = newCurrency
pricing.currency(newCurrency).done()
+ $scope.updateExpiry = () ->
+ parsedDateObj = ccUtils.parseExpiry $scope.data.mmYY
+ if parsedDateObj?
+ $scope.data.month = parsedDateObj.month
+ $scope.data.year = parsedDateObj.year
+
$scope.validateCardNumber = validateCardNumber = ->
if $scope.data.number?.length != 0
$scope.validation.correctCardNumber = recurly.validate.cardNumber($scope.data.number)
@@ -98,14 +97,26 @@ define [
if $scope.data.cvv?.length != 0
$scope.validation.correctCvv = recurly.validate.cvv($scope.data.cvv)
+ $scope.inputHasError = inputHasError = (formItem) ->
+ if !formItem?
+ return false
+
+ return (formItem.$touched && formItem.$invalid)
+
+ $scope.isFormValid = isFormValid = (form) ->
+ return $scope.paymentMethod == 'paypal' or
+ (form.$valid and
+ $scope.validation.correctCardNumber and
+ $scope.validation.correctExpiry and
+ $scope.validation.correctCvv)
+
$scope.updateCountry = ->
pricing.address({country:$scope.data.country}).done()
- $scope.changePaymentMethod = (paymentMethod)->
- if paymentMethod == "paypal"
- $scope.usePaypal = true
- else
- $scope.usePaypal = false
+ $scope.setPaymentMethod = setPaymentMethod = (method) ->
+ $scope.paymentMethod = method;
+ $scope.validation.errorFields = {}
+ $scope.genericError = ""
completeSubscription = (err, recurly_token_id) ->
$scope.validation.errorFields = {}
diff --git a/services/web/public/stylesheets/app/subscription.less b/services/web/public/stylesheets/app/subscription.less
index 5e7656adc7..b99fe5d774 100644
--- a/services/web/public/stylesheets/app/subscription.less
+++ b/services/web/public/stylesheets/app/subscription.less
@@ -21,6 +21,15 @@
margin-bottom: -10px;
}
+.input-feedback-message {
+ display: none;
+ font-size: 0.8em;
+
+ .has-error & {
+ display: inline-block;
+ }
+}
+
.payment-submit {
padding-top: (@line-height-computed / 2);
}
From bcc8bfbe6cbfb598c1b982cc6b1fa9bbb90405a2 Mon Sep 17 00:00:00 2001
From: James Allen
Date: Mon, 22 Aug 2016 17:36:33 +0100
Subject: [PATCH 196/378] Redirect to working update billing details end point
that shows a nice message
---
.../Features/Subscription/SubscriptionController.coffee | 7 +++++--
.../coffee/Features/Subscription/SubscriptionRouter.coffee | 1 +
services/web/app/views/subscriptions/dashboard.jade | 5 +++++
.../coffee/Subscription/SubscriptionControllerTests.coffee | 2 +-
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
index 3d2ba910d0..943c78f62d 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
@@ -97,7 +97,7 @@ module.exports = SubscriptionController =
groupSubscriptions: groupSubscriptions
subscriptionTabActive: true
user:user
-
+ saved_billing_details: req.query.saved_billing_details?
userCustomSubscriptionPage: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
@@ -131,9 +131,12 @@ module.exports = SubscriptionController =
currency: "USD"
subdomain: Settings.apis.recurly.subdomain
signature : signature
- successURL : "#{Settings.siteUrl}/user/subscription/update"
+ successURL : "#{Settings.siteUrl}/user/subscription/billing-details/update"
user :
id : user._id
+
+ updateBillingDetails: (req, res, next) ->
+ res.redirect "/user/subscription?saved_billing_details=true"
createSubscription: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee
index f2d66c30c5..62d4d306ab 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee
@@ -16,6 +16,7 @@ module.exports =
webRouter.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage
webRouter.get '/user/subscription/billing-details/edit', AuthenticationController.requireLogin(), SubscriptionController.editBillingDetailsPage
+ webRouter.post '/user/subscription/billing-details/update', AuthenticationController.requireLogin(), SubscriptionController.updateBillingDetails
webRouter.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription
diff --git a/services/web/app/views/subscriptions/dashboard.jade b/services/web/app/views/subscriptions/dashboard.jade
index 9b9464419e..34493e5a8c 100644
--- a/services/web/app/views/subscriptions/dashboard.jade
+++ b/services/web/app/views/subscriptions/dashboard.jade
@@ -40,6 +40,11 @@ block content
.container(ng-controller="UserSubscriptionController")
.row
.col-md-8.col-md-offset-2
+ if saved_billing_details
+ .alert.alert-success
+ i.fa.fa-check
+ |
+ | #{translate("your_billing_details_were_saved")}
.card(ng-if="view == 'overview'")
.page-header
h1 #{translate("your_subscription")}
diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee
index 27e9e571d1..db5e4e8c1c 100644
--- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee
@@ -109,7 +109,7 @@ describe "SubscriptionController sanboxed", ->
it "should set the correct variables for the template", ->
should.exist @res.renderedVariables.signature
- @res.renderedVariables.successURL.should.equal "#{@settings.siteUrl}/user/subscription/update"
+ @res.renderedVariables.successURL.should.equal "#{@settings.siteUrl}/user/subscription/billing-details/update"
@res.renderedVariables.user.id.should.equal @user._id
describe "with a user without subscription", ->
From 362d75ef81c49518c355d2e7d51337edabaac83e Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Tue, 23 Aug 2016 09:59:46 +0100
Subject: [PATCH 197/378] Log subscription errors.
---
services/web/public/coffee/main/new-subscription.coffee | 1 +
1 file changed, 1 insertion(+)
diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee
index f1c81458ef..7d9e4f8501 100644
--- a/services/web/public/coffee/main/new-subscription.coffee
+++ b/services/web/public/coffee/main/new-subscription.coffee
@@ -121,6 +121,7 @@ define [
completeSubscription = (err, recurly_token_id) ->
$scope.validation.errorFields = {}
if err?
+ event_tracking.sendMB "subscription-error", err
# We may or may not be in a digest loop here depending on
# whether recurly could do validation locally, so do it async
$scope.$evalAsync () ->
From 1ac31f318e7eebd7cd005412ed68eef477dc8a8d Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Tue, 23 Aug 2016 10:06:00 +0100
Subject: [PATCH 198/378] Restart validation on form changes.
---
services/web/public/coffee/main/new-subscription.coffee | 3 +++
1 file changed, 3 insertions(+)
diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee
index 7d9e4f8501..0cd2dbf372 100644
--- a/services/web/public/coffee/main/new-subscription.coffee
+++ b/services/web/public/coffee/main/new-subscription.coffee
@@ -86,14 +86,17 @@ define [
$scope.data.year = parsedDateObj.year
$scope.validateCardNumber = validateCardNumber = ->
+ $scope.validation.errorFields = {}
if $scope.data.number?.length != 0
$scope.validation.correctCardNumber = recurly.validate.cardNumber($scope.data.number)
$scope.validateExpiry = validateExpiry = ->
+ $scope.validation.errorFields = {}
if $scope.data.month?.length != 0 and $scope.data.year?.length != 0
$scope.validation.correctExpiry = recurly.validate.expiry($scope.data.month, $scope.data.year)
$scope.validateCvv = validateCvv = ->
+ $scope.validation.errorFields = {}
if $scope.data.cvv?.length != 0
$scope.validation.correctCvv = recurly.validate.cvv($scope.data.cvv)
From f79bee789312fdccde0ccc180ad4d9b77d734f4c Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Tue, 23 Aug 2016 11:27:09 +0100
Subject: [PATCH 199/378] Loading indicator when submitting.
---
services/web/app/views/subscriptions/new.jade | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade
index d26a760e69..f08c2f669a 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.jade
@@ -321,7 +321,11 @@ block content
ng-click="submit()"
ng-disabled="processing || !isFormValid(simpleCCForm);"
sixpack-convert="payment-left-menu-bottom"
- ) {{ paymentMethod === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }}
+ )
+ span(ng-show="processing")
+ i.fa.fa-refresh.fa-spin
+ |
+ | {{ paymentMethod === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }}
.col-md-3.col-md-pull-4
From 4b50505ec9ddd9e640205db6a11321c23b2f871e Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Tue, 23 Aug 2016 11:27:27 +0100
Subject: [PATCH 200/378] suppress all cascading chktex environment errors
---
.../ide/human-readable-logs/HumanReadableLogsRules.coffee | 2 ++
1 file changed, 2 insertions(+)
diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
index ffd46fb0c7..164e78ce66 100644
--- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
+++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
@@ -155,6 +155,7 @@ define -> [
,
ruleId: "hint_mismatched_environment2"
types: ['environment']
+ cascadesFrom: ['environment']
regexToMatch: /Error: `\\end\{([^\}]+)\}' expected but found `\\end\{([^\}]+)\}'.*/
newMessage: "Error: environments do not match: \\begin{$1} ... \\end{$2}"
humanReadableHint: """
@@ -163,6 +164,7 @@ define -> [
,
ruleId: "hint_mismatched_environment3"
types: ['environment']
+ cascadesFrom: ['environment']
regexToMatch: /Warning: No matching \\end found for `\\begin\{([^\}]+)\}'.*/
newMessage: "Warning: No matching \\end found for \\begin{$1}"
humanReadableHint: """
From bc16999a4e92e7133a57f7226b443a2154a304db Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Tue, 23 Aug 2016 11:55:23 +0100
Subject: [PATCH 201/378] Validate on blur; reset validation on focus.
---
services/web/app/views/subscriptions/new.jade | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade
index f08c2f669a..b32fc05b0b 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.jade
@@ -234,8 +234,9 @@ block content
type="text"
ng-model="data.number"
name="ccNumber"
+ ng-focus="validation.correctCardNumber = true; validation.errorFields.number = false;"
+ ng-blur="validateCardNumber();"
required
- ng-change="validateCardNumber()"
cc-format-card-number
)
span.input-feedback-message {{ simpleCCForm.ccNumber.$error.required ? 'This field is required' : 'Please re-check the card number' }}
@@ -248,9 +249,10 @@ block content
type="text"
ng-model="data.mmYY"
name="expiry"
- required
placeholder="MM / YY"
- ng-change="updateExpiry(); validateExpiry()"
+ ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;"
+ ng-blur="updateExpiry(); validateExpiry()"
+ required
cc-format-expiry
)
span.input-feedback-message {{ simpleCCForm.expiry.$error.required ? 'This field is required' : 'Please re-check the expiry date' }}
@@ -261,7 +263,8 @@ block content
input.form-control(
type="text"
ng-model="data.cvv"
- ng-change="validateCvv()"
+ ng-focus="validation.correctCvv = true; validation.errorFields.cvv = false;"
+ ng-blur="validateCvv()"
name="cvv"
required
cc-format-sec-code
From 8abf839d55be4381a4d72e06682a6ba54c6e73d7 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Tue, 23 Aug 2016 13:36:54 +0100
Subject: [PATCH 202/378] Add country to the paypal form.
---
services/web/app/views/subscriptions/new.jade | 8 +++++---
.../web/public/coffee/main/new-subscription.coffee | 13 ++++++++-----
2 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade
index b32fc05b0b..75d806eb7a 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.jade
@@ -279,6 +279,8 @@ block content
) ?
span.input-feedback-message {{ simpleCCForm.cvv.$error.required ? 'This field is required' : 'Please re-check the security code' }}
+
+ div
.form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''")
label(for="country") #{translate('country')}
select#country.form-control(
@@ -290,8 +292,7 @@ block content
)
mixin countries_options()
span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
-
- div
+
if (showVatField)
.form-group
label(for="vat-no") #{translate('vat_number')}
@@ -310,6 +311,7 @@ block content
)
p(ng-if="paymentMethod === 'paypal'") #{translate("paypal_upgrade")}
+
div.price-breakdown(ng-if="price.next.tax !== '0.00'")
hr.thin
span Total:
@@ -326,7 +328,7 @@ block content
sixpack-convert="payment-left-menu-bottom"
)
span(ng-show="processing")
- i.fa.fa-refresh.fa-spin
+ i.fa.fa-spinner.fa-spin
|
| {{ paymentMethod === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }}
diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee
index 0cd2dbf372..f5096c06ea 100644
--- a/services/web/public/coffee/main/new-subscription.coffee
+++ b/services/web/public/coffee/main/new-subscription.coffee
@@ -107,13 +107,16 @@ define [
return (formItem.$touched && formItem.$invalid)
$scope.isFormValid = isFormValid = (form) ->
- return $scope.paymentMethod == 'paypal' or
- (form.$valid and
- $scope.validation.correctCardNumber and
- $scope.validation.correctExpiry and
- $scope.validation.correctCvv)
+ if $scope.paymentMethod == 'paypal'
+ return $scope.data.country != ""
+ else
+ return (form.$valid and
+ $scope.validation.correctCardNumber and
+ $scope.validation.correctExpiry and
+ $scope.validation.correctCvv)
$scope.updateCountry = ->
+ console.log $scope.data.country
pricing.address({country:$scope.data.country}).done()
$scope.setPaymentMethod = setPaymentMethod = (method) ->
From d3ebdb64b2dc7f6a918d8858601d7fb041b58c11 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Tue, 23 Aug 2016 15:31:09 +0100
Subject: [PATCH 203/378] precompile the jade partial views
---
services/web/app/coffee/infrastructure/Modules.coffee | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/services/web/app/coffee/infrastructure/Modules.coffee b/services/web/app/coffee/infrastructure/Modules.coffee
index 2df8907f7e..0dfbf3fa22 100644
--- a/services/web/app/coffee/infrastructure/Modules.coffee
+++ b/services/web/app/coffee/infrastructure/Modules.coffee
@@ -25,14 +25,14 @@ module.exports = Modules =
for module in @modules
for view, partial of module.viewIncludes or {}
@viewIncludes[view] ||= []
- @viewIncludes[view].push fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade"))
+ @viewIncludes[view].push jade.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade")), doctype: "html")
moduleIncludes: (view, locals) ->
- partials = Modules.viewIncludes[view] or []
+ compiledPartials = Modules.viewIncludes[view] or []
html = ""
- for partial in partials
- compiler = jade.compile(partial, doctype: "html")
- html += compiler(locals)
+ for compiledPartial in compiledPartials
+ d = new Date()
+ html += compiledPartial(locals)
return html
moduleIncludesAvailable: (view) ->
From 6a72c2fce08d2e9ef0602d7c08bc7c7c59556b99 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Tue, 23 Aug 2016 16:46:23 +0100
Subject: [PATCH 204/378] comment settings back out
---
services/web/config/settings.defaults.coffee | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee
index 56f3ee2b81..10fef74a11 100644
--- a/services/web/config/settings.defaults.coffee
+++ b/services/web/config/settings.defaults.coffee
@@ -114,9 +114,9 @@ module.exports = settings =
showSocialButtons: false
showComments: false
- cdn:
- web:
- host:"http://nowhere.sharelatex.dev"
+ # cdn:
+ # web:
+ # host:"http://nowhere.sharelatex.dev"
# darkHost:"http://cdn.sharelatex.dev:3000"
# Where your instance of ShareLaTeX can be found publically. Used in emails
From fcc75854449c5e690ae097a44d9efdbe98dc2595 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Tue, 23 Aug 2016 16:58:39 +0100
Subject: [PATCH 205/378] put jquery as first script in head tag
---
services/web/app/views/layout.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade
index f1bfeedfee..88fe4187fe 100644
--- a/services/web/app/views/layout.jade
+++ b/services/web/app/views/layout.jade
@@ -52,7 +52,6 @@ html(itemscope, itemtype='http://schema.org/Product')
window.csrfToken = "#{csrfToken}";
block scripts
- script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
script(type="text/javascript").
var noCdnKey = "nocdn=true"
@@ -61,6 +60,7 @@ html(itemscope, itemtype='http://schema.org/Product')
if (cdnBlocked && !noCdnAlreadyInUrl) {
window.location.search += '&'+noCdnKey;
}
+ script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
script.
window.sharelatex = {
From 934e908697b95969f3c4581992604144429e9f9e Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Tue, 23 Aug 2016 17:00:13 +0100
Subject: [PATCH 206/378] just use plain req.ip for logging
---
services/web/app/coffee/infrastructure/ExpressLocals.coffee | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index 60f1b682e5..d3b9d9a24b 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -64,8 +64,7 @@ module.exports = (app, webRouter, apiRouter)->
cdnBlocked = req.query.nocdn == 'true' or req.session.cdnBlocked
if cdnBlocked and !req.session.cdnBlocked?
- ip = req.ip || req.socket?.socket?.remoteAddress || req.socket?.remoteAddress
- logger.log user_id:req?.session?.user?._id, ip:ip, "cdnBlocked for user, not using it"
+ logger.log user_id:req?.session?.user?._id, ip:req?.ip, "cdnBlocked for user, not using it and turning it off for future requets"
req.session.cdnBlocked = true
isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark"
From c82ab65077806717760679c2297355dba3fa9be7 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Wed, 24 Aug 2016 11:02:53 +0100
Subject: [PATCH 207/378] Bind to an object to avoid writing in different .
---
services/web/app/views/subscriptions/new.jade | 16 ++++++++--------
.../public/coffee/main/new-subscription.coffee | 12 ++++++------
2 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade
index 75d806eb7a..31c8205e7c 100644
--- a/services/web/app/views/subscriptions/new.jade
+++ b/services/web/app/views/subscriptions/new.jade
@@ -60,19 +60,19 @@ block content
.row
.col-md-6
label.radio-inline
- input.paymentTypeOption(type="radio",value="credit_card", ng-model="paymentMethod")
+ input.paymentTypeOption(type="radio",value="credit_card", ng-model="paymentMethod.value")
i.fa.fa-cc-mastercard.fa-3x
span
i.fa.fa-cc-visa.fa-3x
.col-md-6
label.radio-inline
- input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod")
+ input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod.value")
i.fa.fa-cc-paypal.fa-3x
.alert.alert-warning.small(ng-show="genericError")
strong {{genericError}}
- span(ng-hide="paymentMethod == 'paypal'")
+ span(ng-hide="paymentMethod.value == 'paypal'")
.row
.col-md-12
.form-group
@@ -184,7 +184,7 @@ block content
a.payment-method-toggle-switch(
href
ng-click="setPaymentMethod('credit_card');"
- ng-class="paymentMethod === 'credit_card' ? 'payment-method-toggle-switch-selected' : ''"
+ ng-class="paymentMethod.value === 'credit_card' ? 'payment-method-toggle-switch-selected' : ''"
)
i.fa.fa-cc-mastercard.fa-2x
span
@@ -194,14 +194,14 @@ block content
a.payment-method-toggle-switch(
href
ng-click="setPaymentMethod('paypal');"
- ng-class="paymentMethod === 'paypal' ? 'payment-method-toggle-switch-selected' : ''"
+ ng-class="paymentMethod.value === 'paypal' ? 'payment-method-toggle-switch-selected' : ''"
)
i.fa.fa-cc-paypal.fa-2x
.alert.alert-warning.small(ng-show="genericError")
strong {{genericError}}
- div(ng-if="paymentMethod === 'credit_card'")
+ div(ng-if="paymentMethod.value === 'credit_card'")
.row
.col-xs-6
.form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''")
@@ -310,7 +310,7 @@ block content
ng-model="data.coupon"
)
- p(ng-if="paymentMethod === 'paypal'") #{translate("paypal_upgrade")}
+ p(ng-if="paymentMethod.value === 'paypal'") #{translate("paypal_upgrade")}
div.price-breakdown(ng-if="price.next.tax !== '0.00'")
hr.thin
@@ -330,7 +330,7 @@ block content
span(ng-show="processing")
i.fa.fa-spinner.fa-spin
|
- | {{ paymentMethod === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }}
+ | {{ paymentMethod.value === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }}
.col-md-3.col-md-pull-4
diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee
index f5096c06ea..5c9d5cdd86 100644
--- a/services/web/public/coffee/main/new-subscription.coffee
+++ b/services/web/public/coffee/main/new-subscription.coffee
@@ -14,7 +14,8 @@ define [
event_tracking.sendMB "subscription-form", { plan : window.plan_code }
- $scope.paymentMethod = "credit_card"
+ $scope.paymentMethod =
+ value: "credit_card"
$scope.data =
number: ""
@@ -107,7 +108,7 @@ define [
return (formItem.$touched && formItem.$invalid)
$scope.isFormValid = isFormValid = (form) ->
- if $scope.paymentMethod == 'paypal'
+ if $scope.paymentMethod.value == 'paypal'
return $scope.data.country != ""
else
return (form.$valid and
@@ -116,11 +117,10 @@ define [
$scope.validation.correctCvv)
$scope.updateCountry = ->
- console.log $scope.data.country
pricing.address({country:$scope.data.country}).done()
$scope.setPaymentMethod = setPaymentMethod = (method) ->
- $scope.paymentMethod = method;
+ $scope.paymentMethod.value = method;
$scope.validation.errorFields = {}
$scope.genericError = ""
@@ -142,7 +142,7 @@ define [
currencyCode:pricing.items.currency
plan_code:pricing.items.plan.code
coupon_code:pricing.items?.coupon?.code || ""
- isPaypal: $scope.paymentMethod == 'paypal'
+ isPaypal: $scope.paymentMethod.value == 'paypal'
address:
address1: $scope.data.address1
address2: $scope.data.address2
@@ -170,7 +170,7 @@ define [
$scope.submit = ->
$scope.processing = true
- if $scope.paymentMethod == 'paypal'
+ if $scope.paymentMethod.value == 'paypal'
opts = { description: $scope.planName }
recurly.paypal opts, completeSubscription
else
From 98770974dfb61915bd771f1f113c502bbb75cc98 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Wed, 24 Aug 2016 16:34:33 +0100
Subject: [PATCH 208/378] remove console.log
---
services/web/public/coffee/main/subscription-dashboard.coffee | 1 -
1 file changed, 1 deletion(-)
diff --git a/services/web/public/coffee/main/subscription-dashboard.coffee b/services/web/public/coffee/main/subscription-dashboard.coffee
index 69d030ed3b..7476d814e1 100644
--- a/services/web/public/coffee/main/subscription-dashboard.coffee
+++ b/services/web/public/coffee/main/subscription-dashboard.coffee
@@ -18,7 +18,6 @@ define [
App.controller "ChangePlanFormController", ($scope, $modal, MultiCurrencyPricing)->
setupReturly()
- console.log("init")
taxRate = window.taxRate
$scope.changePlan = ->
From 74bc157e7c2df1d4a6f46d1c4897341454f160a4 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Wed, 24 Aug 2016 16:05:22 +0100
Subject: [PATCH 209/378] added force recompile option
---
.../web/app/views/project/editor/pdf.jade | 16 +++++++++++++++
.../ide/pdf/controllers/PdfController.coffee | 20 ++++++++++++++++++-
.../public/stylesheets/app/editor/pdf.less | 4 ++++
3 files changed, 39 insertions(+), 1 deletion(-)
diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade
index 492f75f582..2dfcb0a0eb 100644
--- a/services/web/app/views/project/editor/pdf.jade
+++ b/services/web/app/views/project/editor/pdf.jade
@@ -36,6 +36,15 @@ div.full-size.pdf(ng-controller="PdfController")
i.fa.fa-fw(ng-class="{'fa-check': draft}")
| #{translate("fast")}
span.subdued [draft]
+ li.dropdown-header #{translate("compile_time_checks")}
+ li
+ a(href, ng-click="stop_on_validation_error = true")
+ i.fa.fa-fw(ng-class="{'fa-check': stop_on_validation_error}")
+ | #{translate("stop_on_validation_error")}
+ li
+ a(href, ng-click="stop_on_validation_error = false")
+ i.fa.fa-fw(ng-class="{'fa-check': !stop_on_validation_error}")
+ | #{translate("ignore_validation_errors")}
li.dropdown-header #{translate("file_checks")}
li
a(href, ng-click="recompile({check:true})")
@@ -204,6 +213,13 @@ div.full-size.pdf(ng-controller="PdfController")
p.entry-content(ng-show="entry.content") {{ entry.content.trim() }}
+ p.force-recompile(ng-if="$first && !check && stop_on_validation_error")
+ a.btn.btn-info(
+ href,
+ ng-disabled="pdf.compiling",
+ ng-click="recompile({force:true})"
+ )
+ span() #{translate("force_recompile")}
p
.files-dropdown-container
a.btn.btn-default.btn-sm(
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index a31a336885..d6c19cd0ea 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -73,6 +73,12 @@ define [
$scope.pdf.view = 'errors'
$scope.pdf.renderingError = true
+ # abort compile if syntax checks fail
+ $scope.stop_on_validation_error = localStorage("stop_on_validation_error:#{$scope.project_id}") or ide.$scope?.user?.betaProgram
+ $scope.$watch "stop_on_validation_error", (new_value, old_value) ->
+ if new_value? and old_value != new_value
+ localStorage("stop_on_validation_error:#{$scope.project_id}", new_value)
+
$scope.draft = localStorage("draft:#{$scope.project_id}") or false
$scope.$watch "draft", (new_value, old_value) ->
if new_value? and old_value != new_value
@@ -83,10 +89,17 @@ define [
params = {}
if options.isAutoCompile
params["auto_compile"]=true
+ # keep track of whether this is a compile or check
+ $scope.check = if options.check then true else false
+ # send appropriate check type to clsi
+ checkType = switch
+ when $scope.check then "validate" # validate only
+ when $scope.stop_on_validation_error then "error" # try to compile
+ else "silent" # ignore errors
return $http.post url, {
rootDoc_id: options.rootDocOverride_id or null
draft: $scope.draft
- check: if options.check then "validate" else "silent"
+ check: checkType
_csrf: window.csrfToken
}, {params: params}
@@ -347,6 +360,11 @@ define [
$scope.pdf.compiling = true
+ if options?.force
+ # for forced compile, turn off validation check
+ $scope.stop_on_validation_error = false
+ $scope.shouldShowLogs = false # hide the logs while compiling
+
ide.$scope.$broadcast("flush-changes")
options.rootDocOverride_id = getRootDocOverride_id()
diff --git a/services/web/public/stylesheets/app/editor/pdf.less b/services/web/public/stylesheets/app/editor/pdf.less
index 2183798650..12c86a6190 100644
--- a/services/web/public/stylesheets/app/editor/pdf.less
+++ b/services/web/public/stylesheets/app/editor/pdf.less
@@ -161,6 +161,10 @@
.dropdown {
position: relative;
}
+ .force-recompile {
+ margin-top: 10px;
+ text-align: right;
+ }
}
.synctex-controls {
From 7f2041504889b6f6baee870c839f086009f64984 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Wed, 24 Aug 2016 16:48:45 +0100
Subject: [PATCH 210/378] only set compileExited on compile errors
---
.../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index d6c19cd0ea..2ad662dc34 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -118,6 +118,7 @@ define [
$scope.pdf.renderingError = false
$scope.pdf.projectTooLarge = false
$scope.pdf.compileTerminated = false
+ $scope.pdf.compileExited = false
# make a cache to look up files by name
fileByPath = {}
@@ -144,7 +145,6 @@ define [
fetchLogs(fileByPath)
else if response.status in ["validation-fail", "validation-pass"]
$scope.pdf.view = 'pdf'
- $scope.pdf.compileExited = true
$scope.pdf.url = last_pdf_url
$scope.shouldShowLogs = true
fetchLogs(fileByPath, { validation: true })
From 26a4076c2286a15317696ead7f05ac796f84e605 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Thu, 25 Aug 2016 10:48:29 +0100
Subject: [PATCH 211/378] add redirect to /i/university for ab test
---
.../Features/StaticPages/UniversityController.coffee | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/services/web/app/coffee/Features/StaticPages/UniversityController.coffee b/services/web/app/coffee/Features/StaticPages/UniversityController.coffee
index e607c3a020..5d405057ad 100644
--- a/services/web/app/coffee/Features/StaticPages/UniversityController.coffee
+++ b/services/web/app/coffee/Features/StaticPages/UniversityController.coffee
@@ -8,7 +8,7 @@ sanitize = require('sanitizer')
Settings = require("settings-sharelatex")
contentful = require('contentful')
marked = require("marked")
-
+sixpack = require("../../infrastructure/Sixpack")
@@ -36,8 +36,13 @@ module.exports = UniversityController =
getIndexPage: (req, res)->
- req.url = "/university/index.html"
- UniversityController.getPage req, res
+ client = sixpack.client(req?.session?.user?._id?.toString() || req.ip)
+ client.participate 'instapage-pages', ['default', 'instapage'], (err, response)->
+ if response?.alternative?.name == "instapage"
+ return res.redirect("/i/university")
+ else
+ req.url = "/university/index.html"
+ UniversityController.getPage req, res
_directProxy: (originUrl, res)->
upstream = request.get(originUrl)
From 9a1934465a82a6e1896b048d70fc41a70f6cf22e Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 25 Aug 2016 15:51:56 +0100
Subject: [PATCH 212/378] clean up syntax check options on menu
---
.../web/app/views/project/editor/pdf.jade | 23 ++++++++++++-------
1 file changed, 15 insertions(+), 8 deletions(-)
diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade
index 2dfcb0a0eb..dbbb71ced7 100644
--- a/services/web/app/views/project/editor/pdf.jade
+++ b/services/web/app/views/project/editor/pdf.jade
@@ -45,7 +45,6 @@ div.full-size.pdf(ng-controller="PdfController")
a(href, ng-click="stop_on_validation_error = false")
i.fa.fa-fw(ng-class="{'fa-check': !stop_on_validation_error}")
| #{translate("ignore_validation_errors")}
- li.dropdown-header #{translate("file_checks")}
li
a(href, ng-click="recompile({check:true})")
i.fa.fa-fw()
@@ -114,6 +113,21 @@ div.full-size.pdf(ng-controller="PdfController")
strong #{translate("compile_error")}.
span #{translate("generic_failed_compile_message")}.
+ .alert.alert-danger(ng-show="pdf.failedCheck")
+ div
+ strong #{translate("failed_compile_check")}.
+ div(ng-show="!check")
+ a(
+ href,
+ ng-disabled="pdf.compiling",
+ ng-click="recompile({try:true})"
+ ) #{translate("failed_compile_check_try")}
+ a(
+ href,
+ ng-disabled="pdf.compiling",
+ ng-click="recompile({force:true})"
+ ) #{translate("failed_compile_check_ignore")}
+
div(ng-repeat="entry in pdf.logEntries.all", ng-controller="PdfLogEntryController")
.alert(
ng-class="{\
@@ -213,13 +227,6 @@ div.full-size.pdf(ng-controller="PdfController")
p.entry-content(ng-show="entry.content") {{ entry.content.trim() }}
- p.force-recompile(ng-if="$first && !check && stop_on_validation_error")
- a.btn.btn-info(
- href,
- ng-disabled="pdf.compiling",
- ng-click="recompile({force:true})"
- )
- span() #{translate("force_recompile")}
p
.files-dropdown-container
a.btn.btn-default.btn-sm(
From 588be162554f50848795df5504aed834a6536bdb Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 25 Aug 2016 15:52:37 +0100
Subject: [PATCH 213/378] handle try/turn off options for syntax check
---
.../coffee/ide/pdf/controllers/PdfController.coffee | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index 2ad662dc34..0c3136c4f6 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -89,11 +89,14 @@ define [
params = {}
if options.isAutoCompile
params["auto_compile"]=true
+ # if the previous run was a check, clear the error logs
+ $scope.pdf.logEntries = [] if $scope.check
# keep track of whether this is a compile or check
$scope.check = if options.check then true else false
# send appropriate check type to clsi
checkType = switch
when $scope.check then "validate" # validate only
+ when options.try then "silent" # allow use to try compile once
when $scope.stop_on_validation_error then "error" # try to compile
else "silent" # ignore errors
return $http.post url, {
@@ -119,6 +122,7 @@ define [
$scope.pdf.projectTooLarge = false
$scope.pdf.compileTerminated = false
$scope.pdf.compileExited = false
+ $scope.pdf.failedCheck = false
# make a cache to look up files by name
fileByPath = {}
@@ -147,6 +151,7 @@ define [
$scope.pdf.view = 'pdf'
$scope.pdf.url = last_pdf_url
$scope.shouldShowLogs = true
+ $scope.pdf.failedCheck = true if response.status is "validation-fail"
fetchLogs(fileByPath, { validation: true })
else if response.status == "exited"
$scope.pdf.view = 'pdf'
@@ -365,6 +370,9 @@ define [
$scope.stop_on_validation_error = false
$scope.shouldShowLogs = false # hide the logs while compiling
+ if options?.try
+ $scope.shouldShowLogs = false # hide the logs while compiling
+
ide.$scope.$broadcast("flush-changes")
options.rootDocOverride_id = getRootDocOverride_id()
From 696a7206c17aac1cb406968f782c8fa37267bb73 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 25 Aug 2016 16:55:29 +0100
Subject: [PATCH 214/378] clean up display of error message
---
.../web/app/views/project/editor/pdf.jade | 24 +++++++++----------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade
index dbbb71ced7..682cfaa5c4 100644
--- a/services/web/app/views/project/editor/pdf.jade
+++ b/services/web/app/views/project/editor/pdf.jade
@@ -114,19 +114,19 @@ div.full-size.pdf(ng-controller="PdfController")
span #{translate("generic_failed_compile_message")}.
.alert.alert-danger(ng-show="pdf.failedCheck")
- div
+ .card
strong #{translate("failed_compile_check")}.
- div(ng-show="!check")
- a(
- href,
- ng-disabled="pdf.compiling",
- ng-click="recompile({try:true})"
- ) #{translate("failed_compile_check_try")}
- a(
- href,
- ng-disabled="pdf.compiling",
- ng-click="recompile({force:true})"
- ) #{translate("failed_compile_check_ignore")}
+ div(ng-show="!check")
+ a.text-info(
+ href,
+ ng-disabled="pdf.compiling",
+ ng-click="recompile({try:true})"
+ ) #{translate("failed_compile_check_try")} (
+ a.text-info(
+ href,
+ ng-disabled="pdf.compiling",
+ ng-click="recompile({force:true})"
+ ) #{translate("failed_compile_check_ignore")})
div(ng-repeat="entry in pdf.logEntries.all", ng-controller="PdfLogEntryController")
.alert(
From 092d7da4793674749ad1d8882a9de254d3d71c98 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Thu, 25 Aug 2016 16:55:49 +0100
Subject: [PATCH 215/378] use Syntax error for consistency in file check
---
.../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index 0c3136c4f6..b3f7de4db6 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -280,7 +280,7 @@ define [
else
warnings.push result
all = [].concat errors, warnings
- logHints = HumanReadableLogs.parse {type: "Validation", all, errors, warnings}
+ logHints = HumanReadableLogs.parse {type: "Syntax", all, errors, warnings}
accumulateResults logHints
processBiber = (log) ->
From 3c59de31dfdb71d2f20e558d62f41d9719f5c2ad Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Fri, 26 Aug 2016 15:14:57 +0100
Subject: [PATCH 216/378] finalise display of syntax check error message
---
.../web/app/views/project/editor/pdf.jade | 30 ++++++++++---------
1 file changed, 16 insertions(+), 14 deletions(-)
diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade
index 682cfaa5c4..4463043435 100644
--- a/services/web/app/views/project/editor/pdf.jade
+++ b/services/web/app/views/project/editor/pdf.jade
@@ -48,7 +48,7 @@ div.full-size.pdf(ng-controller="PdfController")
li
a(href, ng-click="recompile({check:true})")
i.fa.fa-fw()
- | #{translate("run_syntax_check")}
+ | #{translate("run_syntax_check_now")}
a(
href
ng-click="stop()"
@@ -114,19 +114,21 @@ div.full-size.pdf(ng-controller="PdfController")
span #{translate("generic_failed_compile_message")}.
.alert.alert-danger(ng-show="pdf.failedCheck")
- .card
- strong #{translate("failed_compile_check")}.
- div(ng-show="!check")
- a.text-info(
- href,
- ng-disabled="pdf.compiling",
- ng-click="recompile({try:true})"
- ) #{translate("failed_compile_check_try")} (
- a.text-info(
- href,
- ng-disabled="pdf.compiling",
- ng-click="recompile({force:true})"
- ) #{translate("failed_compile_check_ignore")})
+ strong #{translate("failed_compile_check")}.
+ p
+ p.text-center(ng-show="!check")
+ a.text-info(
+ href,
+ ng-disabled="pdf.compiling",
+ ng-click="recompile({try:true})"
+ ) #{translate("failed_compile_check_try")}
+ | #{translate("failed_compile_option_or")}
+ a.text-info(
+ href,
+ ng-disabled="pdf.compiling",
+ ng-click="recompile({force:true})"
+ ) #{translate("failed_compile_check_ignore")}
+ | .
div(ng-repeat="entry in pdf.logEntries.all", ng-controller="PdfLogEntryController")
.alert(
From 74959e794767596919ee01b9c3d30ea2564de1bc Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Fri, 26 Aug 2016 15:21:18 +0100
Subject: [PATCH 217/378] add Grunt task to run without parallel watch
---
services/web/Gruntfile.coffee | 1 +
1 file changed, 1 insertion(+)
diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee
index 63d0eb6f79..4307621af5 100644
--- a/services/web/Gruntfile.coffee
+++ b/services/web/Gruntfile.coffee
@@ -381,6 +381,7 @@ module.exports = (grunt) ->
grunt.registerTask 'test:modules:unit', 'Run the unit tests for the modules', ['compile:modules:server', 'compile:modules:unit_tests'].concat(moduleUnitTestTasks)
grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'parallel']
+ grunt.registerTask 'runq', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'exec']
grunt.registerTask 'default', 'run'
From 7bf3b9daf91d4488ca4202c4627aa0b4532a4572 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Fri, 26 Aug 2016 15:54:01 +0100
Subject: [PATCH 218/378] add event tracking for syntax check
---
.../public/coffee/ide/pdf/controllers/PdfController.coffee | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index b3f7de4db6..c79d654dbc 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -93,6 +93,7 @@ define [
$scope.pdf.logEntries = [] if $scope.check
# keep track of whether this is a compile or check
$scope.check = if options.check then true else false
+ event_tracking.sendMB "syntax-check-request" if options.check
# send appropriate check type to clsi
checkType = switch
when $scope.check then "validate" # validate only
@@ -152,6 +153,7 @@ define [
$scope.pdf.url = last_pdf_url
$scope.shouldShowLogs = true
$scope.pdf.failedCheck = true if response.status is "validation-fail"
+ event_tracking.sendMB "syntax-check-#{response.status}"
fetchLogs(fileByPath, { validation: true })
else if response.status == "exited"
$scope.pdf.view = 'pdf'
@@ -281,6 +283,7 @@ define [
warnings.push result
all = [].concat errors, warnings
logHints = HumanReadableLogs.parse {type: "Syntax", all, errors, warnings}
+ event_tracking.sendMB "syntax-check-return-count", {errors:errors.length, warnings:warnings.length}
accumulateResults logHints
processBiber = (log) ->
@@ -366,12 +369,14 @@ define [
$scope.pdf.compiling = true
if options?.force
- # for forced compile, turn off validation check
+ # for forced compile, turn off validation check and ignore errors
$scope.stop_on_validation_error = false
$scope.shouldShowLogs = false # hide the logs while compiling
+ event_tracking.sendMB "syntax-check-turn-off-checking"
if options?.try
$scope.shouldShowLogs = false # hide the logs while compiling
+ event_tracking.sendMB "syntax-check-try-compile-anyway"
ide.$scope.$broadcast("flush-changes")
From ddc0023c647183cb22069c4bd24a587ad54051d6 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Tue, 30 Aug 2016 13:07:37 +0100
Subject: [PATCH 219/378] make forceCreate the default for creating
notifications
---
.../Notifications/NotificationsBuilder.coffee | 4 +--
.../Notifications/NotificationsHandler.coffee | 5 ++--
.../NotificationsHandlerTests.coffee | 27 ++++---------------
3 files changed, 9 insertions(+), 27 deletions(-)
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
index 646a520a1c..ffb71f70e6 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
@@ -12,7 +12,7 @@ module.exports =
groupName: licence.name
subscription_id: licence.subscription_id
logger.log user_id:user._id, key:key, "creating notification key for user"
- NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, false, callback
+ NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, callback
read: (callback = ->)->
NotificationsHandler.markAsReadWithKey user._id, @key, callback
@@ -26,6 +26,6 @@ module.exports =
projectId: project._id.toString()
token: invite.token
logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user"
- NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, true, callback
+ NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback
read: (callback=()->) ->
NotificationsHandler.markAsReadByKeyOnly @key, callback
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
index 2ee97dae49..5a6ca47c2e 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
@@ -29,16 +29,15 @@ module.exports =
unreadNotifications = []
callback(null, unreadNotifications)
- createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)->
+ createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)->
payload = {
key:key
messageOpts:messageOpts
templateKey:templateKey
+ forceCreate: true
}
if expiryDateTime?
payload.expires = expiryDateTime
- if forceCreate
- payload.forceCreate = true
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
timeout: oneSecond
diff --git a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
index 0aafe6e5fc..7ee3476b4b 100644
--- a/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Notifications/NotificationsHandlerTests.coffee
@@ -61,14 +61,13 @@ describe 'NotificationsHandler', ->
@messageOpts = {value:12344}
@templateKey = "renderThisHtml"
@expiry = null
- @forceCreate = false
it "should post the message over", (done)->
- @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, @forceCreate, =>
+ @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, =>
args = @request.args[0][0]
args.uri.should.equal "#{notificationUrl}/user/#{user_id}"
args.timeout.should.equal 1000
- expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts}
+ expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts, forceCreate:true}
assert.deepEqual(args.json, expectedJson)
done()
@@ -78,33 +77,17 @@ describe 'NotificationsHandler', ->
@messageOpts = {value:12344}
@templateKey = "renderThisHtml"
@expiry = new Date()
- @forceCreate = false
it 'should post the message over with expiry field', (done) ->
- @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, @forceCreate, =>
+ @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, =>
args = @request.args[0][0]
args.uri.should.equal "#{notificationUrl}/user/#{user_id}"
args.timeout.should.equal 1000
- expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts, expires: @expiry}
+ expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts, expires: @expiry, forceCreate:true}
assert.deepEqual(args.json, expectedJson)
done()
- describe 'when forceCreate is true', ->
- beforeEach ->
- @key = "some key here"
- @messageOpts = {value:12344}
- @templateKey = "renderThisHtml"
- @expiry = null
- @forceCreate = true
-
- it 'should add a forceCreate=true flag to the payload', (done) ->
- @handler.createNotification user_id, @key, @templateKey, @messageOpts, @expiry, @forceCreate, =>
- args = @request.args[0][0]
- args.uri.should.equal "#{notificationUrl}/user/#{user_id}"
- args.timeout.should.equal 1000
- expectedJson = {key:@key, templateKey:@templateKey, messageOpts:@messageOpts, forceCreate: @forceCreate}
- assert.deepEqual(args.json, expectedJson)
- done()
+
describe "markAsReadByKeyOnly", ->
beforeEach ->
From 4a76fcd13b42b2a6163c383313a45d3870a53903 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 30 Aug 2016 14:26:57 +0100
Subject: [PATCH 220/378] Change first param of `getMemberSubscriptions` to
`user_or_id`, to match semantics of usage.
This function works whether a user object, or an ObjectId is passed, but the `user_id`
param name is confusing.
---
.../Features/Subscription/SubscriptionLocator.coffee | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee
index d73a35a69d..beee4a3158 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee
@@ -14,7 +14,11 @@ module.exports =
logger.log user_id:user_id, "got users subscription"
callback(err, subscription)
- getMemberSubscriptions: (user_id, callback) ->
+ getMemberSubscriptions: (user_or_id, callback) ->
+ if user_or_id? and user_or_id._id?
+ user_id = user_or_id._id
+ else if user_or_id?
+ user_id = user_or_id
logger.log user_id: user_id, "getting users group subscriptions"
Subscription.find(member_ids: user_id).populate("admin_id").exec callback
@@ -25,4 +29,4 @@ module.exports =
Subscription.findOne {member_ids: user_id, _id:subscription_id}, {_id:1}, callback
getGroupSubscriptionMemberOf: (user_id, callback)->
- Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback
\ No newline at end of file
+ Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback
From 2885164748712e1d617f77a11ff6a73d9b69b1f9 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Tue, 30 Aug 2016 15:32:23 +0100
Subject: [PATCH 221/378] Isolate create project dropdown styles, to avoid
influencing other dropdowns.
---
.../app/views/project/list/project-list-minimal.jade | 8 ++------
services/web/public/stylesheets/app/project-list.less | 10 ++++++++++
2 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/services/web/app/views/project/list/project-list-minimal.jade b/services/web/app/views/project/list/project-list-minimal.jade
index b73210f016..e7e5dc85fc 100644
--- a/services/web/app/views/project/list/project-list-minimal.jade
+++ b/services/web/app/views/project/list/project-list-minimal.jade
@@ -160,19 +160,15 @@
.row
.col-md-offset-4.col-md-4
- .dropdown(dropdown)
+ .dropdown.minimal-create-proj-dropdown(dropdown)
a.btn.btn-success.dropdown-toggle(
href="#",
data-toggle="dropdown",
dropdown-toggle
)
| Create First Project
- style.
- .dropdown{text-align:center;}
- .button, .dropdown-menu{margin:2px auto}
- .dropdown-menu{width:200px; left:50%; margin-left:-100px;}
- ul.dropdown-menu(role="menu", style="text-align:center;")
+ ul.dropdown-menu.minimal-create-proj-dropdown-menu(role="menu", style="text-align:center;")
li
a(
href,
diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less
index d3131f136c..8e5da6cb3d 100644
--- a/services/web/public/stylesheets/app/project-list.less
+++ b/services/web/public/stylesheets/app/project-list.less
@@ -279,3 +279,13 @@ ul.project-list {
font-weight: 700;
}
}
+
+.minimal-create-proj-dropdown {
+ text-align:center;
+
+ &-menu {
+ width:200px;
+ left:50%;
+ margin-left:-100px;
+ }
+}
From e3fe4a2eeb3c0608d1141ac7d779ceb3c1493da7 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Tue, 30 Aug 2016 16:07:31 +0100
Subject: [PATCH 222/378] Rename empty projects list file, do some clean-up.
---
services/web/app/views/project/list.jade | 2 +-
.../project/list/empty-project-list.jade | 46 ++++
.../project/list/project-list-minimal.jade | 198 ------------------
3 files changed, 47 insertions(+), 199 deletions(-)
create mode 100644 services/web/app/views/project/list/empty-project-list.jade
delete mode 100644 services/web/app/views/project/list/project-list-minimal.jade
diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade
index 1c80b9a7a2..8204aed813 100644
--- a/services/web/app/views/project/list.jade
+++ b/services/web/app/views/project/list.jade
@@ -33,6 +33,6 @@ block content
span(ng-if="first_sign_up == 'minimial' && projects.length == 0")
.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8
- include ./list/project-list-minimal
+ include ./list/empty-project-list
include ./list/modals
\ No newline at end of file
diff --git a/services/web/app/views/project/list/empty-project-list.jade b/services/web/app/views/project/list/empty-project-list.jade
new file mode 100644
index 0000000000..a6c6dcd299
--- /dev/null
+++ b/services/web/app/views/project/list/empty-project-list.jade
@@ -0,0 +1,46 @@
+.row.row-spaced
+ .col-xs-12
+ .card.card-thin.project-list-card
+ div.welcome.text-centered(ng-cloak)
+ h2 #{translate("welcome_to_sl")}
+ p #{translate("new_to_latex_look_at")}
+ a(href="/templates") #{translate("templates").toLowerCase()}
+ | #{translate("or")}
+ a(href="/learn") #{translate("latex_help_guide")}
+
+
+ .row
+ .col-md-offset-4.col-md-4
+ .dropdown.minimal-create-proj-dropdown(dropdown)
+ a.btn.btn-success.dropdown-toggle(
+ href="#",
+ data-toggle="dropdown",
+ dropdown-toggle
+ )
+ | Create First Project
+
+ ul.dropdown-menu.minimal-create-proj-dropdown-menu(role="menu")
+ li
+ a(
+ href,
+ ng-click="openCreateProjectModal()"
+ ) #{translate("blank_project")}
+ li
+ a(
+ href,
+ ng-click="openCreateProjectModal('example')"
+ ) #{translate("example_project")}
+ li
+ a(
+ href,
+ ng-click="openUploadProjectModal()"
+ ) #{translate("upload_project")}
+ != moduleIncludes("newProjectMenu", locals)
+ if (templates)
+ li.divider
+ li.dropdown-header #{translate("templates")}
+ each item in templates
+ li
+ a.menu-indent(href=item.url) #{translate(item.name)}
+
+
diff --git a/services/web/app/views/project/list/project-list-minimal.jade b/services/web/app/views/project/list/project-list-minimal.jade
deleted file mode 100644
index e7e5dc85fc..0000000000
--- a/services/web/app/views/project/list/project-list-minimal.jade
+++ /dev/null
@@ -1,198 +0,0 @@
-.row
- .col-xs-12(ng-cloak)
-
- .project-tools(ng-cloak)
- .btn-toolbar(ng-show="filter != 'archived'")
- .btn-group(ng-hide="selectedProjects.length < 1")
- a.btn.btn-default(
- href='#',
- tooltip="#{translate('download')}",
- tooltip-placement="bottom",
- tooltip-append-to-body="true",
- ng-click="downloadSelectedProjects()"
- )
- i.fa.fa-cloud-download
- a.btn.btn-default(
- href='#',
- tooltip="#{translate('delete')}",
- tooltip-placement="bottom",
- tooltip-append-to-body="true",
- ng-click="openArchiveProjectsModal()"
- )
- i.fa.fa-trash-o
-
- .btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown)
- a.btn.btn-default.dropdown-toggle(
- href="#",
- data-toggle="dropdown",
- dropdown-toggle,
- tooltip="#{translate('add_to_folders')}",
- tooltip-append-to-body="true",
- tooltip-placement="bottom"
- )
- i.fa.fa-folder-open-o
- |
- span.caret
- ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu(
- role="menu"
- ng-controller="TagListController"
- )
- li.dropdown-header #{translate("add_to_folder")}
- li(
- ng-repeat="tag in tags | filter:nonEmpty | orderBy:'name'",
- ng-controller="TagDropdownItemController"
- )
- a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click")
- i.fa(
- ng-class="{\
- 'fa-check-square-o': areSelectedProjectsInTag == true,\
- 'fa-square-o': areSelectedProjectsInTag == false,\
- 'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\
- }"
- )
- | {{tag.name}}
- li.divider
- li
- a(href="#", ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")}
-
- .btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown
- a.btn.btn-default.dropdown-toggle(
- href='#',
- data-toggle="dropdown",
- dropdown-toggle
- ) #{translate("more")}
- span.caret
- ul.dropdown-menu.dropdown-menu-right(role="menu")
- li(ng-show="getFirstSelectedProject().accessLevel == 'owner'")
- a(
- href='#',
- ng-click="openRenameProjectModal()"
- ) #{translate("rename")}
- li
- a(
- href='#',
- ng-click="openCloneProjectModal()"
- ) #{translate("make_copy")}
-
- .btn-toolbar(ng-show="filter == 'archived'")
- .btn-group(ng-hide="selectedProjects.length < 1")
- a.btn.btn-default(
- href='#',
- data-original-title="Restore",
- data-toggle="tooltip",
- data-placement="bottom",
- ng-click="restoreSelectedProjects()"
- ) #{translate("restore")}
-
- .btn-group(ng-hide="selectedProjects.length < 1")
- a.btn.btn-danger(
- href='#',
- data-original-title="Delete Forever",
- data-toggle="tooltip",
- data-placement="bottom",
- ng-click="openDeleteProjectsModal()"
- ) #{translate("delete_forever")}
-
-.row.row-spaced
- .col-xs-12
- .card.card-thin.project-list-card
- ul.list-unstyled.project-list.structured-list(
- select-all-list,
- ng-if="projects.length > 0",
- max-height="projectListHeight - 25",
- ng-cloak
- )
- li.container-fluid
- .row
- .col-xs-6
- input.select-all(
- select-all,
- type="checkbox"
- )
- span.header.clickable(ng-click="changePredicate('name')") #{translate("title")}
- i.tablesort.fa(ng-class="getSortIconClass('name')")
- .col-xs-2
- span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")}
- i.tablesort.fa(ng-class="getSortIconClass('accessLevel')")
- .col-xs-4
- span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")}
- i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')")
- li.project_entry.container-fluid(
- ng-repeat="project in visibleProjects | orderBy:predicate:reverse",
- ng-controller="ProjectListItemController"
- )
- .row
- .col-xs-6
- input.select-item(
- select-individual,
- type="checkbox",
- ng-model="project.selected"
- )
- span
- a.projectName(href="/project/{{project.id}}") {{project.name}}
- span(
- ng-controller="TagListController"
- )
- a.label.label-default.tag-label(
- href,
- ng-repeat='tag in project.tags',
- ng-click="selectTag(tag)"
- ) {{tag.name}}
- .col-xs-2
- span.owner {{ownerName()}}
- .col-xs-4
- span.last-modified {{project.lastUpdated | formatDate}}
- li(
- ng-if="visibleProjects.length == 0",
- ng-cloak
- )
- .row
- .col-xs-12.text-centered
- small #{translate("no_projects")}
-
- div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak)
- h2 #{translate("welcome_to_sl")}
- p #{translate("new_to_latex_look_at")}
- a(href="/templates") #{translate("templates").toLowerCase()}
- | #{translate("or")}
- a(href="/learn") #{translate("latex_help_guide")}
-
-
- .row
- .col-md-offset-4.col-md-4
- .dropdown.minimal-create-proj-dropdown(dropdown)
- a.btn.btn-success.dropdown-toggle(
- href="#",
- data-toggle="dropdown",
- dropdown-toggle
- )
- | Create First Project
-
- ul.dropdown-menu.minimal-create-proj-dropdown-menu(role="menu", style="text-align:center;")
- li
- a(
- href,
- ng-click="openCreateProjectModal()"
- sixpack-convert="first_sign_up",
- ) #{translate("blank_project")}
- li
- a(
- href,
- sixpack-convert="first_sign_up",
- ng-click="openCreateProjectModal('example')"
- ) #{translate("example_project")}
- li
- a(
- href,
- sixpack-convert="first_sign_up",
- ng-click="openUploadProjectModal()"
- ) #{translate("upload_project")}
- != moduleIncludes("newProjectMenu", locals)
- if (templates)
- li.divider
- li.dropdown-header #{translate("templates")}
- each item in templates
- li
- a.menu-indent(href=item.url, sixpack-convert="first_sign_up") #{translate(item.name)}
-
-
From 8c7b9edd21876d3d332b825b8b3cd63e0043bc1f Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Tue, 30 Aug 2016 16:10:04 +0100
Subject: [PATCH 223/378] Remove AB-related logic from template and controller.
---
services/web/app/views/project/list.jade | 4 ++--
.../coffee/main/project-list/project-list.coffee | 14 +++++---------
2 files changed, 7 insertions(+), 11 deletions(-)
diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade
index 8204aed813..a6a3957720 100644
--- a/services/web/app/views/project/list.jade
+++ b/services/web/app/views/project/list.jade
@@ -23,7 +23,7 @@ block content
.container
.row(ng-cloak)
- span(ng-show="first_sign_up == 'default' || projects.length > 0")
+ span(ng-if="projects.length > 0")
aside.col-md-2.col-xs-3
include ./list/side-bar
@@ -31,7 +31,7 @@ block content
include ./list/notifications
include ./list/project-list
- span(ng-if="first_sign_up == 'minimial' && projects.length == 0")
+ span(ng-if="projects.length === 0")
.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8
include ./list/empty-project-list
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 a528c0b96f..313b52ba1f 100644
--- a/services/web/public/coffee/main/project-list/project-list.coffee
+++ b/services/web/public/coffee/main/project-list/project-list.coffee
@@ -2,7 +2,7 @@ define [
"base"
], (App) ->
- App.controller "ProjectPageController", ($scope, $modal, $q, $window, queuedHttp, event_tracking, $timeout, sixpack) ->
+ App.controller "ProjectPageController", ($scope, $modal, $q, $window, queuedHttp, event_tracking, $timeout) ->
$scope.projects = window.data.projects
$scope.tags = window.data.tags
$scope.notifications = window.data.notifications
@@ -12,14 +12,10 @@ define [
$scope.predicate = "lastUpdated"
$scope.reverse = true
- if $scope.projects.length > 0
- $scope.first_sign_up = "default"
- else
- sixpack.participate 'first_sign_up', ['default', 'minimial'], (chosenVariation, rawResponse)->
- $scope.first_sign_up = chosenVariation
- $timeout () ->
- recalculateProjectListHeight()
- , 10
+ if $scope.projects.length == 0
+ $timeout () ->
+ recalculateProjectListHeight()
+ , 10
recalculateProjectListHeight = () ->
topOffset = $(".project-list-card")?.offset()?.top
From c44f33ce99854d1577ed9490c82598b49f7c8713 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Tue, 30 Aug 2016 16:45:21 +0100
Subject: [PATCH 224/378] allow negative values for synctex positions
---
.../web/app/coffee/Features/Compile/CompileController.coffee | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee
index 00f4a880ff..688b746830 100755
--- a/services/web/app/coffee/Features/Compile/CompileController.coffee
+++ b/services/web/app/coffee/Features/Compile/CompileController.coffee
@@ -151,9 +151,9 @@ module.exports = CompileController =
{page, h, v} = req.query
if not page?.match(/^\d+$/)
return next(new Error("invalid page parameter"))
- if not h?.match(/^\d+\.\d+$/)
+ if not h?.match(/^-?\d+\.\d+$/)
return next(new Error("invalid h parameter"))
- if not v?.match(/^\d+\.\d+$/)
+ if not v?.match(/^-?\d+\.\d+$/)
return next(new Error("invalid v parameter"))
# whether this request is going to a per-user container
CompileController._compileAsUser req, (error, user_id) ->
From 09c188030ca050f2f022701bdb4249734e284029 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Tue, 30 Aug 2016 17:42:23 +0100
Subject: [PATCH 225/378] Hide sign up button via visibility, to keep the
layout fixed.
---
services/web/app/views/subscriptions/plans.jade | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/services/web/app/views/subscriptions/plans.jade b/services/web/app/views/subscriptions/plans.jade
index d451c94540..2ee8fbbd9c 100644
--- a/services/web/app/views/subscriptions/plans.jade
+++ b/services/web/app/views/subscriptions/plans.jade
@@ -72,7 +72,10 @@ block content
li
li
br
- a.btn.btn-info(href="/register") #{translate("sign_up_now")}
+ a.btn.btn-info(
+ href="/register"
+ style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
+ ) #{translate("sign_up_now")}
.col-md-4
.card.card-highlighted
.card-header
From 1399ee46898f5bb08fc62ff653ab6c1863c76430 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 31 Aug 2016 09:34:20 +0100
Subject: [PATCH 226/378] Fix reference to `@key` in log expression.
---
.../coffee/Features/Notifications/NotificationsBuilder.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
index ffb71f70e6..6a5ac91bd7 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
@@ -11,7 +11,7 @@ module.exports =
messageOpts =
groupName: licence.name
subscription_id: licence.subscription_id
- logger.log user_id:user._id, key:key, "creating notification key for user"
+ logger.log user_id:user._id, key: @key, "creating notification key for user"
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, callback
read: (callback = ->)->
From 960ed520b6af1c3540863ea272a596d9be8777c6 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Wed, 31 Aug 2016 09:40:40 +0100
Subject: [PATCH 227/378] fix notifications builder @key bug
---
.../coffee/Features/Notifications/NotificationsBuilder.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
index ffb71f70e6..941f4d4d4d 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
@@ -11,7 +11,7 @@ module.exports =
messageOpts =
groupName: licence.name
subscription_id: licence.subscription_id
- logger.log user_id:user._id, key:key, "creating notification key for user"
+ logger.log user_id:user._id, key:@key, "creating notification key for user"
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, callback
read: (callback = ->)->
From f59d5d836d7f52d60ec16274201559e70c466de1 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 31 Aug 2016 10:18:53 +0100
Subject: [PATCH 228/378] Check plan, return error if not valid.
This prevents a crash later when we refer to properties
of the plan object, which can end up being `null` when
the `planCode` is either missing, or not a valid code.
---
.../Features/Subscription/SubscriptionViewModelBuilder.coffee | 3 +++
1 file changed, 3 insertions(+)
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
index e239a779b9..fa3bda3385 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
@@ -16,6 +16,9 @@ module.exports =
if subscription?
return callback(error) if error?
plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
+ if !plan?
+ err = new Error('No plan found for planCode "#{subscription.planCode}"')
+ return callback(err)
RecurlyWrapper.getSubscription subscription.recurlySubscription_id, (err, recurlySubscription)->
tax = recurlySubscription?.tax_in_cents || 0
callback null, {
From 47b1a5099ad1d3a9572e6d936cf3479e69762044 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 31 Aug 2016 10:40:30 +0100
Subject: [PATCH 229/378] Better logging when plan is null.
---
.../Features/Subscription/SubscriptionViewModelBuilder.coffee | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
index fa3bda3385..44c31c8d60 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
@@ -4,6 +4,7 @@ PlansLocator = require("./PlansLocator")
SubscriptionFormatters = require("./SubscriptionFormatters")
LimitationsManager = require("./LimitationsManager")
SubscriptionLocator = require("./SubscriptionLocator")
+logger = require('logger-sharelatex')
_ = require("underscore")
module.exports =
@@ -17,7 +18,8 @@ module.exports =
return callback(error) if error?
plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
if !plan?
- err = new Error('No plan found for planCode "#{subscription.planCode}"')
+ err = new Error("No plan found for planCode '#{subscription.planCode}'")
+ logger.error {user_id: user._id, err}, "error getting subscription plan for user"
return callback(err)
RecurlyWrapper.getSubscription subscription.recurlySubscription_id, (err, recurlySubscription)->
tax = recurlySubscription?.tax_in_cents || 0
From bba39cc9025d1d3ace9ff6f49713d76fdf02cdd8 Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Wed, 31 Aug 2016 11:40:27 +0100
Subject: [PATCH 230/378] Also hide button, when viewing student plans.
---
services/web/app/views/subscriptions/plans.jade | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/services/web/app/views/subscriptions/plans.jade b/services/web/app/views/subscriptions/plans.jade
index 2ee8fbbd9c..9db21fa3a6 100644
--- a/services/web/app/views/subscriptions/plans.jade
+++ b/services/web/app/views/subscriptions/plans.jade
@@ -139,7 +139,10 @@ block content
li
li
br
- a.btn.btn-info(href="/register") #{translate("sign_up_now")}
+ a.btn.btn-info(
+ href="/register"
+ style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
+ ) #{translate("sign_up_now")}
.col-md-4
.card.card-highlighted
From f849bf6d4cfa84f97eb444b7429da440a8d6f185 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 31 Aug 2016 13:31:04 +0100
Subject: [PATCH 231/378] Add Autocomplete descriptions to hotkeys
---
.../web/app/views/project/editor/hotkeys.jade | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/services/web/app/views/project/editor/hotkeys.jade b/services/web/app/views/project/editor/hotkeys.jade
index c2c5e66759..cad838d683 100644
--- a/services/web/app/views/project/editor/hotkeys.jade
+++ b/services/web/app/views/project/editor/hotkeys.jade
@@ -66,6 +66,31 @@ script(type="text/ng-template", id="hotkeysModalTemplate")
.hotkey
span.combination {{ctrl}} + I
span.description Italic Text
+
+ h3 #{translate("autocomplete")}
+ .row
+ .col-xs-6
+ .hotkey
+ span.combination Ctrl + Space
+ span.description Autocomplete Menu
+
+ .col-xs-6
+ .hotkey
+ span.combination Tab / Up / Down
+ span.description Select Candidate
+
+ .hotkey
+ span.combination Enter
+ span.description Insert Candidate
+
+ h3 #{translate("autocomplete_references")}
+ .row
+ .col-xs-6
+ .hotkey
+ span.combination Ctrl + Space
+ span.description Search References
+
+
.modal-footer
button.btn.btn-default(
ng-click="cancel()"
From 1d835248533e4b959ad38396844e296d3c7b16fe Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 31 Aug 2016 13:54:51 +0100
Subject: [PATCH 232/378] Add a block to `autocomplete_references`.
Use unescaped interpolation.
---
services/web/app/views/project/editor/hotkeys.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/project/editor/hotkeys.jade b/services/web/app/views/project/editor/hotkeys.jade
index cad838d683..15153cdccf 100644
--- a/services/web/app/views/project/editor/hotkeys.jade
+++ b/services/web/app/views/project/editor/hotkeys.jade
@@ -83,7 +83,7 @@ script(type="text/ng-template", id="hotkeysModalTemplate")
span.combination Enter
span.description Insert Candidate
- h3 #{translate("autocomplete_references")}
+ h3 !{translate("autocomplete_references")}
.row
.col-xs-6
.hotkey
From b95a2c6d049de2c3f0f01ccbaa60269b5375d59d Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Wed, 31 Aug 2016 15:21:23 +0100
Subject: [PATCH 233/378] clean up compile check
use a valid user id, report all failures as errors, clear timeout on
success
---
services/web/app/coffee/router.coffee | 21 ++++++++++++++++-----
1 file changed, 16 insertions(+), 5 deletions(-)
diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee
index 5d07dc9e82..123c20581c 100644
--- a/services/web/app/coffee/router.coffee
+++ b/services/web/app/coffee/router.coffee
@@ -252,13 +252,24 @@ module.exports = class Router
apiRouter.get "/status/compiler/:Project_id", AuthorizationMiddlewear.ensureUserCanReadProject, (req, res) ->
sendRes = _.once (statusCode, message)->
- res.writeHead statusCode
- res.end message
- CompileManager.compile req.params.Project_id, "test-compile", {}, () ->
- sendRes 200, "Compiler returned in less than 10 seconds"
- setTimeout (() ->
+ res.status statusCode
+ res.send message
+ # set a timeout
+ handler = setTimeout (() ->
sendRes 500, "Compiler timed out"
+ handler = null
), 10000
+ # use a valid user id for testing
+ test_user_id = "123456789012345678901234"
+ # run the compile
+ CompileManager.compile req.params.Project_id, test_user_id, {}, (error, status) ->
+ clearTimeout handler if handler?
+ if error?
+ sendRes 500, "Compiler returned error #{error.message}"
+ else if status is "success"
+ sendRes 200, "Compiler returned in less than 10 seconds"
+ else
+ sendRes 500, "Compiler returned failure #{status}"
apiRouter.get "/ip", (req, res, next) ->
res.send({
From e82411ac792d125572cfa72c5466f77230ad0de3 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Wed, 31 Aug 2016 16:10:24 +0100
Subject: [PATCH 234/378] clear serverid on every compile check
---
.../web/app/coffee/Features/Compile/ClsiCookieManager.coffee | 4 ++++
services/web/app/coffee/router.coffee | 5 ++++-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee
index b6270cfce9..c237f212c0 100644
--- a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee
+++ b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee
@@ -48,6 +48,10 @@ module.exports = ClsiCookieManager =
multi.exec (err)->
callback(err, serverId)
+ clearServerId: (project_id, callback = (err)->)->
+ if !clsiCookiesEnabled
+ return callback()
+ rclient.del buildKey(project_id), callback
getCookieJar: (project_id, callback = (err, jar)->)->
if !clsiCookiesEnabled
diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee
index 123c20581c..e6de2e7183 100644
--- a/services/web/app/coffee/router.coffee
+++ b/services/web/app/coffee/router.coffee
@@ -22,6 +22,7 @@ UserPagesController = require('./Features/User/UserPagesController')
DocumentController = require('./Features/Documents/DocumentController')
CompileManager = require("./Features/Compile/CompileManager")
CompileController = require("./Features/Compile/CompileController")
+ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")
HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
FileStoreController = require("./Features/FileStore/FileStoreController")
@@ -251,9 +252,11 @@ module.exports = class Router
apiRouter.get '/health_check/redis', HealthCheckController.checkRedis
apiRouter.get "/status/compiler/:Project_id", AuthorizationMiddlewear.ensureUserCanReadProject, (req, res) ->
+ project_id = req.params.Project_id
sendRes = _.once (statusCode, message)->
res.status statusCode
res.send message
+ ClsiCookieManager.clearServerId project_id # force every compile to a new server
# set a timeout
handler = setTimeout (() ->
sendRes 500, "Compiler timed out"
@@ -262,7 +265,7 @@ module.exports = class Router
# use a valid user id for testing
test_user_id = "123456789012345678901234"
# run the compile
- CompileManager.compile req.params.Project_id, test_user_id, {}, (error, status) ->
+ CompileManager.compile project_id, test_user_id, {}, (error, status) ->
clearTimeout handler if handler?
if error?
sendRes 500, "Compiler returned error #{error.message}"
From 8002930270a5b685e2128850b79bdb0cec3b0ff4 Mon Sep 17 00:00:00 2001
From: Henry Oswald
Date: Thu, 1 Sep 2016 13:51:50 +0100
Subject: [PATCH 235/378] don't use mathjax for cdn
---
services/web/app/views/project/editor.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade
index 1e36e374f1..ee02c8e730 100644
--- a/services/web/app/views/project/editor.jade
+++ b/services/web/app/views/project/editor.jade
@@ -100,7 +100,7 @@ block content
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
window.requirejs = {
"paths" : {
- "mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {qs:{config:'TeX-AMS_HTML', fingerprint:false}})}",
+ "mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, fingerprint:false, qs:{config:'TeX-AMS_HTML'})}",
"moment": "libs/moment-2.7.0",
"libs/pdf": "libs/pdfjs-1.3.91/pdf"
},
From e4f432515099a15df00df4aeeca39cbc4482cdf7 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Fri, 2 Sep 2016 16:17:37 +0100
Subject: [PATCH 236/378] Basic passport integration
---
.../AuthenticationController.coffee | 48 +++++++++++++++++++
.../Features/Project/ProjectController.coffee | 8 ++--
.../Features/User/UserController.coffee | 1 +
.../app/coffee/infrastructure/Server.coffee | 20 ++++++++
services/web/app/coffee/router.coffee | 10 ++--
services/web/package.json | 2 +
6 files changed, 82 insertions(+), 7 deletions(-)
diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index 4972144737..a31d0eb45e 100644
--- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -16,6 +16,54 @@ module.exports = AuthenticationController =
login: (req, res, next = (error) ->) ->
AuthenticationController.doLogin req.body, req, res, next
+ serializeUser: (user, callback) ->
+ console.log ">> serialize", user._id
+ lightUser =
+ _id: user._id
+ first_name: user.first_name
+ last_name: user.last_name
+ isAdmin: user.isAdmin
+ email: user.email
+ referal_id: user.referal_id
+ session_created: (new Date()).toISOString()
+ ip_address: user._login_req_ip
+ callback(null, lightUser)
+
+ deserializeUser: (user, cb) ->
+ console.log ">> de-serialize", user._id
+ cb(null, user)
+
+ doPassportLogin: (req, username, password, done) ->
+ email = username.toLowerCase()
+ redir = Url.parse(req?.body?.redir or "/project").path
+ console.log ">> doing passport login", username, password, redir
+ LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
+ return done(err) if err?
+ if !isAllowed
+ logger.log email:email, "too many login requests"
+ return done(null, null, {message: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'})
+ AuthenticationManager.authenticate email: email, password, (error, user) ->
+ return done(error) if error?
+ if user?
+ # async actions
+ UserHandler.setupLoginData(user, ()->)
+ LoginRateLimiter.recordSuccessfulLogin(email)
+ AuthenticationController._recordSuccessfulLogin(user._id)
+ Analytics.recordEvent(user._id, "user-logged-in")
+ UserSessionsManager.trackSession(user, req.sessionID, () ->)
+ req.session.justLoggedIn = true
+ logger.log email: email, user_id: user._id.toString(), "successful log in"
+ # capture the request ip for use when creating the session
+ user._login_req_ip = req.ip
+ req._redir = redir
+ console.log ">> done, returning user"
+ return done(null, user)
+ else
+ AuthenticationController._recordFailedLogin()
+ logger.log email: email, "failed log in"
+ return done(null, false, {message: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'})
+
+
doLogin: (options, req, res, next) ->
email = options.email?.toLowerCase()
password = options.password
diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee
index d273516056..d28b5ca868 100644
--- a/services/web/app/coffee/Features/Project/ProjectController.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectController.coffee
@@ -45,10 +45,10 @@ module.exports = ProjectController =
async.series jobs, (error) ->
return next(error) if error?
res.sendStatus(204)
-
+
updateProjectAdminSettings: (req, res, next) ->
project_id = req.params.Project_id
-
+
jobs = []
if req.body.publicAccessLevel?
jobs.push (callback) ->
@@ -149,7 +149,7 @@ module.exports = ProjectController =
return next(err)
logger.log results:results, user_id:user_id, "rendering project list"
tags = results.tags[0]
- notifications = require("underscore").map results.notifications, (notification)->
+ notifications = require("underscore").map results.notifications, (notification)->
notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
return notification
projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2]
@@ -332,4 +332,4 @@ do generateThemeList = () ->
for file in files
if file.slice(-2) == "js" and file.match(/^theme-/)
cleanName = file.slice(0,-3).slice(6)
- THEME_LIST.push cleanName
\ No newline at end of file
+ THEME_LIST.push cleanName
diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee
index 546cea035e..25eb2afd4c 100644
--- a/services/web/app/coffee/Features/User/UserController.coffee
+++ b/services/web/app/coffee/Features/User/UserController.coffee
@@ -84,6 +84,7 @@ module.exports = UserController =
logger.log user: req?.session?.user, "logging out"
sessionId = req.sessionID
user = req?.session?.user
+ req.logout?() # passport logout
req.session.destroy (err)->
if err
logger.err err: err, 'error destorying session'
diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee
index 8ac543c698..49f272a821 100644
--- a/services/web/app/coffee/infrastructure/Server.coffee
+++ b/services/web/app/coffee/infrastructure/Server.coffee
@@ -21,6 +21,9 @@ cookieParser = require('cookie-parser')
sessionStore = new RedisStore(client:rclient)
+passport = require('passport')
+LocalStrategy = require('passport-local').Strategy
+
Mongoose = require("./Mongoose")
oneDayInMilliseconds = 86400000
@@ -32,6 +35,7 @@ Modules = require "./Modules"
ErrorController = require "../Features/Errors/ErrorController"
UserSessionsManager = require "../Features/User/UserSessionsManager"
+AuthenticationController = require "../Features/Authentication/AuthenticationController"
metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongojs/node_modules/mongodb"), logger)
metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongoose/node_modules/mongodb"), logger)
@@ -87,6 +91,22 @@ webRouter.use csrfProtection
webRouter.use translations.expressMiddlewear
webRouter.use translations.setLangBasedOnDomainMiddlewear
+# passport
+webRouter.use passport.initialize()
+webRouter.use passport.session()
+
+passport.use(new LocalStrategy(
+ {
+ passReqToCallback: true,
+ usernameField: 'email',
+ passwordField: 'password'
+ },
+ AuthenticationController.doPassportLogin
+))
+passport.serializeUser(AuthenticationController.serializeUser)
+passport.deserializeUser(AuthenticationController.deserializeUser)
+
+
# Measure expiry from last request, not last login
webRouter.use (req, res, next) ->
req.session.touch()
diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee
index e6de2e7183..adf45bc36a 100644
--- a/services/web/app/coffee/router.coffee
+++ b/services/web/app/coffee/router.coffee
@@ -43,6 +43,7 @@ AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
logger = require("logger-sharelatex")
_ = require("underscore")
+passport = require('passport')
module.exports = class Router
constructor: (webRouter, apiRouter)->
@@ -53,7 +54,10 @@ module.exports = class Router
webRouter.get '/login', UserPagesController.loginPage
AuthenticationController.addEndpointToLoginWhitelist '/login'
- webRouter.post '/login', AuthenticationController.login
+ # webRouter.post '/login', AuthenticationController.login
+ webRouter.post '/login', passport.authenticate('local'), AuthenticationController.login, (req, res) ->
+ console.log ">> login done", req._redir
+ res.json {redir: req._redir}
webRouter.get '/logout', UserController.logout
webRouter.get '/restricted', AuthorizationMiddlewear.restricted
@@ -71,7 +75,7 @@ module.exports = class Router
RealTimeProxyRouter.apply(webRouter, apiRouter)
ContactRouter.apply(webRouter, apiRouter)
AnalyticsRouter.apply(webRouter, apiRouter)
-
+
Modules.applyRouter(webRouter, apiRouter)
@@ -182,7 +186,7 @@ module.exports = class Router
webRouter.delete '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), TagsController.removeProjectFromTag
webRouter.get '/notifications', AuthenticationController.requireLogin(), NotificationsController.getAllUnreadNotifications
- webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead
+ webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead
# Deprecated in favour of /internal/project/:project_id but still used by versioning
apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails
diff --git a/services/web/package.json b/services/web/package.json
index 83381005f0..449039dcd2 100644
--- a/services/web/package.json
+++ b/services/web/package.json
@@ -46,6 +46,8 @@
"nodemailer-sendgrid-transport": "^0.2.0",
"nodemailer-ses-transport": "^1.3.0",
"optimist": "0.6.1",
+ "passport": "^0.3.2",
+ "passport-local": "^1.0.0",
"pg": "^6.0.3",
"pg-hstore": "^2.3.2",
"redback": "0.4.0",
From ab2fe1de97a0b1a84de22c767e333a70b68ac9cd Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Fri, 2 Sep 2016 16:20:16 +0100
Subject: [PATCH 237/378] fix unclosed brace in template
---
services/web/app/views/project/editor.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade
index ee02c8e730..8a3c1aae27 100644
--- a/services/web/app/views/project/editor.jade
+++ b/services/web/app/views/project/editor.jade
@@ -100,7 +100,7 @@ block content
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
window.requirejs = {
"paths" : {
- "mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, fingerprint:false, qs:{config:'TeX-AMS_HTML'})}",
+ "mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, fingerprint:false, qs:{config:'TeX-AMS_HTML'}})}",
"moment": "libs/moment-2.7.0",
"libs/pdf": "libs/pdfjs-1.3.91/pdf"
},
From e6c7aa25ecf7ba5cbde203a4593f13997ea8999d Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 5 Sep 2016 10:28:47 +0100
Subject: [PATCH 238/378] barely functional login and logout
---
.../Authentication/AuthenticationController.coffee | 5 +++++
services/web/app/coffee/infrastructure/Server.coffee | 8 ++++++++
services/web/app/coffee/router.coffee | 2 +-
3 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index a31d0eb45e..4e358cc2cd 100644
--- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -13,6 +13,7 @@ UserSessionsManager = require("../User/UserSessionsManager")
Analytics = require "../Analytics/AnalyticsManager"
module.exports = AuthenticationController =
+
login: (req, res, next = (error) ->) ->
AuthenticationController.doLogin req.body, req, res, next
@@ -34,6 +35,7 @@ module.exports = AuthenticationController =
cb(null, user)
doPassportLogin: (req, username, password, done) ->
+ console.log(">>", username)
email = username.toLowerCase()
redir = Url.parse(req?.body?.redir or "/project").path
console.log ">> doing passport login", username, password, redir
@@ -65,6 +67,7 @@ module.exports = AuthenticationController =
doLogin: (options, req, res, next) ->
+ dienow
email = options.email?.toLowerCase()
password = options.password
redir = Url.parse(options.redir or "/project").path
@@ -111,6 +114,7 @@ module.exports = AuthenticationController =
requireLogin: () ->
doRequest = (req, res, next = (error) ->) ->
+ console.log ">>>>", req.currentUser()
if !req.session.user?
AuthenticationController._redirectToLoginOrRegisterPage(req, res)
else
@@ -176,6 +180,7 @@ module.exports = AuthenticationController =
callback()
establishUserSession: (req, user, callback = (error) ->) ->
+ dienow
lightUser =
_id: user._id
first_name: user.first_name
diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee
index 49f272a821..095c98be2b 100644
--- a/services/web/app/coffee/infrastructure/Server.coffee
+++ b/services/web/app/coffee/infrastructure/Server.coffee
@@ -106,6 +106,14 @@ passport.use(new LocalStrategy(
passport.serializeUser(AuthenticationController.serializeUser)
passport.deserializeUser(AuthenticationController.deserializeUser)
+# standard access to the current user
+currentUserMiddleware = (req, res, next) ->
+ req.currentUser = () ->
+ req.user
+ next()
+
+webRouter.use(currentUserMiddleware)
+apiRouter.use(currentUserMiddleware)
# Measure expiry from last request, not last login
webRouter.use (req, res, next) ->
diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee
index adf45bc36a..86a97b0a40 100644
--- a/services/web/app/coffee/router.coffee
+++ b/services/web/app/coffee/router.coffee
@@ -55,7 +55,7 @@ module.exports = class Router
AuthenticationController.addEndpointToLoginWhitelist '/login'
# webRouter.post '/login', AuthenticationController.login
- webRouter.post '/login', passport.authenticate('local'), AuthenticationController.login, (req, res) ->
+ webRouter.post '/login', passport.authenticate('local'), (req, res) ->
console.log ">> login done", req._redir
res.json {redir: req._redir}
webRouter.get '/logout', UserController.logout
From ab2c1e82fb22866e53728c2ebe4c1ffb2bf8e986 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 5 Sep 2016 15:58:31 +0100
Subject: [PATCH 239/378] WIP: refactor
---
.../Analytics/AnalyticsController.coffee | 4 +-
.../AuthenticationController.coffee | 135 ++++++++++--------
.../AuthorizationMiddlewear.coffee | 27 ++--
.../Features/Chat/ChatController.coffee | 8 +-
.../CollaboratorsInviteController.coffee | 127 ++++++++--------
.../Features/Compile/CompileController.coffee | 62 ++++----
.../Contacts/ContactController.coffee | 47 +++---
.../NotificationsController.coffee | 8 +-
.../Features/Project/ProjectController.coffee | 117 +++++++--------
.../Features/Referal/ReferalController.coffee | 6 +-
.../Features/Referal/ReferalMiddleware.coffee | 7 +-
.../Security/RateLimiterMiddlewear.coffee | 14 +-
.../Spelling/SpellingController.coffee | 4 +-
.../StaticPages/HomeController.coffee | 5 +-
.../SubscriptionController.coffee | 4 +-
.../SubscriptionGroupController.coffee | 40 +++---
.../Features/Tags/TagsController.coffee | 23 +--
.../TrackChangesController.coffee | 27 ++--
.../Uploads/ProjectUploadController.coffee | 10 +-
.../Features/User/UserController.coffee | 31 ++--
.../Features/User/UserPagesController.coffee | 10 +-
.../infrastructure/ExpressLocals.coffee | 31 ++--
22 files changed, 390 insertions(+), 357 deletions(-)
diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee
index a49a419e07..4d5239ac45 100644
--- a/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee
+++ b/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee
@@ -1,7 +1,7 @@
-AnalyticsManager = require "./AnalyticsManager"
+AnalyticsManager = equire "./AnalyticsManager"
module.exports = AnalyticsController =
recordEvent: (req, res, next) ->
AnalyticsManager.recordEvent req.session?.user?._id, req.params.event, req.body, (error) ->
return next(error) if error?
- res.send 204
\ No newline at end of file
+ res.send 204
diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index 4e358cc2cd..572ebe5b3f 100644
--- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -14,8 +14,8 @@ Analytics = require "../Analytics/AnalyticsManager"
module.exports = AuthenticationController =
- login: (req, res, next = (error) ->) ->
- AuthenticationController.doLogin req.body, req, res, next
+ # login: (req, res, next = (error) ->) ->
+ # AuthenticationController.doLogin req.body, req, res, next
serializeUser: (user, callback) ->
console.log ">> serialize", user._id
@@ -65,60 +65,38 @@ module.exports = AuthenticationController =
logger.log email: email, "failed log in"
return done(null, false, {message: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'})
+ isUserLoggedIn: (req) ->
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ return user_id?
- doLogin: (options, req, res, next) ->
- dienow
- email = options.email?.toLowerCase()
- password = options.password
- redir = Url.parse(options.redir or "/project").path
- LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
- if !isAllowed
- logger.log email:email, "too many login requests"
- res.statusCode = 429
- return res.send
- message:
- text: req.i18n.translate("to_many_login_requests_2_mins"),
- type: 'error'
- AuthenticationManager.authenticate email: email, password, (error, user) ->
- return next(error) if error?
- if user?
- UserHandler.setupLoginData user, ->
- LoginRateLimiter.recordSuccessfulLogin email
- AuthenticationController._recordSuccessfulLogin user._id
- AuthenticationController.establishUserSession req, user, (error) ->
- return next(error) if error?
- req.session.justLoggedIn = true
- logger.log email: email, user_id: user._id.toString(), "successful log in"
- Analytics.recordEvent user._id, "user-logged-in"
- res.json redir: redir
- else
- AuthenticationController._recordFailedLogin()
- logger.log email: email, "failed log in"
- res.json message:
- text: req.i18n.translate("email_or_password_wrong_try_again"),
- type: 'error'
-
- getLoggedInUserId: (req, callback = (error, user_id) ->) ->
+ # TODO: perhaps should produce an error if the current user is not present
+ getLoggedInUserId: (req) ->
+ # old sessions
if req?.session?.user?._id?
- callback null, req.session.user._id.toString()
+ return req.session.user._id.toString()
+ # new passport sessions
+ else if req?.session?.passport?.user?._id?
+ return req.session.passport.user._id.toString()
+ # neither
else
- callback null, null
+ return null
+ # TODO: perhaps should produce an error if the current user is not present
getLoggedInUser: (req, callback = (error, user) ->) ->
if req.session?.user?._id?
query = req.session.user._id
else
return callback null, null
- UserGetter.getUser query, callback
+ # omit sensitive information
+ UserGetter.getUser query, {hashedPassword: false, refProviders: false}, callback
requireLogin: () ->
doRequest = (req, res, next = (error) ->) ->
console.log ">>>>", req.currentUser()
- if !req.session.user?
+ if !AuthenticationController.isUserLoggedIn()?
AuthenticationController._redirectToLoginOrRegisterPage(req, res)
else
- req.user = req.session.user
return next()
return doRequest
@@ -133,7 +111,7 @@ module.exports = AuthenticationController =
if req.headers['authorization']?
return AuthenticationController.httpAuth(req, res, next)
- else if req.session.user?
+ else if AuthenticationController.isUserLoggedIn()?
return next()
else
logger.log url:req.url, "user trying to access endpoint not in global whitelist"
@@ -179,26 +157,59 @@ module.exports = AuthenticationController =
Metrics.inc "user.login.failed"
callback()
- establishUserSession: (req, user, callback = (error) ->) ->
- dienow
- lightUser =
- _id: user._id
- first_name: user.first_name
- last_name: user.last_name
- isAdmin: user.isAdmin
- email: user.email
- referal_id: user.referal_id
- session_created: (new Date()).toISOString()
- ip_address: req.ip
- # Regenerate the session to get a new sessionID (cookie value) to
- # protect against session fixation attacks
- oldSession = req.session
- req.session.destroy()
- req.sessionStore.generate(req)
- for key, value of oldSession
- req.session[key] = value
+ # establishUserSession: (req, user, callback = (error) ->) ->
+ # dienow
+ # lightUser =
+ # _id: user._id
+ # first_name: user.first_name
+ # last_name: user.last_name
+ # isAdmin: user.isAdmin
+ # email: user.email
+ # referal_id: user.referal_id
+ # session_created: (new Date()).toISOString()
+ # ip_address: req.ip
+ # # Regenerate the session to get a new sessionID (cookie value) to
+ # # protect against session fixation attacks
+ # oldSession = req.session
+ # req.session.destroy()
+ # req.sessionStore.generate(req)
+ # for key, value of oldSession
+ # req.session[key] = value
- req.session.user = lightUser
+ # req.session.user = lightUser
- UserSessionsManager.trackSession(user, req.sessionID, () ->)
- callback()
+ # UserSessionsManager.trackSession(user, req.sessionID, () ->)
+ # callback()
+
+
+ # doLogin: (options, req, res, next) ->
+ # dienow
+ # email = options.email?.toLowerCase()
+ # password = options.password
+ # redir = Url.parse(options.redir or "/project").path
+ # LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
+ # if !isAllowed
+ # logger.log email:email, "too many login requests"
+ # res.statusCode = 429
+ # return res.send
+ # message:
+ # text: req.i18n.translate("to_many_login_requests_2_mins"),
+ # type: 'error'
+ # AuthenticationManager.authenticate email: email, password, (error, user) ->
+ # return next(error) if error?
+ # if user?
+ # UserHandler.setupLoginData user, ->
+ # LoginRateLimiter.recordSuccessfulLogin email
+ # AuthenticationController._recordSuccessfulLogin user._id
+ # AuthenticationController.establishUserSession req, user, (error) ->
+ # return next(error) if error?
+ # req.session.justLoggedIn = true
+ # logger.log email: email, user_id: user._id.toString(), "successful log in"
+ # Analytics.recordEvent user._id, "user-logged-in"
+ # res.json redir: redir
+ # else
+ # AuthenticationController._recordFailedLogin()
+ # logger.log email: email, "failed log in"
+ # res.json message:
+ # text: req.i18n.translate("email_or_password_wrong_try_again"),
+ # type: 'error'
diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee
index 4888db0c8a..f321b015f6 100644
--- a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee
+++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee
@@ -3,6 +3,7 @@ async = require "async"
logger = require "logger-sharelatex"
ObjectId = require("mongojs").ObjectId
Errors = require "../Errors/Errors"
+AuthenticationController = require "../Authentication/AuthenticationController"
module.exports = AuthorizationMiddlewear =
ensureUserCanReadMultipleProjects: (req, res, next) ->
@@ -20,7 +21,7 @@ module.exports = AuthorizationMiddlewear =
AuthorizationMiddlewear.redirectToRestricted req, res, next
else
next()
-
+
ensureUserCanReadProject: (req, res, next) ->
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
@@ -32,7 +33,7 @@ module.exports = AuthorizationMiddlewear =
else
logger.log {user_id, project_id}, "denying user read access to project"
AuthorizationMiddlewear.redirectToRestricted req, res, next
-
+
ensureUserCanWriteProjectSettings: (req, res, next) ->
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
@@ -44,7 +45,7 @@ module.exports = AuthorizationMiddlewear =
else
logger.log {user_id, project_id}, "denying user write access to project settings"
AuthorizationMiddlewear.redirectToRestricted req, res, next
-
+
ensureUserCanWriteProjectContent: (req, res, next) ->
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
@@ -56,7 +57,7 @@ module.exports = AuthorizationMiddlewear =
else
logger.log {user_id, project_id}, "denying user write access to project settings"
AuthorizationMiddlewear.redirectToRestricted req, res, next
-
+
ensureUserCanAdminProject: (req, res, next) ->
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
@@ -68,7 +69,7 @@ module.exports = AuthorizationMiddlewear =
else
logger.log {user_id, project_id}, "denying user admin access to project"
AuthorizationMiddlewear.redirectToRestricted req, res, next
-
+
ensureUserIsSiteAdmin: (req, res, next) ->
AuthorizationMiddlewear._getUserId req, (error, user_id) ->
return next(error) if error?
@@ -90,22 +91,18 @@ module.exports = AuthorizationMiddlewear =
AuthorizationMiddlewear._getUserId req, (error, user_id) ->
return callback(error) if error?
callback(null, user_id, project_id)
-
+
_getUserId: (req, callback = (error, user_id) ->) ->
- if req.session?.user?._id?
- user_id = req.session.user._id
- else
- user_id = null
- callback null, user_id
-
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ return callback(null, user_id)
+
redirectToRestricted: (req, res, next) ->
res.redirect "/restricted"
-
+
restricted : (req, res, next)->
- if req.session.user?
+ if AuthenticationController.isUserLoggedIn()?
res.render 'user/restricted',
title:'restricted'
else
logger.log "user not logged in and trying to access #{req.url}, being redirected to login"
res.redirect '/register'
-
\ 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 ef3cbf94fc..35c280712a 100644
--- a/services/web/app/coffee/Features/Chat/ChatController.coffee
+++ b/services/web/app/coffee/Features/Chat/ChatController.coffee
@@ -1,14 +1,18 @@
ChatHandler = require("./ChatHandler")
EditorRealTimeController = require("../Editor/EditorRealTimeController")
logger = require("logger-sharelatex")
+AuthenticationController = require('../Authentication/AuthenticationController')
module.exports =
- sendMessage: (req, res)->
+ sendMessage: (req, res, next)->
project_id = req.params.Project_id
- user_id = req.session.user._id
messageContent = 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"
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 4f8daf4bdb..cfc6c4b08d 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -8,6 +8,7 @@ EmailHelper = require "../Helpers/EmailHelper"
EditorRealTimeController = require("../Editor/EditorRealTimeController")
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
AnalyticsManger = require("../Analytics/AnalyticsManager")
+AuthenticationController = require("../Authentication/AuthenticationController")
module.exports = CollaboratorsInviteController =
@@ -23,26 +24,27 @@ module.exports = CollaboratorsInviteController =
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
email = req.body.email
- sendingUser = req.session.user
- sendingUserId = sendingUser._id
- logger.log {projectId, email, sendingUserId}, "inviting to project"
- LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
- return next(error) if error?
- if !allowed
- logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
- return res.json {invite: null}
- {email, privileges} = req.body
- email = EmailHelper.parseEmail(email)
- if !email? or email == ""
- logger.log {projectId, email, sendingUserId}, "invalid email address"
- return res.sendStatus(400)
- 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}
+ AuthenticationController.getLoggedInUser req, (err, sendingUser) ->
+ return callback(err) if err?
+ sendingUserId = sendingUser._id
+ logger.log {projectId, email, sendingUserId}, "inviting to project"
+ LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
+ return next(error) if error?
+ if !allowed
+ logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
+ return res.json {invite: null}
+ {email, privileges} = req.body
+ email = EmailHelper.parseEmail(email)
+ if !email? or email == ""
+ logger.log {projectId, email, sendingUserId}, "invalid email address"
+ return res.sendStatus(400)
+ 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
@@ -58,67 +60,70 @@ module.exports = CollaboratorsInviteController =
resendInvite: (req, res, next) ->
projectId = req.params.Project_id
inviteId = req.params.invite_id
- sendingUser = req.session.user
logger.log {projectId, inviteId}, "resending invite"
- CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) ->
- if err?
- logger.err {projectId, inviteId}, "error resending invite"
- return next(err)
- res.sendStatus(201)
+ AuthenticationController.getLoggedInUser req, (err, sendingUser) ->
+ return callback(err) if err?
+ CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) ->
+ if err?
+ logger.err {projectId, inviteId}, "error resending invite"
+ return next(err)
+ res.sendStatus(201)
viewInvite: (req, res, next) ->
projectId = req.params.Project_id
token = req.params.token
- currentUser = req.session.user
_renderInvalidPage = () ->
logger.log {projectId, token}, "invite not valid, rendering not-valid page"
res.render "project/invite/not-valid", {title: "Invalid Invite"}
# check if the user is already a member of the project
- CollaboratorsHandler.isUserMemberOfProject currentUser._id, projectId, (err, isMember, _privilegeLevel) ->
- if err?
- logger.err {err, projectId}, "error checking if user is member of project"
- return next(err)
- if isMember
- logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting"
- return res.redirect "/project/#{projectId}"
- # get the invite
- CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) ->
+ AuthenticationController.getLoggedInUser req, (err, currentUser) ->
+ return callback(err) if err?
+ CollaboratorsHandler.isUserMemberOfProject currentUser._id, projectId, (err, isMember, _privilegeLevel) ->
if err?
- logger.err {projectId, token}, "error getting invite by token"
+ logger.err {err, projectId}, "error checking if user is member of project"
return next(err)
- # check if invite is gone, or otherwise non-existent
- if !invite?
- logger.log {projectId, token}, "no invite found for this token"
- return _renderInvalidPage()
- # check the user who sent the invite exists
- UserGetter.getUser {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
+ if isMember
+ logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting"
+ return res.redirect "/project/#{projectId}"
+ # get the invite
+ CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) ->
if err?
- logger.err {err, projectId}, "error getting project owner"
+ logger.err {projectId, token}, "error getting invite by token"
return next(err)
- if !owner?
- logger.log {projectId}, "no project owner found"
+ # check if invite is gone, or otherwise non-existent
+ if !invite?
+ logger.log {projectId, token}, "no invite found for this token"
return _renderInvalidPage()
- # fetch the project name
- ProjectGetter.getProject projectId, {}, (err, project) ->
+ # check the user who sent the invite exists
+ UserGetter.getUser {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
if err?
- logger.err {err, projectId}, "error getting project"
+ logger.err {err, projectId}, "error getting project owner"
return next(err)
- if !project?
- logger.log {projectId}, "no project found"
+ if !owner?
+ logger.log {projectId}, "no project owner found"
return _renderInvalidPage()
- # finally render the invite
- res.render "project/invite/show", {invite, project, owner, title: "Project Invite"}
+ # fetch the project name
+ ProjectGetter.getProject projectId, {}, (err, project) ->
+ if err?
+ logger.err {err, projectId}, "error getting project"
+ return next(err)
+ if !project?
+ logger.log {projectId}, "no project found"
+ return _renderInvalidPage()
+ # finally render the invite
+ res.render "project/invite/show", {invite, project, owner, title: "Project Invite"}
acceptInvite: (req, res, next) ->
projectId = req.params.Project_id
inviteId = req.params.invite_id
{token} = req.body
- currentUser = req.session.user
logger.log {projectId, inviteId, userId: currentUser._id}, "accepting invite"
- CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) ->
- if err?
- logger.err {projectId, inviteId}, "error accepting invite by token"
- return next(err)
- EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
- AnalyticsManger.recordEvent(currentUser._id, "project-invite-accept", {inviteId:inviteId, projectId:projectId})
- res.redirect "/project/#{projectId}"
+ AuthenticationController.getLoggedInUser req, (err, currentUser) ->
+ return callback(err) if err?
+ CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) ->
+ if err?
+ logger.err {projectId, inviteId}, "error accepting invite by token"
+ return next(err)
+ EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
+ AnalyticsManger.recordEvent(currentUser._id, "project-invite-accept", {inviteId:inviteId, projectId:projectId})
+ res.redirect "/project/#{projectId}"
diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee
index 688b746830..4a23431574 100755
--- a/services/web/app/coffee/Features/Compile/CompileController.coffee
+++ b/services/web/app/coffee/Features/Compile/CompileController.coffee
@@ -16,53 +16,53 @@ module.exports = CompileController =
res.setTimeout(5 * 60 * 1000)
project_id = req.params.Project_id
isAutoCompile = !!req.query?.auto_compile
- AuthenticationController.getLoggedInUserId req, (error, user_id) ->
+ user_id = AuthenticationController.getLoggedInUserId req
+ options = {
+ isAutoCompile: isAutoCompile
+ }
+ if req.body?.rootDoc_id?
+ options.rootDoc_id = req.body.rootDoc_id
+ else if req.body?.settingsOverride?.rootDoc_id? # Can be removed after deploy
+ options.rootDoc_id = req.body.settingsOverride.rootDoc_id
+ if req.body?.compiler
+ options.compiler = req.body.compiler
+ if req.body?.draft
+ options.draft = req.body.draft
+ if req.body?.check in ['validate', 'error', 'silent']
+ options.check = req.body.check
+ logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
+ CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
return next(error) if error?
- options = {
- isAutoCompile: isAutoCompile
+ res.contentType("application/json")
+ res.status(200).send JSON.stringify {
+ status: status
+ outputFiles: outputFiles
+ compileGroup: limits?.compileGroup
+ clsiServerId:clsiServerId
+ validationProblems:validationProblems
}
- if req.body?.rootDoc_id?
- options.rootDoc_id = req.body.rootDoc_id
- else if req.body?.settingsOverride?.rootDoc_id? # Can be removed after deploy
- options.rootDoc_id = req.body.settingsOverride.rootDoc_id
- if req.body?.compiler
- options.compiler = req.body.compiler
- if req.body?.draft
- options.draft = req.body.draft
- if req.body?.check in ['validate', 'error', 'silent']
- options.check = req.body.check
- logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
- CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
- return next(error) if error?
- res.contentType("application/json")
- res.status(200).send JSON.stringify {
- status: status
- outputFiles: outputFiles
- compileGroup: limits?.compileGroup
- clsiServerId:clsiServerId
- validationProblems:validationProblems
- }
stopCompile: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
- AuthenticationController.getLoggedInUserId req, (error, user_id) ->
+ user_id = AuthenticationController.getLoggedInUserId req
+ logger.log {project_id:project_id, user_id:user_id}, "stop compile request"
+ CompileManager.stopCompile project_id, user_id, (error) ->
return next(error) if error?
- logger.log {project_id:project_id, user_id:user_id}, "stop compile request"
- CompileManager.stopCompile project_id, user_id, (error) ->
- return next(error) if error?
- res.status(200).send()
+ res.status(200).send()
_compileAsUser: (req, callback) ->
# callback with user_id if per-user, undefined otherwise
if not Settings.disablePerUserCompiles
- AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
+ user_id = AuthenticationController.getLoggedInUserId req
+ return callback(null, user_id)
else
callback() # do a per-project compile, not per-user
_downloadAsUser: (req, callback) ->
# callback with user_id if per-user, undefined otherwise
if not Settings.disablePerUserCompiles
- AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
+ user_id = AuthenticationController.getLoggedInUserId req
+ return callback(null, user_id)
else
callback() # do a per-project compile, not per-user
diff --git a/services/web/app/coffee/Features/Contacts/ContactController.coffee b/services/web/app/coffee/Features/Contacts/ContactController.coffee
index a62c22bbf2..b3c2f7d02b 100644
--- a/services/web/app/coffee/Features/Contacts/ContactController.coffee
+++ b/services/web/app/coffee/Features/Contacts/ContactController.coffee
@@ -6,33 +6,32 @@ Modules = require "../../infrastructure/Modules"
module.exports = ContactsController =
getContacts: (req, res, next) ->
- AuthenticationController.getLoggedInUserId req, (error, user_id) ->
+ user_id = AuthenticationController.getLoggedInUserId req
+ ContactManager.getContactIds user_id, {limit: 50}, (error, contact_ids) ->
return next(error) if error?
- ContactManager.getContactIds user_id, {limit: 50}, (error, contact_ids) ->
+ UserGetter.getUsers contact_ids, {
+ email: 1, first_name: 1, last_name: 1, holdingAccount: 1
+ }, (error, contacts) ->
return next(error) if error?
- UserGetter.getUsers contact_ids, {
- email: 1, first_name: 1, last_name: 1, holdingAccount: 1
- }, (error, contacts) ->
- return next(error) if error?
-
- # UserGetter.getUsers may not preserve order so put them back in order
- positions = {}
- for contact_id, i in contact_ids
- positions[contact_id] = i
- contacts.sort (a,b) -> positions[a._id?.toString()] - positions[b._id?.toString()]
- # Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
- contacts = contacts.filter (c) -> !c.holdingAccount
-
- contacts = contacts.map(ContactsController._formatContact)
-
- Modules.hooks.fire "getContacts", user_id, contacts, (error, additional_contacts) ->
- return next(error) if error?
- contacts = contacts.concat(additional_contacts...)
- res.send({
- contacts: contacts
- })
-
+ # UserGetter.getUsers may not preserve order so put them back in order
+ positions = {}
+ for contact_id, i in contact_ids
+ positions[contact_id] = i
+ contacts.sort (a,b) -> positions[a._id?.toString()] - positions[b._id?.toString()]
+
+ # Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
+ contacts = contacts.filter (c) -> !c.holdingAccount
+
+ contacts = contacts.map(ContactsController._formatContact)
+
+ Modules.hooks.fire "getContacts", user_id, contacts, (error, additional_contacts) ->
+ return next(error) if error?
+ contacts = contacts.concat(additional_contacts...)
+ res.send({
+ contacts: contacts
+ })
+
_formatContact: (contact) ->
return {
id: contact._id?.toString()
diff --git a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee
index c4931f5327..5b83a60248 100644
--- a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee
+++ b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee
@@ -1,18 +1,20 @@
NotificationsHandler = require("./NotificationsHandler")
+AuthenticationController = require("../Authentication/AuthenticationController")
logger = require("logger-sharelatex")
_ = require("underscore")
module.exports =
getAllUnreadNotifications: (req, res)->
- NotificationsHandler.getUserNotifications req.session.user._id, (err, unreadNotifications)->
- unreadNotifications = _.map unreadNotifications, (notification)->
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ NotificationsHandler.getUserNotifications user_id, (err, unreadNotifications)->
+ unreadNotifications = _.map unreadNotifications, (notification)->
notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
return notification
res.send(unreadNotifications)
markNotificationAsRead: (req, res)->
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
notification_id = req.params.notification_id
NotificationsHandler.markAsRead user_id, notification_id, ->
res.send()
diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee
index d28b5ca868..9c016b7b2e 100644
--- a/services/web/app/coffee/Features/Project/ProjectController.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectController.coffee
@@ -18,6 +18,7 @@ InactiveProjectManager = require("../InactiveData/InactiveProjectManager")
ProjectUpdateHandler = require("./ProjectUpdateHandler")
ProjectGetter = require("./ProjectGetter")
PrivilegeLevels = require("../Authorization/PrivilegeLevels")
+AuthenticationController = require("../Authentication/AuthenticationController")
module.exports = ProjectController =
@@ -88,32 +89,34 @@ module.exports = ProjectController =
project_id = req.params.Project_id
projectName = req.body.projectName
logger.log project_id:project_id, projectName:projectName, "cloning project"
- if !req.session.user?
+ if !AuthenticationController.isUserLoggedIn()?
return res.send redir:"/register"
- projectDuplicator.duplicate req.session.user, project_id, projectName, (err, project)->
- if err?
- logger.error err:err, project_id: project_id, user_id: req.session.user._id, "error cloning project"
- return next(err)
- res.send(project_id:project._id)
+ AuthenticationController.getLoggedInUser req, (err, currentUser) ->
+ return next(err) if err?
+ projectDuplicator.duplicate currentUser, project_id, projectName, (err, project)->
+ if err?
+ logger.error err:err, project_id: project_id, user_id: currentUser._id, "error cloning project"
+ return next(err)
+ res.send(project_id:project._id)
newProject: (req, res)->
- user = req.session.user
+ user_id = AuthenticationController.getLoggedInUserId(req)
projectName = req.body.projectName?.trim()
template = req.body.template
- logger.log user: user, projectType: template, name: projectName, "creating project"
+ logger.log user: user_id, projectType: template, name: projectName, "creating project"
async.waterfall [
(cb)->
if template == 'example'
- projectCreationHandler.createExampleProject user._id, projectName, cb
+ projectCreationHandler.createExampleProject user_id, projectName, cb
else
- projectCreationHandler.createBasicProject user._id, projectName, cb
+ projectCreationHandler.createBasicProject user_id, projectName, cb
], (err, project)->
if err?
- logger.error err: err, project: project, user: user, name: projectName, templateType: template, "error creating project"
+ logger.error err: err, project: project, user: user_id, name: projectName, templateType: template, "error creating project"
res.sendStatus 500
else
- logger.log project: project, user: user, name: projectName, templateType: template, "created project"
+ logger.log project: project, user: user_id, name: projectName, templateType: template, "created project"
res.send {project_id:project._id}
@@ -131,51 +134,53 @@ module.exports = ProjectController =
projectListPage: (req, res, next)->
timer = new metrics.Timer("project-list")
- user_id = req.session.user._id
- async.parallel {
- tags: (cb)->
- TagsHandler.getAllTags user_id, cb
- notifications: (cb)->
- NotificationsHandler.getUserNotifications user_id, cb
- projects: (cb)->
- ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb
- hasSubscription: (cb)->
- LimitationsManager.userHasSubscriptionOrIsGroupMember req.session.user, cb
- user: (cb) ->
- User.findById user_id, "featureSwitches", cb
- }, (err, results)->
- if err?
- logger.err err:err, "error getting data for project list page"
- return next(err)
- logger.log results:results, user_id:user_id, "rendering project list"
- tags = results.tags[0]
- notifications = require("underscore").map results.notifications, (notification)->
- notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
- return notification
- projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2]
- user = results.user
- ProjectController._injectProjectOwners projects, (error, projects) ->
- return next(error) if error?
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ AuthenticationController.getLoggedInUser req, (err, currentUser) ->
+ return next(err) if err?
+ async.parallel {
+ tags: (cb)->
+ TagsHandler.getAllTags user_id, cb
+ notifications: (cb)->
+ NotificationsHandler.getUserNotifications user_id, cb
+ projects: (cb)->
+ ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb
+ hasSubscription: (cb)->
+ LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, cb
+ user: (cb) ->
+ User.findById user_id, "featureSwitches", cb
+ }, (err, results)->
+ if err?
+ logger.err err:err, "error getting data for project list page"
+ return next(err)
+ logger.log results:results, user_id:user_id, "rendering project list"
+ tags = results.tags[0]
+ notifications = require("underscore").map results.notifications, (notification)->
+ notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
+ return notification
+ projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2]
+ user = results.user
+ ProjectController._injectProjectOwners projects, (error, projects) ->
+ return next(error) if error?
- viewModel = {
- title:'your_projects'
- priority_title: true
- projects: projects
- tags: tags
- notifications: notifications or []
- user: user
- hasSubscription: results.hasSubscription[0]
- }
+ viewModel = {
+ title:'your_projects'
+ priority_title: true
+ projects: projects
+ tags: tags
+ notifications: notifications or []
+ user: user
+ hasSubscription: results.hasSubscription[0]
+ }
- if Settings?.algolia?.app_id? and Settings?.algolia?.read_only_api_key?
- viewModel.showUserDetailsArea = true
- viewModel.algolia_api_key = Settings.algolia.read_only_api_key
- viewModel.algolia_app_id = Settings.algolia.app_id
- else
- viewModel.showUserDetailsArea = false
+ if Settings?.algolia?.app_id? and Settings?.algolia?.read_only_api_key?
+ viewModel.showUserDetailsArea = true
+ viewModel.algolia_api_key = Settings.algolia.read_only_api_key
+ viewModel.algolia_app_id = Settings.algolia.app_id
+ else
+ viewModel.showUserDetailsArea = false
- res.render 'project/list', viewModel
- timer.done()
+ res.render 'project/list', viewModel
+ timer.done()
loadEditor: (req, res, next)->
@@ -183,8 +188,8 @@ module.exports = ProjectController =
if !Settings.editorIsOpen
return res.render("general/closed", {title:"updating_site"})
- if req.session.user?
- user_id = req.session.user._id
+ if AuthenticationController.isUserLoggedIn(req)?
+ user_id = AuthenticationController.getLoggedInUserId(req)
anonymous = false
else
anonymous = true
diff --git a/services/web/app/coffee/Features/Referal/ReferalController.coffee b/services/web/app/coffee/Features/Referal/ReferalController.coffee
index fbe812a22e..1c8cef03b4 100644
--- a/services/web/app/coffee/Features/Referal/ReferalController.coffee
+++ b/services/web/app/coffee/Features/Referal/ReferalController.coffee
@@ -1,9 +1,11 @@
logger = require('logger-sharelatex')
ReferalHandler = require('./ReferalHandler')
+AuthenticationController = require('../Authentication/AuthenticationController')
-module.exports =
+module.exports =
bonus: (req, res)->
- ReferalHandler.getReferedUserIds req.session.user._id, (err, refered_users)->
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ ReferalHandler.getReferedUserIds user_id, (err, refered_users)->
res.render "referal/bonus",
title: "bonus_please_recommend_us"
refered_users: refered_users
diff --git a/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee b/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee
index c6f02d34bf..18e5a34d39 100644
--- a/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee
+++ b/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee
@@ -1,11 +1,12 @@
User = require("../../models/User").User
+AuthenticationController = require('../Authentication/AuthenticationController')
module.exports = RefererMiddleware =
getUserReferalId: (req, res, next) ->
- if req.session? and req.session.user?
- User.findById req.session.user._id, (error, user) ->
+ if AuthenticationController.isUserLoggedIn()?
+ AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
- req.session.user.referal_id = user.referal_id
+ req.user.referal_id = user.referal_id
next()
else
next()
diff --git a/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee b/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee
index dc71da09fc..f486e94493 100644
--- a/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee
+++ b/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee
@@ -1,24 +1,22 @@
RateLimiter = require "../../infrastructure/RateLimiter"
logger = require "logger-sharelatex"
+AuthenticationController = require('../Authentication/AuthenticationController')
module.exports = RateLimiterMiddlewear =
###
Do not allow more than opts.maxRequests from a single client in
opts.timeInterval. Pass an array of opts.params to segment this based on
parameters in the request URL, e.g.:
-
+
app.get "/project/:project_id", RateLimiterMiddlewear.rateLimit(endpointName: "open-editor", params: ["project_id"])
-
+
will rate limit each project_id separately.
-
+
Unique clients are identified by user_id if logged in, and IP address if not.
###
rateLimit: (opts) ->
return (req, res, next) ->
- if req.session?.user?
- user_id = req.session.user._id
- else
- user_id = req.ip
+ user_id = AuthenticationController.getLoggedInUserId(req) || req.ip
params = (opts.params or []).map (p) -> req.params[p]
params.push user_id
if !opts.endpointName?
@@ -37,4 +35,4 @@ module.exports = RateLimiterMiddlewear =
logger.warn options, "rate limit exceeded"
res.status(429) # Too many requests
res.write("Rate limit reached, please try again later")
- res.end()
\ No newline at end of file
+ res.end()
diff --git a/services/web/app/coffee/Features/Spelling/SpellingController.coffee b/services/web/app/coffee/Features/Spelling/SpellingController.coffee
index 973655701b..cff53ec171 100644
--- a/services/web/app/coffee/Features/Spelling/SpellingController.coffee
+++ b/services/web/app/coffee/Features/Spelling/SpellingController.coffee
@@ -1,13 +1,15 @@
request = require 'request'
Settings = require 'settings-sharelatex'
logger = require 'logger-sharelatex'
+AuthenticationController = require('../Authentication/AuthenticationController')
TEN_SECONDS = 1000 * 10
module.exports = SpellingController =
proxyRequestToSpellingApi: (req, res, next) ->
+ user_id = AuthenticationController.getLoggedInUserId(req)
url = req.url.slice("/spelling".length)
- url = "/user/#{req.session.user._id}#{url}"
+ url = "/user/#{user_id}#{url}"
req.headers["Host"] = Settings.apis.spelling.host
request(url: Settings.apis.spelling.url + url, method: req.method, headers: req.headers, json: req.body, timeout:TEN_SECONDS)
.on "error", (error) ->
diff --git a/services/web/app/coffee/Features/StaticPages/HomeController.coffee b/services/web/app/coffee/Features/StaticPages/HomeController.coffee
index 3e9ac8439c..3535c6c5eb 100755
--- a/services/web/app/coffee/Features/StaticPages/HomeController.coffee
+++ b/services/web/app/coffee/Features/StaticPages/HomeController.coffee
@@ -5,12 +5,13 @@ Path = require "path"
fs = require "fs"
ErrorController = require "../Errors/ErrorController"
+AuthenticationController = require('../Authentication/AuthenticationController')
homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.jade")
module.exports = HomeController =
index : (req,res)->
- if req.session.user
+ if AuthenticationController.isUserLoggedIn(req)?
if req.query.scribtex_path?
res.redirect "/project?scribtex_path=#{req.query.scribtex_path}"
else
@@ -33,4 +34,4 @@ module.exports = HomeController =
res.render "external/#{page}.jade",
title: title
else
- ErrorController.notFound(req, res, next)
\ No newline at end of file
+ ErrorController.notFound(req, res, next)
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
index 943c78f62d..09a9e82faf 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
@@ -13,7 +13,7 @@ module.exports = SubscriptionController =
plansPage: (req, res, next) ->
plans = SubscriptionViewModelBuilder.buildViewModel()
- if !req.session.user?
+ if AuthenticationController.isUserLoggedIn(req)?
baseUrl = "/register?redir="
else
baseUrl = ""
@@ -134,7 +134,7 @@ module.exports = SubscriptionController =
successURL : "#{Settings.siteUrl}/user/subscription/billing-details/update"
user :
id : user._id
-
+
updateBillingDetails: (req, res, next) ->
res.redirect "/user/subscription?saved_billing_details=true"
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee
index 5b20608eea..c09ed1f47d 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee
@@ -3,27 +3,28 @@ logger = require("logger-sharelatex")
SubscriptionLocator = require("./SubscriptionLocator")
ErrorsController = require("../Errors/ErrorController")
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
+AuthenticationController = require('../Authentication/AuthenticationController')
_ = require("underscore")
async = require("async")
module.exports =
addUserToGroup: (req, res)->
- adminUserId = req.session.user._id
+ adminUserId = AuthenticationController.getLoggedInUserId(req)
newEmail = req.body?.email?.toLowerCase()?.trim()
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription"
SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)->
if err?
logger.err err:err, newEmail:newEmail, adminUserId:adminUserId, "error adding user from group"
return res.sendStatus 500
- result =
+ result =
user:user
if err and err.limitReached
result.limitReached = true
res.json(result)
removeUserFromGroup: (req, res)->
- adminUserId = req.session.user._id
+ adminUserId = AuthenticationController.getLoggedInUserId(req)
userToRemove_id = req.params.user_id
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription"
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
@@ -31,10 +32,10 @@ module.exports =
logger.err err:err, adminUserId:adminUserId, userToRemove_id:userToRemove_id, "error removing user from group"
return res.sendStatus 500
res.send()
-
+
removeSelfFromGroup: (req, res)->
adminUserId = req.query.admin_user_id
- userToRemove_id = req.session.user._id
+ userToRemove_id = AuthenticationController.getLoggedInUserId(req)
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription after self request"
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
if err?
@@ -43,7 +44,7 @@ module.exports =
res.send()
renderSubscriptionGroupAdminPage: (req, res)->
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
if !subscription.groupPlan
return res.redirect("/")
@@ -55,11 +56,11 @@ module.exports =
renderGroupInvitePage: (req, res)->
group_subscription_id = req.params.subscription_id
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(group_subscription_id)
if !licence?
return ErrorsController.notFound(req, res)
- jobs =
+ jobs =
partOfGroup: (cb)->
SubscriptionGroupHandler.isUserPartOfGroup user_id, licence.group_subscription_id, cb
subscription: (cb)->
@@ -77,15 +78,18 @@ module.exports =
beginJoinGroup: (req, res)->
subscription_id = req.params.subscription_id
- user_id = req.session.user._id
- licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)
- if !licence?
- return ErrorsController.notFound(req, res)
- SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, req.session.user.email, (err)->
+ AuthenticationController.getLoggedInUser req, (err, currentUser) ->
if err?
- res.sendStatus 500
- else
- res.sendStatus 200
+ logger.err {subscription_id}, "error getting current user"
+ return res.sendStatus 500
+ licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)
+ if !licence?
+ return ErrorsController.notFound(req, res)
+ SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, currentUser.email, (err)->
+ if err?
+ res.sendStatus 500
+ else
+ res.sendStatus 200
completeJoin: (req, res)->
subscription_id = req.params.subscription_id
@@ -109,10 +113,10 @@ module.exports =
return ErrorsController.notFound(req, res)
res.render "subscriptions/group/successful_join",
title: "Sucessfully joined group"
- licenceName:licence.name
+ licenceName:licence.name
exportGroupCsv: (req, res)->
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
logger.log user_id: user_id, "exporting group csv"
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
if !subscription.groupPlan
diff --git a/services/web/app/coffee/Features/Tags/TagsController.coffee b/services/web/app/coffee/Features/Tags/TagsController.coffee
index 2e67be2fd4..0cd15ab5e7 100644
--- a/services/web/app/coffee/Features/Tags/TagsController.coffee
+++ b/services/web/app/coffee/Features/Tags/TagsController.coffee
@@ -1,48 +1,49 @@
TagsHandler = require("./TagsHandler")
logger = require("logger-sharelatex")
+AuthenticationController = require('../Authentication/AuthenticationController')
module.exports =
getAllTags: (req, res, next)->
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "getting tags"
TagsHandler.getAllTags user_id, (error, allTags)->
return next(error) if error?
res.json(allTags)
-
+
createTag: (req, res, next) ->
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
name = req.body.name
logger.log {user_id, name}, "creating tag"
TagsHandler.createTag user_id, name, (error, tag) ->
return next(error) if error?
res.json(tag)
-
+
addProjectToTag: (req, res, next) ->
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
{tag_id, project_id} = req.params
logger.log {user_id, tag_id, project_id}, "adding tag to project"
TagsHandler.addProjectToTag user_id, tag_id, project_id, (error) ->
return next(error) if error?
res.status(204).end()
-
+
removeProjectFromTag: (req, res, next) ->
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
{tag_id, project_id} = req.params
logger.log {user_id, tag_id, project_id}, "removing tag from project"
TagsHandler.removeProjectFromTag user_id, tag_id, project_id, (error) ->
return next(error) if error?
res.status(204).end()
-
+
deleteTag: (req, res, next) ->
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
tag_id = req.params.tag_id
logger.log {user_id, tag_id}, "deleting tag"
TagsHandler.deleteTag user_id, tag_id, (error) ->
return next(error) if error?
res.status(204).end()
-
+
renameTag: (req, res, next) ->
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
tag_id = req.params.tag_id
name = req.body?.name
if !name?
diff --git a/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee b/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee
index f548cadde6..bc6e00a29a 100644
--- a/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee
+++ b/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee
@@ -5,17 +5,16 @@ AuthenticationController = require "../Authentication/AuthenticationController"
module.exports = TrackChangesController =
proxyToTrackChangesApi: (req, res, next = (error) ->) ->
- AuthenticationController.getLoggedInUserId req, (error, user_id) ->
- return next(error) if error?
- 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)
\ No newline at end of file
+ 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/Uploads/ProjectUploadController.coffee b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee
index 557a1d5449..de23c45015 100644
--- a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee
+++ b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee
@@ -4,11 +4,12 @@ fs = require "fs"
Path = require "path"
FileSystemImportManager = require "./FileSystemImportManager"
ProjectUploadManager = require "./ProjectUploadManager"
+AuthenticationController = require('../Authentication/AuthenticationController')
module.exports = ProjectUploadController =
uploadProject: (req, res, next) ->
timer = new metrics.Timer("project-upload")
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
{originalname, path} = req.files.qqfile
name = Path.basename(originalname, ".zip")
ProjectUploadManager.createProjectFromZipArchive user_id, name, path, (error, project) ->
@@ -24,7 +25,7 @@ module.exports = ProjectUploadController =
project: project._id, file_path: path, file_name: name,
"uploaded project"
res.send success: true, project_id: project._id
-
+
uploadFile: (req, res, next) ->
timer = new metrics.Timer("file-upload")
name = req.files.qqfile?.originalname
@@ -35,7 +36,7 @@ module.exports = ProjectUploadController =
logger.err project_id:project_id, name:name, "bad name when trying to upload file"
return res.send success: false
logger.log folder_id:folder_id, project_id:project_id, "getting upload file request"
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
FileSystemImportManager.addEntity user_id, project_id, folder_id, name, path, true, (error, entity) ->
fs.unlink path, ->
timer.done()
@@ -50,6 +51,3 @@ module.exports = ProjectUploadController =
project_id: project_id, file_path: path, file_name: name, folder_id: folder_id
"uploaded file"
res.send success: true, entity_id: entity?._id
-
-
-
diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee
index 25eb2afd4c..ebbebd9c0a 100644
--- a/services/web/app/coffee/Features/User/UserController.coffee
+++ b/services/web/app/coffee/Features/User/UserController.coffee
@@ -8,6 +8,7 @@ logger = require("logger-sharelatex")
metrics = require("../../infrastructure/Metrics")
Url = require("url")
AuthenticationManager = require("../Authentication/AuthenticationManager")
+AuthenticationController = require('../Authentication/AuthenticationController')
UserSessionsManager = require("./UserSessionsManager")
UserUpdater = require("./UserUpdater")
settings = require "settings-sharelatex"
@@ -15,20 +16,21 @@ settings = require "settings-sharelatex"
module.exports = UserController =
deleteUser: (req, res)->
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
UserDeleter.deleteUser user_id, (err)->
if !err?
req.session?.destroy()
res.sendStatus(200)
unsubscribe: (req, res)->
- UserLocator.findById req.session.user._id, (err, user)->
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ UserLocator.findById user_id, (err, user)->
newsLetterManager.unsubscribe user, ->
res.send()
updateUserSettings : (req, res)->
- logger.log user: req.session.user, "updating account settings"
- user_id = req.session.user._id
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ logger.log user: user_id, "updating account settings"
User.findById user_id, (err, user)->
if err? or !user?
logger.err err:err, user_id:user_id, "problem updaing user settings"
@@ -73,7 +75,7 @@ module.exports = UserController =
if err?
logger.err err:err, user_id:user_id, "error getting user for email update"
return res.send 500
- req.session.user.email = user.email
+ req.user.email = user.email
UserHandler.populateGroupLicenceInvite user, (err)-> #need to refresh this in the background
if err?
logger.err err:err, "error populateGroupLicenceInvite"
@@ -83,13 +85,13 @@ module.exports = UserController =
metrics.inc "user.logout"
logger.log user: req?.session?.user, "logging out"
sessionId = req.sessionID
- user = req?.session?.user
- req.logout?() # passport logout
- req.session.destroy (err)->
- if err
- logger.err err: err, 'error destorying session'
- UserSessionsManager.untrackSession(user, sessionId)
- res.redirect '/login'
+ AuthenticationController.getLoggedInUser req, (err, user) ->
+ req.logout?() # passport logout
+ req.session.destroy (err)->
+ if err
+ logger.err err: err, 'error destorying session'
+ UserSessionsManager.untrackSession(user, sessionId)
+ res.redirect '/login'
register : (req, res, next = (error) ->)->
email = req.body.email
@@ -106,10 +108,11 @@ module.exports = UserController =
changePassword : (req, res, next = (error) ->)->
metrics.inc "user.password-change"
oldPass = req.body.currentPassword
- AuthenticationManager.authenticate {_id:req.session.user._id}, oldPass, (err, user)->
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ AuthenticationManager.authenticate {_id:user_id}, oldPass, (err, user)->
return next(err) if err?
if(user)
- logger.log user: req.session.user, "changing password"
+ logger.log user: user._id, "changing password"
newPassword1 = req.body.newPassword1
newPassword2 = req.body.newPassword2
if newPassword1 != newPassword2
diff --git a/services/web/app/coffee/Features/User/UserPagesController.coffee b/services/web/app/coffee/Features/User/UserPagesController.coffee
index 567cacd35c..c4bbb3de0a 100644
--- a/services/web/app/coffee/Features/User/UserPagesController.coffee
+++ b/services/web/app/coffee/Features/User/UserPagesController.coffee
@@ -4,6 +4,7 @@ ErrorController = require("../Errors/ErrorController")
logger = require("logger-sharelatex")
Settings = require("settings-sharelatex")
fs = require('fs')
+AuthenticationController = require('../Authentication/AuthenticationController')
module.exports =
@@ -22,14 +23,14 @@ module.exports =
sharedProjectData: sharedProjectData
newTemplateData: newTemplateData
new_email:req.query.new_email || ""
-
+
activateAccountPage: (req, res) ->
# An 'activation' is actually just a password reset on an account that
# was set with a random password originally.
logger.log query:req.query, "activiate account page called"
if !req.query?.user_id? or !req.query?.token?
return ErrorController.notFound(req, res)
-
+
UserGetter.getUser req.query.user_id, {email: 1, loginCount: 1}, (error, user) ->
return next(error) if error?
if !user
@@ -53,8 +54,9 @@ module.exports =
email: req.query.email
settingsPage : (req, res, next)->
- logger.log user: req.session.user, "loading settings page"
- UserLocator.findById req.session.user._id, (err, user)->
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ logger.log user: user_id, "loading settings page"
+ UserLocator.findById user_id, (err, user)->
return next(err) if err?
res.render 'user/settings',
title:'account_settings'
diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index d3b9d9a24b..dabe33af88 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -5,6 +5,7 @@ Settings = require('settings-sharelatex')
SubscriptionFormatters = require('../Features/Subscription/SubscriptionFormatters')
querystring = require('querystring')
SystemMessageManager = require("../Features/SystemMessages/SystemMessageManager")
+AuthenticationController = require("../Features/Authentication/AuthenticationController")
_ = require("underscore")
Modules = require "./Modules"
Url = require "url"
@@ -59,7 +60,7 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.session = req.session
next()
- webRouter.use (req, res, next)->
+ webRouter.use (req, res, next)->
cdnBlocked = req.query.nocdn == 'true' or req.session.cdnBlocked
@@ -77,7 +78,7 @@ module.exports = (app, webRouter, apiRouter)->
staticFilesBase = Settings.cdn?.web?.darkHost
else
staticFilesBase = ""
-
+
res.locals.jsPath = jsPath
res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath)
@@ -86,7 +87,7 @@ module.exports = (app, webRouter, apiRouter)->
path = Path.join(jsPath, jsFile)
doFingerPrint = opts.fingerprint != false
-
+
if !opts.qs?
opts.qs = {}
@@ -95,7 +96,7 @@ module.exports = (app, webRouter, apiRouter)->
if opts.cdn != false
path = Url.resolve(staticFilesBase, path)
-
+
qs = querystring.stringify(opts.qs)
if qs? and qs.length > 0
@@ -115,7 +116,7 @@ module.exports = (app, webRouter, apiRouter)->
- webRouter.use (req, res, next)->
+ webRouter.use (req, res, next)->
res.locals.settings = Settings
next()
@@ -143,7 +144,7 @@ module.exports = (app, webRouter, apiRouter)->
return formatedPrivileges[privilegeLevel] || "Private"
next()
- webRouter.use (req, res, next)->
+ webRouter.use (req, res, next)->
res.locals.buildReferalUrl = (referal_medium) ->
url = Settings.siteUrl
if req.session? and req.session.user? and req.session.user.referal_id?
@@ -167,7 +168,7 @@ module.exports = (app, webRouter, apiRouter)->
return ""
res.locals.getLoggedInUserId = ->
- return req.session.user?._id
+ return AuthenticationController.getLoggedInUserId(req)
next()
webRouter.use (req, res, next) ->
@@ -179,11 +180,11 @@ module.exports = (app, webRouter, apiRouter)->
return req.query?[field]
next()
- webRouter.use (req, res, next)->
+ webRouter.use (req, res, next)->
res.locals.fingerprint = getFingerprint
next()
- webRouter.use (req, res, next)->
+ webRouter.use (req, res, next)->
res.locals.formatPrice = SubscriptionFormatters.formatPrice
next()
@@ -193,11 +194,11 @@ module.exports = (app, webRouter, apiRouter)->
next()
webRouter.use (req, res, next)->
- if req.session.user?
+ if req.user?
res.locals.user =
- email: req.session.user.email
- first_name: req.session.user.first_name
- last_name: req.session.user.last_name
+ email: req.user.email
+ first_name: req.user.first_name
+ last_name: req.user.last_name
if req.session.justRegistered
res.locals.justRegistered = true
delete req.session.justRegistered
@@ -223,7 +224,7 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.nav[key] = _.clone(Settings.nav[key])
res.locals.templates = Settings.templateLinks
next()
-
+
webRouter.use (req, res, next) ->
SystemMessageManager.getMessages (error, messages = []) ->
res.locals.systemMessages = messages
@@ -246,5 +247,3 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.moduleIncludes = Modules.moduleIncludes
res.locals.moduleIncludesAvailable = Modules.moduleIncludesAvailable
next()
-
-
From eca4c46f7ffebd1d223529537e6122f7de58669f Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Mon, 5 Sep 2016 16:23:37 +0100
Subject: [PATCH 240/378] WIP: refactor
---
.../web/app/coffee/infrastructure/ExpressLocals.coffee | 8 ++++----
services/web/app/coffee/infrastructure/Server.coffee | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index dabe33af88..33237e3ad4 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -147,12 +147,12 @@ module.exports = (app, webRouter, apiRouter)->
webRouter.use (req, res, next)->
res.locals.buildReferalUrl = (referal_medium) ->
url = Settings.siteUrl
- if req.session? and req.session.user? and req.session.user.referal_id?
- url+="?r=#{req.session.user.referal_id}&rm=#{referal_medium}&rs=b" # Referal source = bonus
+ if req.user? and req.referal_id?
+ url+="?r=#{req.user.referal_id}&rm=#{referal_medium}&rs=b" # Referal source = bonus
return url
res.locals.getReferalId = ->
- if req.session? and req.session.user? and req.session.user.referal_id
- return req.session.user.referal_id
+ if req.user? and req.referal_id?
+ return req.user.referal_id
res.locals.getReferalTagLine = ->
tagLines = [
"Roar!"
diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee
index 095c98be2b..c1ce2a92cd 100644
--- a/services/web/app/coffee/infrastructure/Server.coffee
+++ b/services/web/app/coffee/infrastructure/Server.coffee
@@ -119,7 +119,7 @@ apiRouter.use(currentUserMiddleware)
webRouter.use (req, res, next) ->
req.session.touch()
if req?.session?.user?
- UserSessionsManager.touch(req.session.user, (err)->)
+ UserSessionsManager.touch(req.user, (err)->)
next()
webRouter.use ReferalConnect.use
From 6aef092dce4c72805c53830ec0ac0f69aca6ead6 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 6 Sep 2016 09:29:58 +0100
Subject: [PATCH 241/378] fix typo
---
.../app/coffee/Features/Analytics/AnalyticsController.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee
index 4d5239ac45..f9431a0f5c 100644
--- a/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee
+++ b/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee
@@ -1,4 +1,4 @@
-AnalyticsManager = equire "./AnalyticsManager"
+AnalyticsManager = require "./AnalyticsManager"
module.exports = AnalyticsController =
recordEvent: (req, res, next) ->
From c8ee803570d589b7ae642c9283d1a22c1269fcbb Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 6 Sep 2016 09:37:53 +0100
Subject: [PATCH 242/378] fix limits on sharing, account for both members and
invites.
---
.../ide/share/controllers/ShareProjectModalController.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
index 409608825e..48c6440718 100644
--- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
+++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
@@ -19,7 +19,7 @@ define [
, 200
INFINITE_COLLABORATORS = -1
- $scope.$watch "project.members.length", (noOfMembers) ->
+ $scope.$watch "(project.members.length + project.invites.length)", (noOfMembers) ->
allowedNoOfMembers = $scope.project.features.collaborators
$scope.canAddCollaborators = noOfMembers < allowedNoOfMembers or allowedNoOfMembers == INFINITE_COLLABORATORS
From afa910c32d3f34fd141e142b3bdfa6c42f2b7814 Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Tue, 6 Sep 2016 11:19:14 +0100
Subject: [PATCH 243/378] turn on syntax check for all users
---
.../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index c79d654dbc..790f2384a1 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -74,7 +74,8 @@ define [
$scope.pdf.renderingError = true
# abort compile if syntax checks fail
- $scope.stop_on_validation_error = localStorage("stop_on_validation_error:#{$scope.project_id}") or ide.$scope?.user?.betaProgram
+ $scope.stop_on_validation_error = localStorage("stop_on_validation_error:#{$scope.project_id}")
+ $scope.stop_on_validation_error ?= true # turn on for all users by default
$scope.$watch "stop_on_validation_error", (new_value, old_value) ->
if new_value? and old_value != new_value
localStorage("stop_on_validation_error:#{$scope.project_id}", new_value)
From e412e662eef0107da7c3937f0a628d4459f28d47 Mon Sep 17 00:00:00 2001
From: James Allen
Date: Tue, 6 Sep 2016 11:41:14 +0100
Subject: [PATCH 244/378] Set default compileTimeout to 3 minutes for onsite
users
---
services/web/config/settings.defaults.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee
index 10fef74a11..ea5b6ef8a1 100644
--- a/services/web/config/settings.defaults.coffee
+++ b/services/web/config/settings.defaults.coffee
@@ -155,7 +155,7 @@ module.exports = settings =
collaborators: -1
dropbox: true
versioning: true
- compileTimeout: 60
+ compileTimeout: 180
compileGroup: "standard"
references: true
templates: true
From 749658a91652393cd09690d1c2861f1a858155c6 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 6 Sep 2016 13:21:22 +0100
Subject: [PATCH 245/378] WIP: fixing acceptance tests
---
.../AuthenticationController.coffee | 15 ++++++---------
.../CollaboratorsInviteController.coffee | 2 +-
2 files changed, 7 insertions(+), 10 deletions(-)
diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index 572ebe5b3f..afb986f617 100644
--- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -83,21 +83,18 @@ module.exports = AuthenticationController =
# TODO: perhaps should produce an error if the current user is not present
getLoggedInUser: (req, callback = (error, user) ->) ->
- if req.session?.user?._id?
- query = req.session.user._id
- else
- return callback null, null
-
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ if !user_id?
+ return callback(null, null)
# omit sensitive information
- UserGetter.getUser query, {hashedPassword: false, refProviders: false}, callback
+ UserGetter.getUser user_id, {hashedPassword: false, refProviders: false}, callback
requireLogin: () ->
doRequest = (req, res, next = (error) ->) ->
- console.log ">>>>", req.currentUser()
- if !AuthenticationController.isUserLoggedIn()?
+ if !AuthenticationController.isUserLoggedIn(req)
AuthenticationController._redirectToLoginOrRegisterPage(req, res)
else
- return next()
+ next()
return doRequest
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index cfc6c4b08d..1a625c7eaf 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -117,9 +117,9 @@ module.exports = CollaboratorsInviteController =
projectId = req.params.Project_id
inviteId = req.params.invite_id
{token} = req.body
- logger.log {projectId, inviteId, userId: currentUser._id}, "accepting invite"
AuthenticationController.getLoggedInUser req, (err, currentUser) ->
return callback(err) if err?
+ logger.log {projectId, inviteId, userId: currentUser._id}, "accepting invite"
CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) ->
if err?
logger.err {projectId, inviteId}, "error accepting invite by token"
From afdded702b1bd01c6477f162e3f9b31dfd3f88fe Mon Sep 17 00:00:00 2001
From: Paulo Reis
Date: Tue, 6 Sep 2016 14:59:18 +0100
Subject: [PATCH 246/378] Use an object instead of a string, to avoid writing
to another scope.
---
services/web/app/views/project/list/project-list.jade | 4 ++--
.../public/coffee/main/project-list/project-list.coffee | 8 +++++---
2 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/services/web/app/views/project/list/project-list.jade b/services/web/app/views/project/list/project-list.jade
index 649c860b26..01007213d0 100644
--- a/services/web/app/views/project/list/project-list.jade
+++ b/services/web/app/views/project/list/project-list.jade
@@ -8,7 +8,7 @@
input.form-control.col-md-7.col-xs-12(
placeholder="#{translate('search_projects')}…",
autofocus='autofocus',
- ng-model="searchText",
+ ng-model="searchText.value",
focus-on='search:clear',
ng-keyup="searchProjects()"
)
@@ -16,7 +16,7 @@
i.fa.fa-times.form-control-feedback(
ng-click="clearSearchText()",
style="cursor: pointer;",
- ng-show="searchText.length > 0"
+ ng-show="searchText.value.length > 0"
)
//- i.fa.fa-remove
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 313b52ba1f..39ce68043f 100644
--- a/services/web/public/coffee/main/project-list/project-list.coffee
+++ b/services/web/public/coffee/main/project-list/project-list.coffee
@@ -11,6 +11,8 @@ define [
$scope.filter = "all"
$scope.predicate = "lastUpdated"
$scope.reverse = true
+ $scope.searchText =
+ value : ""
if $scope.projects.length == 0
$timeout () ->
@@ -69,7 +71,7 @@ define [
$scope.updateVisibleProjects()
$scope.clearSearchText = () ->
- $scope.searchText = ""
+ $scope.searchText.value = ""
$scope.filter = "all"
$scope.$emit "search:clear"
$scope.updateVisibleProjects()
@@ -96,8 +98,8 @@ define [
for project in $scope.projects
visible = true
# Only show if it matches any search text
- if $scope.searchText? and $scope.searchText != ""
- if !project.name.toLowerCase().match($scope.searchText.toLowerCase())
+ if $scope.searchText.value? and $scope.searchText.value != ""
+ if !project.name.toLowerCase().match($scope.searchText.value.toLowerCase())
visible = false
# Only show if it matches the selected tag
if $scope.filter == "tag" and selectedTag? and project.id not in selectedTag.project_ids
From b0a10c948cc2e74bd9e826364f327ddc9f9e3442 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 6 Sep 2016 15:22:13 +0100
Subject: [PATCH 247/378] wip refactor
---
.../Authentication/AuthenticationController.coffee | 11 +++++++++--
.../app/coffee/infrastructure/ExpressLocals.coffee | 4 ++++
services/web/app/views/layout/navbar.jade | 4 ++--
.../web/test/acceptance/coffee/helpers/redis.coffee | 10 ++++++++--
4 files changed, 23 insertions(+), 6 deletions(-)
diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index afb986f617..096f3335ca 100644
--- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -71,12 +71,19 @@ module.exports = AuthenticationController =
# TODO: perhaps should produce an error if the current user is not present
getLoggedInUserId: (req) ->
+ user = AuthenticationController.getSessionUser(req)
+ if user?
+ return user._id
+ else
+ return null
+
+ getSessionUser: (req) ->
# old sessions
if req?.session?.user?._id?
- return req.session.user._id.toString()
+ return req.session.user
# new passport sessions
else if req?.session?.passport?.user?._id?
- return req.session.passport.user._id.toString()
+ return req.session.passport.user
# neither
else
return null
diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index 33237e3ad4..8d519ecf28 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -169,6 +169,10 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.getLoggedInUserId = ->
return AuthenticationController.getLoggedInUserId(req)
+ res.locals.isUserLoggedIn = ->
+ return AuthenticationController.isUserLoggedIn(req)
+ res.locals.getSessionUser = ->
+ return AuthenticationController.getSessionUser(req)
next()
webRouter.use (req, res, next) ->
diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.jade
index b6b3c61741..e0a89fdea7 100644
--- a/services/web/app/views/layout/navbar.jade
+++ b/services/web/app/views/layout/navbar.jade
@@ -13,7 +13,7 @@ nav.navbar.navbar-default
.navbar-collapse.collapse(collapse="navCollapsed")
ul.nav.navbar-nav.navbar-right
- if (session && session.user && session.user.isAdmin)
+ if (getSessionUser() && getSessionUser().isAdmin)
li.dropdown(class="subdued", dropdown)
a.dropdown-toggle(href, dropdown-toggle)
| Admin
@@ -25,7 +25,7 @@ nav.navbar.navbar-default
a(href="/admin/user") Manage Users
each item in nav.header
- if ((item.only_when_logged_in && session && session.user) || (item.only_when_logged_out && (!session || !session.user)) || (!item.only_when_logged_out && !item.only_when_logged_in))
+ 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)
a.dropdown-toggle(href, dropdown-toggle)
diff --git a/services/web/test/acceptance/coffee/helpers/redis.coffee b/services/web/test/acceptance/coffee/helpers/redis.coffee
index 2611e4fc57..c4cb0ad032 100644
--- a/services/web/test/acceptance/coffee/helpers/redis.coffee
+++ b/services/web/test/acceptance/coffee/helpers/redis.coffee
@@ -8,8 +8,14 @@ rclient = redis.createClient(Settings.redis.web)
module.exports =
getUserSessions: (user, callback=(err, sessionsSet)->) ->
- rclient.smembers "UserSessions:#{user._id}", (err, result) ->
- return callback(err, result)
+ console.log ">> user, get sessions", user._id
+ setTimeout(
+ ()->
+ rclient.smembers "UserSessions:#{user._id}", (err, result) ->
+ console.log ">>", result
+ return callback(err, result)
+ , 1000
+ )
clearUserSessions: (user, callback=(err)->) ->
sessionSetKey = "UserSessions:#{user._id}"
From 3a5b3a8e8d868be2489f7b45bb17198708782e34 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Tue, 6 Sep 2016 15:55:34 +0100
Subject: [PATCH 248/378] wip: acceptance tests working
---
.../Authentication/AuthenticationController.coffee | 5 -----
.../web/test/acceptance/coffee/helpers/redis.coffee | 10 ++--------
2 files changed, 2 insertions(+), 13 deletions(-)
diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index 096f3335ca..161b0f2078 100644
--- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -18,7 +18,6 @@ module.exports = AuthenticationController =
# AuthenticationController.doLogin req.body, req, res, next
serializeUser: (user, callback) ->
- console.log ">> serialize", user._id
lightUser =
_id: user._id
first_name: user.first_name
@@ -31,14 +30,11 @@ module.exports = AuthenticationController =
callback(null, lightUser)
deserializeUser: (user, cb) ->
- console.log ">> de-serialize", user._id
cb(null, user)
doPassportLogin: (req, username, password, done) ->
- console.log(">>", username)
email = username.toLowerCase()
redir = Url.parse(req?.body?.redir or "/project").path
- console.log ">> doing passport login", username, password, redir
LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
return done(err) if err?
if !isAllowed
@@ -58,7 +54,6 @@ module.exports = AuthenticationController =
# capture the request ip for use when creating the session
user._login_req_ip = req.ip
req._redir = redir
- console.log ">> done, returning user"
return done(null, user)
else
AuthenticationController._recordFailedLogin()
diff --git a/services/web/test/acceptance/coffee/helpers/redis.coffee b/services/web/test/acceptance/coffee/helpers/redis.coffee
index c4cb0ad032..2611e4fc57 100644
--- a/services/web/test/acceptance/coffee/helpers/redis.coffee
+++ b/services/web/test/acceptance/coffee/helpers/redis.coffee
@@ -8,14 +8,8 @@ rclient = redis.createClient(Settings.redis.web)
module.exports =
getUserSessions: (user, callback=(err, sessionsSet)->) ->
- console.log ">> user, get sessions", user._id
- setTimeout(
- ()->
- rclient.smembers "UserSessions:#{user._id}", (err, result) ->
- console.log ">>", result
- return callback(err, result)
- , 1000
- )
+ rclient.smembers "UserSessions:#{user._id}", (err, result) ->
+ return callback(err, result)
clearUserSessions: (user, callback=(err)->) ->
sessionSetKey = "UserSessions:#{user._id}"
From 9758dd77b390e4a45004e8964291fe0252d60872 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 7 Sep 2016 08:58:57 +0100
Subject: [PATCH 249/378] kill whitespace
---
services/web/app/coffee/infrastructure/ExpressLocals.coffee | 2 --
1 file changed, 2 deletions(-)
diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index 8d519ecf28..7e2945ead9 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -82,7 +82,6 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.jsPath = jsPath
res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath)
-
res.locals.buildJsPath = (jsFile, opts = {})->
path = Path.join(jsPath, jsFile)
@@ -103,7 +102,6 @@ module.exports = (app, webRouter, apiRouter)->
path = path + "?" + qs
return path
-
res.locals.buildCssPath = (cssFile)->
path = Path.join("/stylesheets/", cssFile)
return Url.resolve(staticFilesBase, path) + "?fingerprint=" + getFingerprint(path)
From cc5ddc92bbdd77deb62b8c07265a12669dc36aa0 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 7 Sep 2016 10:30:58 +0100
Subject: [PATCH 250/378] use `getSessionUser` rather than `getLoggedInUser`
---
.../CollaboratorsInviteController.coffee | 128 ++++---
.../Features/Project/ProjectController.coffee | 98 +++--
.../Features/Referal/ReferalMiddleware.coffee | 7 +-
.../SubscriptionController.coffee | 334 +++++++++---------
.../SubscriptionGroupController.coffee | 22 +-
.../Features/User/UserController.coffee | 14 +-
6 files changed, 292 insertions(+), 311 deletions(-)
diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
index 1a625c7eaf..847bd99bc3 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
@@ -24,27 +24,26 @@ module.exports = CollaboratorsInviteController =
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
email = req.body.email
- AuthenticationController.getLoggedInUser req, (err, sendingUser) ->
- return callback(err) if err?
- sendingUserId = sendingUser._id
- logger.log {projectId, email, sendingUserId}, "inviting to project"
- LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
- return next(error) if error?
- if !allowed
- logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
- return res.json {invite: null}
- {email, privileges} = req.body
- email = EmailHelper.parseEmail(email)
- if !email? or email == ""
- logger.log {projectId, email, sendingUserId}, "invalid email address"
- return res.sendStatus(400)
- 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}
+ sendingUser = AuthenticationController.getSessionUser(req)
+ sendingUserId = sendingUser._id
+ logger.log {projectId, email, sendingUserId}, "inviting to project"
+ LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
+ return next(error) if error?
+ if !allowed
+ logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
+ return res.json {invite: null}
+ {email, privileges} = req.body
+ email = EmailHelper.parseEmail(email)
+ if !email? or email == ""
+ logger.log {projectId, email, sendingUserId}, "invalid email address"
+ return res.sendStatus(400)
+ 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
@@ -61,13 +60,12 @@ module.exports = CollaboratorsInviteController =
projectId = req.params.Project_id
inviteId = req.params.invite_id
logger.log {projectId, inviteId}, "resending invite"
- AuthenticationController.getLoggedInUser req, (err, sendingUser) ->
- return callback(err) if err?
- CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) ->
- if err?
- logger.err {projectId, inviteId}, "error resending invite"
- return next(err)
- res.sendStatus(201)
+ sendingUser = AuthenticationController.getSessionUser(req)
+ CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) ->
+ if err?
+ logger.err {projectId, inviteId}, "error resending invite"
+ return next(err)
+ res.sendStatus(201)
viewInvite: (req, res, next) ->
projectId = req.params.Project_id
@@ -76,54 +74,52 @@ module.exports = CollaboratorsInviteController =
logger.log {projectId, token}, "invite not valid, rendering not-valid page"
res.render "project/invite/not-valid", {title: "Invalid Invite"}
# check if the user is already a member of the project
- AuthenticationController.getLoggedInUser req, (err, currentUser) ->
- return callback(err) if err?
- CollaboratorsHandler.isUserMemberOfProject currentUser._id, projectId, (err, isMember, _privilegeLevel) ->
+ currentUser = AuthenticationController.getSessionUser(req)
+ CollaboratorsHandler.isUserMemberOfProject currentUser._id, projectId, (err, isMember, _privilegeLevel) ->
+ if err?
+ logger.err {err, projectId}, "error checking if user is member of project"
+ return next(err)
+ if isMember
+ logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting"
+ return res.redirect "/project/#{projectId}"
+ # get the invite
+ CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) ->
if err?
- logger.err {err, projectId}, "error checking if user is member of project"
+ logger.err {projectId, token}, "error getting invite by token"
return next(err)
- if isMember
- logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting"
- return res.redirect "/project/#{projectId}"
- # get the invite
- CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) ->
+ # check if invite is gone, or otherwise non-existent
+ if !invite?
+ logger.log {projectId, token}, "no invite found for this token"
+ return _renderInvalidPage()
+ # check the user who sent the invite exists
+ UserGetter.getUser {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
if err?
- logger.err {projectId, token}, "error getting invite by token"
+ logger.err {err, projectId}, "error getting project owner"
return next(err)
- # check if invite is gone, or otherwise non-existent
- if !invite?
- logger.log {projectId, token}, "no invite found for this token"
+ if !owner?
+ logger.log {projectId}, "no project owner found"
return _renderInvalidPage()
- # check the user who sent the invite exists
- UserGetter.getUser {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
+ # fetch the project name
+ ProjectGetter.getProject projectId, {}, (err, project) ->
if err?
- logger.err {err, projectId}, "error getting project owner"
+ logger.err {err, projectId}, "error getting project"
return next(err)
- if !owner?
- logger.log {projectId}, "no project owner found"
+ if !project?
+ logger.log {projectId}, "no project found"
return _renderInvalidPage()
- # fetch the project name
- ProjectGetter.getProject projectId, {}, (err, project) ->
- if err?
- logger.err {err, projectId}, "error getting project"
- return next(err)
- if !project?
- logger.log {projectId}, "no project found"
- return _renderInvalidPage()
- # finally render the invite
- res.render "project/invite/show", {invite, project, owner, title: "Project Invite"}
+ # finally render the invite
+ res.render "project/invite/show", {invite, project, owner, title: "Project Invite"}
acceptInvite: (req, res, next) ->
projectId = req.params.Project_id
inviteId = req.params.invite_id
{token} = req.body
- AuthenticationController.getLoggedInUser req, (err, currentUser) ->
- return callback(err) if err?
- logger.log {projectId, inviteId, userId: currentUser._id}, "accepting invite"
- CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) ->
- if err?
- logger.err {projectId, inviteId}, "error accepting invite by token"
- return next(err)
- EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
- AnalyticsManger.recordEvent(currentUser._id, "project-invite-accept", {inviteId:inviteId, projectId:projectId})
- res.redirect "/project/#{projectId}"
+ currentUser = AuthenticationController.getSessionUser(req)
+ logger.log {projectId, inviteId, userId: currentUser._id}, "accepting invite"
+ CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) ->
+ if err?
+ logger.err {projectId, inviteId}, "error accepting invite by token"
+ return next(err)
+ EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
+ AnalyticsManger.recordEvent(currentUser._id, "project-invite-accept", {inviteId:inviteId, projectId:projectId})
+ res.redirect "/project/#{projectId}"
diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee
index 9c016b7b2e..28af4fbf8d 100644
--- a/services/web/app/coffee/Features/Project/ProjectController.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectController.coffee
@@ -91,13 +91,12 @@ module.exports = ProjectController =
logger.log project_id:project_id, projectName:projectName, "cloning project"
if !AuthenticationController.isUserLoggedIn()?
return res.send redir:"/register"
- AuthenticationController.getLoggedInUser req, (err, currentUser) ->
- return next(err) if err?
- projectDuplicator.duplicate currentUser, project_id, projectName, (err, project)->
- if err?
- logger.error err:err, project_id: project_id, user_id: currentUser._id, "error cloning project"
- return next(err)
- res.send(project_id:project._id)
+ currentUser = AuthenticationController.getSessionUser(req)
+ projectDuplicator.duplicate currentUser, project_id, projectName, (err, project)->
+ if err?
+ logger.error err:err, project_id: project_id, user_id: currentUser._id, "error cloning project"
+ return next(err)
+ res.send(project_id:project._id)
newProject: (req, res)->
@@ -135,52 +134,51 @@ module.exports = ProjectController =
projectListPage: (req, res, next)->
timer = new metrics.Timer("project-list")
user_id = AuthenticationController.getLoggedInUserId(req)
- AuthenticationController.getLoggedInUser req, (err, currentUser) ->
- return next(err) if err?
- async.parallel {
- tags: (cb)->
- TagsHandler.getAllTags user_id, cb
- notifications: (cb)->
- NotificationsHandler.getUserNotifications user_id, cb
- projects: (cb)->
- ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb
- hasSubscription: (cb)->
- LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, cb
- user: (cb) ->
- User.findById user_id, "featureSwitches", cb
- }, (err, results)->
- if err?
- logger.err err:err, "error getting data for project list page"
- return next(err)
- logger.log results:results, user_id:user_id, "rendering project list"
- tags = results.tags[0]
- notifications = require("underscore").map results.notifications, (notification)->
- notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
- return notification
- projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2]
- user = results.user
- ProjectController._injectProjectOwners projects, (error, projects) ->
- return next(error) if error?
+ currentUser = AuthenticationController.getSessionUser(req)
+ async.parallel {
+ tags: (cb)->
+ TagsHandler.getAllTags user_id, cb
+ notifications: (cb)->
+ NotificationsHandler.getUserNotifications user_id, cb
+ projects: (cb)->
+ ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb
+ hasSubscription: (cb)->
+ LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, cb
+ user: (cb) ->
+ User.findById user_id, "featureSwitches", cb
+ }, (err, results)->
+ if err?
+ logger.err err:err, "error getting data for project list page"
+ return next(err)
+ logger.log results:results, user_id:user_id, "rendering project list"
+ tags = results.tags[0]
+ notifications = require("underscore").map results.notifications, (notification)->
+ notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
+ return notification
+ projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2]
+ user = results.user
+ ProjectController._injectProjectOwners projects, (error, projects) ->
+ return next(error) if error?
- viewModel = {
- title:'your_projects'
- priority_title: true
- projects: projects
- tags: tags
- notifications: notifications or []
- user: user
- hasSubscription: results.hasSubscription[0]
- }
+ viewModel = {
+ title:'your_projects'
+ priority_title: true
+ projects: projects
+ tags: tags
+ notifications: notifications or []
+ user: user
+ hasSubscription: results.hasSubscription[0]
+ }
- if Settings?.algolia?.app_id? and Settings?.algolia?.read_only_api_key?
- viewModel.showUserDetailsArea = true
- viewModel.algolia_api_key = Settings.algolia.read_only_api_key
- viewModel.algolia_app_id = Settings.algolia.app_id
- else
- viewModel.showUserDetailsArea = false
+ if Settings?.algolia?.app_id? and Settings?.algolia?.read_only_api_key?
+ viewModel.showUserDetailsArea = true
+ viewModel.algolia_api_key = Settings.algolia.read_only_api_key
+ viewModel.algolia_app_id = Settings.algolia.app_id
+ else
+ viewModel.showUserDetailsArea = false
- res.render 'project/list', viewModel
- timer.done()
+ res.render 'project/list', viewModel
+ timer.done()
loadEditor: (req, res, next)->
diff --git a/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee b/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee
index 18e5a34d39..ccfe7a802f 100644
--- a/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee
+++ b/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee
@@ -4,9 +4,8 @@ AuthenticationController = require('../Authentication/AuthenticationController')
module.exports = RefererMiddleware =
getUserReferalId: (req, res, next) ->
if AuthenticationController.isUserLoggedIn()?
- AuthenticationController.getLoggedInUser req, (error, user) ->
- return next(error) if error?
- req.user.referal_id = user.referal_id
- next()
+ user = AuthenticationController.getSessionUser(req)
+ req.user.referal_id = user.referal_id
+ next()
else
next()
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
index 09a9e82faf..e9ccefe2c8 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
@@ -32,168 +32,159 @@ module.exports = SubscriptionController =
#get to show the recurly.js page
paymentPage: (req, res, next) ->
- AuthenticationController.getLoggedInUser req, (error, user) =>
- return next(error) if error?
- plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
- LimitationsManager.userHasSubscription user, (err, hasSubscription)->
- return next(err) if err?
- if hasSubscription or !plan?
- res.redirect "/user/subscription"
- else
- currency = req.query.currency?.toUpperCase()
- GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)->
- return next(err) if err?
- if recomendedCurrency? and !currency?
- currency = recomendedCurrency
- RecurlyWrapper.sign {
- subscription:
- plan_code : req.query.planCode
+ user = AuthenticationController.getSessionUser(req)
+ plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
+ LimitationsManager.userHasSubscription user, (err, hasSubscription)->
+ return next(err) if err?
+ if hasSubscription or !plan?
+ res.redirect "/user/subscription"
+ else
+ currency = req.query.currency?.toUpperCase()
+ GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)->
+ return next(err) if err?
+ if recomendedCurrency? and !currency?
+ currency = recomendedCurrency
+ RecurlyWrapper.sign {
+ subscription:
+ plan_code : req.query.planCode
+ currency: currency
+ account_code: user._id
+ }, (error, signature) ->
+ return next(error) if error?
+ res.render "subscriptions/new",
+ title : "subscribe"
+ plan_code: req.query.planCode
+ currency: currency
+ countryCode:countryCode
+ plan:plan
+ showStudentPlan: req.query.ssp
+ recurlyConfig: JSON.stringify
currency: currency
- account_code: user._id
- }, (error, signature) ->
- return next(error) if error?
- res.render "subscriptions/new",
- title : "subscribe"
- plan_code: req.query.planCode
- currency: currency
- countryCode:countryCode
- plan:plan
- showStudentPlan: req.query.ssp
- recurlyConfig: JSON.stringify
- currency: currency
- subdomain: Settings.apis.recurly.subdomain
- showCouponField: req.query.scf
- showVatField: req.query.svf
- couponCode: req.query.cc or ""
+ subdomain: Settings.apis.recurly.subdomain
+ showCouponField: req.query.scf
+ showVatField: req.query.svf
+ couponCode: req.query.cc or ""
userSubscriptionPage: (req, res, next) ->
- AuthenticationController.getLoggedInUser req, (error, user) =>
- return next(error) if error?
- LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
- return next(err) if err?
- groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user)
- if subscription?.customAccount
- logger.log user: user, "redirecting to custom account page"
- res.redirect "/user/subscription/custom_account"
- else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember
- logger.log user:user, "redirecting to group subscription invite page"
- res.redirect groupLicenceInviteUrl
- else if !hasSubOrIsGroupMember
- logger.log user: user, "redirecting to plans"
- res.redirect "/user/subscription/plans"
- else
- SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
- return next(error) if error?
- logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard"
- plans = SubscriptionViewModelBuilder.buildViewModel()
- res.render "subscriptions/dashboard",
- title: "your_subscription"
- recomendedCurrency: subscription?.currency
- taxRate:subscription?.taxRate
- plans: plans
- subscription: subscription || {}
- groupSubscriptions: groupSubscriptions
- subscriptionTabActive: true
- user:user
- saved_billing_details: req.query.saved_billing_details?
+ user = AuthenticationController.getSessionUser(req)
+ LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
+ return next(err) if err?
+ groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user)
+ if subscription?.customAccount
+ logger.log user: user, "redirecting to custom account page"
+ res.redirect "/user/subscription/custom_account"
+ else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember
+ logger.log user:user, "redirecting to group subscription invite page"
+ res.redirect groupLicenceInviteUrl
+ else if !hasSubOrIsGroupMember
+ logger.log user: user, "redirecting to plans"
+ res.redirect "/user/subscription/plans"
+ else
+ SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
+ return next(error) if error?
+ logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard"
+ plans = SubscriptionViewModelBuilder.buildViewModel()
+ res.render "subscriptions/dashboard",
+ title: "your_subscription"
+ recomendedCurrency: subscription?.currency
+ taxRate:subscription?.taxRate
+ plans: plans
+ subscription: subscription || {}
+ groupSubscriptions: groupSubscriptions
+ subscriptionTabActive: true
+ user:user
+ saved_billing_details: req.query.saved_billing_details?
userCustomSubscriptionPage: (req, res, next)->
- AuthenticationController.getLoggedInUser req, (error, user) ->
- return next(error) if error?
- LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
- return next(err) if err?
- if !subscription?
- err = new Error("subscription null for custom account, user:#{user?._id}")
- logger.warn err:err, "subscription is null for custom accounts page"
- return next(err)
- res.render "subscriptions/custom_account",
- title: "your_subscription"
- subscription: subscription
+ user = AuthenticationController.getSessionUser(req)
+ LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
+ return next(err) if err?
+ if !subscription?
+ err = new Error("subscription null for custom account, user:#{user?._id}")
+ logger.warn err:err, "subscription is null for custom accounts page"
+ return next(err)
+ res.render "subscriptions/custom_account",
+ title: "your_subscription"
+ subscription: subscription
editBillingDetailsPage: (req, res, next) ->
- AuthenticationController.getLoggedInUser req, (error, user) ->
- return next(error) if error?
- LimitationsManager.userHasSubscription user, (err, hasSubscription)->
- return next(err) if err?
- if !hasSubscription
- res.redirect "/user/subscription"
- else
- RecurlyWrapper.sign {
- account_code: user._id
- }, (error, signature) ->
- return next(error) if error?
- res.render "subscriptions/edit-billing-details",
- title : "update_billing_details"
- recurlyConfig: JSON.stringify
- currency: "USD"
- subdomain: Settings.apis.recurly.subdomain
- signature : signature
- successURL : "#{Settings.siteUrl}/user/subscription/billing-details/update"
- user :
- id : user._id
+ user = AuthenticationController.getSessionUser(req)
+ LimitationsManager.userHasSubscription user, (err, hasSubscription)->
+ return next(err) if err?
+ if !hasSubscription
+ res.redirect "/user/subscription"
+ else
+ RecurlyWrapper.sign {
+ account_code: user._id
+ }, (error, signature) ->
+ return next(error) if error?
+ res.render "subscriptions/edit-billing-details",
+ title : "update_billing_details"
+ recurlyConfig: JSON.stringify
+ currency: "USD"
+ subdomain: Settings.apis.recurly.subdomain
+ signature : signature
+ successURL : "#{Settings.siteUrl}/user/subscription/billing-details/update"
+ user :
+ id : user._id
updateBillingDetails: (req, res, next) ->
res.redirect "/user/subscription?saved_billing_details=true"
createSubscription: (req, res, next)->
- AuthenticationController.getLoggedInUser req, (error, user) ->
- return callback(error) if error?
- recurly_token_id = req.body.recurly_token_id
- subscriptionDetails = req.body.subscriptionDetails
- logger.log recurly_token_id: recurly_token_id, user_id:user._id, subscriptionDetails:subscriptionDetails, "creating subscription"
- SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)->
- if err?
- logger.err err:err, user_id:user._id, "something went wrong creating subscription"
- return res.sendStatus 500
- res.sendStatus 201
+ user = AuthenticationController.getSessionUser(req)
+ recurly_token_id = req.body.recurly_token_id
+ subscriptionDetails = req.body.subscriptionDetails
+ logger.log recurly_token_id: recurly_token_id, user_id:user._id, subscriptionDetails:subscriptionDetails, "creating subscription"
+ SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)->
+ if err?
+ logger.err err:err, user_id:user._id, "something went wrong creating subscription"
+ return res.sendStatus 500
+ res.sendStatus 201
successful_subscription: (req, res, next)->
- AuthenticationController.getLoggedInUser req, (error, user) =>
+ user = AuthenticationController.getSessionUser(req)
+ SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
return next(error) if error?
- SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
- return next(error) if error?
- res.render "subscriptions/successful_subscription",
- title: "thank_you"
- subscription:subscription
+ res.render "subscriptions/successful_subscription",
+ title: "thank_you"
+ subscription:subscription
cancelSubscription: (req, res, next) ->
- AuthenticationController.getLoggedInUser req, (error, user) ->
- return next(error) if error?
- logger.log user_id:user._id, "canceling subscription"
- SubscriptionHandler.cancelSubscription user, (err)->
- if err?
- logger.err err:err, user_id:user._id, "something went wrong canceling subscription"
- return next(err)
- res.redirect "/user/subscription"
+ user = AuthenticationController.getSessionUser(req)
+ logger.log user_id:user._id, "canceling subscription"
+ SubscriptionHandler.cancelSubscription user, (err)->
+ if err?
+ logger.err err:err, user_id:user._id, "something went wrong canceling subscription"
+ return next(err)
+ res.redirect "/user/subscription"
updateSubscription: (req, res, next)->
_origin = req?.query?.origin || null
- AuthenticationController.getLoggedInUser req, (error, user) ->
- return next(error) if error?
- planCode = req.body.plan_code
- if !planCode?
- err = new Error('plan_code is not defined')
- logger.err {user_id: user._id, err, planCode, origin: _origin, body: req.body}, "[Subscription] error in updateSubscription form"
+ user = AuthenticationController.getSessionUser(req)
+ planCode = req.body.plan_code
+ if !planCode?
+ err = new Error('plan_code is not defined')
+ logger.err {user_id: user._id, err, planCode, origin: _origin, body: req.body}, "[Subscription] error in updateSubscription form"
+ return next(err)
+ logger.log planCode: planCode, user_id:user._id, "updating subscription"
+ SubscriptionHandler.updateSubscription user, planCode, null, (err)->
+ if err?
+ logger.err err:err, user_id:user._id, "something went wrong updating subscription"
return next(err)
- logger.log planCode: planCode, user_id:user._id, "updating subscription"
- SubscriptionHandler.updateSubscription user, planCode, null, (err)->
- if err?
- logger.err err:err, user_id:user._id, "something went wrong updating subscription"
- return next(err)
- res.redirect "/user/subscription"
+ res.redirect "/user/subscription"
reactivateSubscription: (req, res, next)->
- AuthenticationController.getLoggedInUser req, (error, user) ->
- return next(error) if error?
- logger.log user_id:user._id, "reactivating subscription"
- SubscriptionHandler.reactivateSubscription user, (err)->
- if err?
- logger.err err:err, user_id:user._id, "something went wrong reactivating subscription"
- return next(err)
- res.redirect "/user/subscription"
+ user = AuthenticationController.getSessionUser(req)
+ logger.log user_id:user._id, "reactivating subscription"
+ SubscriptionHandler.reactivateSubscription user, (err)->
+ if err?
+ logger.err err:err, user_id:user._id, "something went wrong reactivating subscription"
+ return next(err)
+ res.redirect "/user/subscription"
recurlyCallback: (req, res, next)->
logger.log data: req.body, "received recurly callback"
@@ -207,47 +198,44 @@ module.exports = SubscriptionController =
res.sendStatus 200
renderUpgradeToAnnualPlanPage: (req, res, next)->
- AuthenticationController.getLoggedInUser req, (error, user) ->
- return next(error) if error?
- LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
- return next(err) if err?
- planCode = subscription?.planCode.toLowerCase()
- if planCode?.indexOf("annual") != -1
- planName = "annual"
- else if planCode?.indexOf("student") != -1
- planName = "student"
- else if planCode?.indexOf("collaborator") != -1
- planName = "collaborator"
- if !hasSubscription
- return res.redirect("/user/subscription/plans")
- logger.log planName:planName, user_id:user._id, "rendering upgrade to annual page"
- res.render "subscriptions/upgradeToAnnual",
- title: "Upgrade to annual"
- planName: planName
+ user = AuthenticationController.getSessionUser(req)
+ LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
+ return next(err) if err?
+ planCode = subscription?.planCode.toLowerCase()
+ if planCode?.indexOf("annual") != -1
+ planName = "annual"
+ else if planCode?.indexOf("student") != -1
+ planName = "student"
+ else if planCode?.indexOf("collaborator") != -1
+ planName = "collaborator"
+ if !hasSubscription
+ return res.redirect("/user/subscription/plans")
+ logger.log planName:planName, user_id:user._id, "rendering upgrade to annual page"
+ res.render "subscriptions/upgradeToAnnual",
+ title: "Upgrade to annual"
+ planName: planName
processUpgradeToAnnualPlan: (req, res, next)->
- AuthenticationController.getLoggedInUser req, (error, user) ->
- return next(error) if error?
- {planName} = req.body
- coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName]
- annualPlanName = "#{planName}-annual"
- logger.log user_id:user._id, planName:annualPlanName, "user is upgrading to annual billing with discount"
- SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)->
- if err?
- logger.err err:err, user_id:user._id, "error updating subscription"
- return next(err)
- res.sendStatus 200
+ user = AuthenticationController.getSessionUser(req)
+ {planName} = req.body
+ coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName]
+ annualPlanName = "#{planName}-annual"
+ logger.log user_id:user._id, planName:annualPlanName, "user is upgrading to annual billing with discount"
+ SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)->
+ if err?
+ logger.err err:err, user_id:user._id, "error updating subscription"
+ return next(err)
+ res.sendStatus 200
extendTrial: (req, res, next)->
- AuthenticationController.getLoggedInUser req, (error, user) ->
- return next(error) if error?
- LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
- return next(err) if err?
- SubscriptionHandler.extendTrial subscription, 14, (err)->
- if err?
- res.send 500
- else
- res.send 200
+ user = AuthenticationController.getSessionUser(req)
+ LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
+ return next(err) if err?
+ SubscriptionHandler.extendTrial subscription, 14, (err)->
+ if err?
+ res.send 500
+ else
+ res.send 200
recurlyNotificationParser: (req, res, next) ->
xml = ""
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee
index c09ed1f47d..91de537058 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee
@@ -78,18 +78,18 @@ module.exports =
beginJoinGroup: (req, res)->
subscription_id = req.params.subscription_id
- AuthenticationController.getLoggedInUser req, (err, currentUser) ->
+ currentUser = AuthenticationController.getSessionUser(req)
+ if !currentUser?
+ logger.err {subscription_id}, "error getting current user"
+ return res.sendStatus 500
+ licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)
+ if !licence?
+ return ErrorsController.notFound(req, res)
+ SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, currentUser.email, (err)->
if err?
- logger.err {subscription_id}, "error getting current user"
- return res.sendStatus 500
- licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)
- if !licence?
- return ErrorsController.notFound(req, res)
- SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, currentUser.email, (err)->
- if err?
- res.sendStatus 500
- else
- res.sendStatus 200
+ res.sendStatus 500
+ else
+ res.sendStatus 200
completeJoin: (req, res)->
subscription_id = req.params.subscription_id
diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee
index ebbebd9c0a..17f3812584 100644
--- a/services/web/app/coffee/Features/User/UserController.coffee
+++ b/services/web/app/coffee/Features/User/UserController.coffee
@@ -85,13 +85,13 @@ module.exports = UserController =
metrics.inc "user.logout"
logger.log user: req?.session?.user, "logging out"
sessionId = req.sessionID
- AuthenticationController.getLoggedInUser req, (err, user) ->
- req.logout?() # passport logout
- req.session.destroy (err)->
- if err
- logger.err err: err, 'error destorying session'
- UserSessionsManager.untrackSession(user, sessionId)
- res.redirect '/login'
+ user = AuthenticationController.getSessionUser(req)
+ req.logout?() # passport logout
+ req.session.destroy (err)->
+ if err
+ logger.err err: err, 'error destorying session'
+ UserSessionsManager.untrackSession(user, sessionId)
+ res.redirect '/login'
register : (req, res, next = (error) ->)->
email = req.body.email
From 8e0103a1bcf54c83904f5339bd6fc9e4f07fceda Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 7 Sep 2016 14:05:51 +0100
Subject: [PATCH 251/378] wip: fix unit tests for AuthenticationController
---
.../AuthenticationController.coffee | 13 +-
.../AuthenticationControllerTests.coffee | 136 ++++++++----------
2 files changed, 68 insertions(+), 81 deletions(-)
diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index 161b0f2078..8d12acfb64 100644
--- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -62,24 +62,21 @@ module.exports = AuthenticationController =
isUserLoggedIn: (req) ->
user_id = AuthenticationController.getLoggedInUserId(req)
- return user_id?
+ return user_id != null
# TODO: perhaps should produce an error if the current user is not present
getLoggedInUserId: (req) ->
user = AuthenticationController.getSessionUser(req)
- if user?
+ if user
return user._id
else
return null
getSessionUser: (req) ->
- # old sessions
- if req?.session?.user?._id?
+ if req?.session?.user?
return req.session.user
- # new passport sessions
- else if req?.session?.passport?.user?._id?
+ else if req?.session?.passport?.user
return req.session.passport.user
- # neither
else
return null
@@ -110,7 +107,7 @@ module.exports = AuthenticationController =
if req.headers['authorization']?
return AuthenticationController.httpAuth(req, res, next)
- else if AuthenticationController.isUserLoggedIn()?
+ else if AuthenticationController.isUserLoggedIn(req)
return next()
else
logger.log url:req.url, "user trying to access endpoint not in global whitelist"
diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
index 86c5e5840f..3e0812d3ff 100644
--- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
@@ -39,35 +39,52 @@ describe "AuthenticationController", ->
@callback = @next = sinon.stub()
tk.freeze(Date.now())
-
afterEach ->
tk.reset()
- describe "login", ->
+ describe 'getSessionUser', ->
+
+ it 'should get the user object from session', ->
+ @req.session =
+ passport:
+ user: {_id: 'one'}
+ user = @AuthenticationController.getSessionUser(@req)
+ expect(user).to.deep.equal {_id: 'one'}
+
+ it 'should work with legacy sessions', ->
+ @req.session =
+ user: {_id: 'one'}
+ user = @AuthenticationController.getSessionUser(@req)
+ expect(user).to.deep.equal {_id: 'one'}
+
+ describe "doPassportLogin", ->
beforeEach ->
@AuthenticationController._recordFailedLogin = sinon.stub()
@AuthenticationController._recordSuccessfulLogin = sinon.stub()
- @AuthenticationController.establishUserSession = sinon.stub().callsArg(2)
+ # @AuthenticationController.establishUserSession = sinon.stub().callsArg(2)
@req.body =
email: @email
password: @password
redir: @redir = "/path/to/redir/to"
+ @cb = sinon.stub()
describe "when the users rate limit", ->
- it "should block the request if the limit has been exceeded", (done)->
+ beforeEach ->
@LoginRateLimiter.processLoginRequest.callsArgWith(1, null, false)
- @res =
- send: (code)=>
- @res.statusCode.should.equal 429
- done()
- @AuthenticationController.login(@req, @res)
+
+ it "should block the request if the limit has been exceeded", (done)->
+ @AuthenticationController.doPassportLogin(@req, @req.body.email, @req.body.password, @cb)
+ @cb.callCount.should.equal 1
+ @cb.calledWith(null, null).should.equal true
+ done()
describe 'when the user is authenticated', ->
beforeEach ->
+ @cb = sinon.stub()
@LoginRateLimiter.processLoginRequest.callsArgWith(1, null, true)
@AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user)
- @AuthenticationController.login(@req, @res)
+ @AuthenticationController.doPassportLogin(@req, @req.body.email, @req.body.password, @cb)
it "should attempt to authorise the user", ->
@AuthenticationManager.authenticate
@@ -78,15 +95,13 @@ describe "AuthenticationController", ->
@UserHandler.setupLoginData.calledWith(@user).should.equal true
it "should establish the user's session", ->
- @AuthenticationController.establishUserSession
- .calledWith(@req, @user)
- .should.equal true
+ @cb.calledWith(null, @user).should.equal true
it "should set res.session.justLoggedIn", ->
@req.session.justLoggedIn.should.equal true
it "should redirect the user to the specified location", ->
- expect(@res.body).to.deep.equal redir: @redir
+ expect(@req._redir).to.deep.equal @redir
it "should record the successful login", ->
@AuthenticationController._recordSuccessfulLogin
@@ -106,23 +121,22 @@ describe "AuthenticationController", ->
.calledWith(@user._id, "user-logged-in")
.should.equal true
-
describe 'when the user is not authenticated', ->
beforeEach ->
@LoginRateLimiter.processLoginRequest.callsArgWith(1, null, true)
@AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, null)
- @AuthenticationController.login(@req, @res)
+ @cb = sinon.stub()
+ @AuthenticationController.doPassportLogin(@req, @req.body.email, @req.body.password, @cb)
- it "should return an error", ->
+ it "should not establish the login", ->
+ @cb.callCount.should.equal 1
+ @cb.calledWith(null, false)
# @res.body.should.exist
- expect(@res.body.message).to.exist
+ expect(@cb.lastCall.args[2]).to.contain.all.keys ['message']
# message:
# text: 'Your email or password were incorrect. Please try again',
# type: 'error'
- it "should not establish a session", ->
- @AuthenticationController.establishUserSession.called.should.equal false
-
it "should not setup the user data in the background", ->
@UserHandler.setupLoginData.called.should.equal false
@@ -139,10 +153,11 @@ describe "AuthenticationController", ->
@LoginRateLimiter.processLoginRequest.callsArgWith(1, null, true)
@req.body.redir = "http://www.facebook.com/test"
@AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user)
- @AuthenticationController.login(@req, @res)
+ @cb = sinon.stub()
+ @AuthenticationController.doPassportLogin(@req, @req.body.email, @req.body.password, @cb)
it "should only redirect to the local path", ->
- expect(@res.body).to.deep.equal redir: "/test"
+ expect(@req._redir).to.equal "/test"
describe "getLoggedInUserId", ->
@@ -150,34 +165,42 @@ describe "AuthenticationController", ->
@req =
session :{}
- it "should return the user id from the session", (done)->
+ it "should return the user id from the session", ()->
@user_id = "2134"
@req.session.user =
_id:@user_id
- @AuthenticationController.getLoggedInUserId @req, (err, user_id)=>
- expect(user_id).to.equal @user_id
- done()
+ result = @AuthenticationController.getLoggedInUserId @req
+ expect(result).to.equal @user_id
- it "should return null if there is no user on the session", (done)->
- @AuthenticationController.getLoggedInUserId @req, (err, user_id)=>
- expect(user_id).to.be.null
- done()
+ it 'should return user for passport session', () ->
+ @user_id = "2134"
+ @req.session = {
+ passport: {
+ user: {
+ _id:@user_id
+ }
+ }
+ }
+ result = @AuthenticationController.getLoggedInUserId @req
+ expect(result).to.equal @user_id
- it "should return null if there is no session", (done)->
+ it "should return null if there is no user on the session", ()->
+ result = @AuthenticationController.getLoggedInUserId @req
+ expect(result).to.equal null
+
+ it "should return null if there is no session", ()->
@req = {}
- @AuthenticationController.getLoggedInUserId @req, (err, user_id)=>
- expect(user_id).to.be.null
- done()
+ result = @AuthenticationController.getLoggedInUserId @req
+ expect(result).to.equal null
- it "should return null if there is no req", (done)->
+ it "should return null if there is no req", ()->
@req = {}
- @AuthenticationController.getLoggedInUserId @req, (err, user_id)=>
- expect(user_id).to.be.null
- done()
+ result = @AuthenticationController.getLoggedInUserId @req
+ expect(result).to.equal null
describe "getLoggedInUser", ->
beforeEach ->
- @UserGetter.getUser = sinon.stub().callsArgWith(1, null, @user)
+ @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
describe "with an established session", ->
beforeEach ->
@@ -209,9 +232,6 @@ describe "AuthenticationController", ->
}
@middleware(@req, @res, @next)
- it "should set the user property on the request", ->
- @req.user.should.deep.equal @user
-
it "should call the next method in the chain", ->
@next.called.should.equal true
@@ -367,33 +387,3 @@ describe "AuthenticationController", ->
it "should call the callback", ->
@callback.called.should.equal true
-
- describe "establishUserSession", ->
- beforeEach ->
- @req.session =
- save: sinon.stub().callsArg(0)
- destroy : sinon.stub()
- @req.sessionStore =
- generate: sinon.stub()
- @req.ip = "1.2.3.4"
- @AuthenticationController.establishUserSession @req, @user, @callback
-
- it "should set the session user to a basic version of the user", ->
- @req.session.user._id.should.equal @user._id
- @req.session.user.email.should.equal @user.email
- @req.session.user.first_name.should.equal @user.first_name
- @req.session.user.last_name.should.equal @user.last_name
- @req.session.user.referal_id.should.equal @user.referal_id
- @req.session.user.isAdmin.should.equal @user.isAdmin
- @req.session.user.ip_address.should.equal @req.ip
- expect(typeof @req.session.user.ip_address).to.equal 'string'
- expect(typeof @req.session.user.session_created).to.equal 'string'
-
- it "should destroy the session", ->
- @req.session.destroy.called.should.equal true
-
- it "should regenerate the session to protect against session fixation", ->
- @req.sessionStore.generate.calledWith(@req).should.equal true
-
- it "should return the callback", ->
- @callback.called.should.equal true
From 438ac45854e1c3e699be22b2865287bb3c740b85 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Wed, 7 Sep 2016 16:40:49 +0100
Subject: [PATCH 252/378] fix unit tests
---
.../AuthorizationMiddlewear.coffee | 2 +-
.../Features/Project/ProjectController.coffee | 4 +-
.../Features/Referal/ReferalMiddleware.coffee | 2 +-
.../StaticPages/HomeController.coffee | 2 +-
.../SubscriptionController.coffee | 2 +-
.../AuthorizationMiddlewearTests.coffee | 69 ++++++++++---------
.../coffee/Chat/ChatControllerTests.coffee | 10 ++-
.../CollaboratorsInviteControllerTests.coffee | 5 ++
.../Compile/CompileControllerTests.coffee | 33 +++++----
.../Contact/ContactControllerTests.coffee | 17 +++--
.../NotificationsControllerTests.coffee | 19 ++---
.../Project/ProjectControllerTests.coffee | 43 +++++++-----
.../RateLimiterMiddlewearTests.coffee | 35 +++++-----
.../SubscriptionControllerTests.coffee | 42 +++++------
.../SubscriptionGroupControllerTests.coffee | 36 +++++-----
.../coffee/Tags/TagsControllerTests.coffee | 41 ++++++-----
.../TrackChangesControllerTests.coffee | 8 +--
.../ProjectUploadControllerTests.coffee | 9 ++-
.../coffee/User/UserControllerTests.coffee | 22 +++---
.../User/UserPagesControllerTests.coffee | 19 ++---
20 files changed, 237 insertions(+), 183 deletions(-)
diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee
index f321b015f6..96fcc5c583 100644
--- a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee
+++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee
@@ -100,7 +100,7 @@ module.exports = AuthorizationMiddlewear =
res.redirect "/restricted"
restricted : (req, res, next)->
- if AuthenticationController.isUserLoggedIn()?
+ if AuthenticationController.isUserLoggedIn(req)
res.render 'user/restricted',
title:'restricted'
else
diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee
index 28af4fbf8d..8c07c8a77f 100644
--- a/services/web/app/coffee/Features/Project/ProjectController.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectController.coffee
@@ -89,7 +89,7 @@ module.exports = ProjectController =
project_id = req.params.Project_id
projectName = req.body.projectName
logger.log project_id:project_id, projectName:projectName, "cloning project"
- if !AuthenticationController.isUserLoggedIn()?
+ if !AuthenticationController.isUserLoggedIn()
return res.send redir:"/register"
currentUser = AuthenticationController.getSessionUser(req)
projectDuplicator.duplicate currentUser, project_id, projectName, (err, project)->
@@ -186,7 +186,7 @@ module.exports = ProjectController =
if !Settings.editorIsOpen
return res.render("general/closed", {title:"updating_site"})
- if AuthenticationController.isUserLoggedIn(req)?
+ if AuthenticationController.isUserLoggedIn(req)
user_id = AuthenticationController.getLoggedInUserId(req)
anonymous = false
else
diff --git a/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee b/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee
index ccfe7a802f..77cbef5254 100644
--- a/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee
+++ b/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee
@@ -3,7 +3,7 @@ AuthenticationController = require('../Authentication/AuthenticationController')
module.exports = RefererMiddleware =
getUserReferalId: (req, res, next) ->
- if AuthenticationController.isUserLoggedIn()?
+ if AuthenticationController.isUserLoggedIn()
user = AuthenticationController.getSessionUser(req)
req.user.referal_id = user.referal_id
next()
diff --git a/services/web/app/coffee/Features/StaticPages/HomeController.coffee b/services/web/app/coffee/Features/StaticPages/HomeController.coffee
index 3535c6c5eb..6675d55333 100755
--- a/services/web/app/coffee/Features/StaticPages/HomeController.coffee
+++ b/services/web/app/coffee/Features/StaticPages/HomeController.coffee
@@ -11,7 +11,7 @@ homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/externa
module.exports = HomeController =
index : (req,res)->
- if AuthenticationController.isUserLoggedIn(req)?
+ if AuthenticationController.isUserLoggedIn(req)
if req.query.scribtex_path?
res.redirect "/project?scribtex_path=#{req.query.scribtex_path}"
else
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
index e9ccefe2c8..19ade80488 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
@@ -13,7 +13,7 @@ module.exports = SubscriptionController =
plansPage: (req, res, next) ->
plans = SubscriptionViewModelBuilder.buildViewModel()
- if AuthenticationController.isUserLoggedIn(req)?
+ if AuthenticationController.isUserLoggedIn(req)
baseUrl = "/register?redir="
else
baseUrl = ""
diff --git a/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee b/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee
index bc62e603de..8631938807 100644
--- a/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee
+++ b/services/web/test/UnitTests/coffee/Authorization/AuthorizationMiddlewearTests.coffee
@@ -8,19 +8,23 @@ Errors = require "../../../../app/js/Features/Errors/Errors.js"
describe "AuthorizationMiddlewear", ->
beforeEach ->
+ @user_id = "user-id-123"
+ @project_id = "project-id-123"
+ @AuthenticationController =
+ getLoggedInUserId: sinon.stub().returns(@user_id)
+ isUserLoggedIn: sinon.stub().returns(true)
@AuthorizationMiddlewear = SandboxedModule.require modulePath, requires:
"./AuthorizationManager": @AuthorizationManager = {}
"logger-sharelatex": {log: () ->}
"mongojs": ObjectId: @ObjectId = {}
"../Errors/Errors": Errors
- @user_id = "user-id-123"
- @project_id = "project-id-123"
+ '../Authentication/AuthenticationController': @AuthenticationController
@req = {}
@res = {}
@ObjectId.isValid = sinon.stub()
@ObjectId.isValid.withArgs(@project_id).returns true
@next = sinon.stub()
-
+
METHODS_TO_TEST = {
"ensureUserCanReadProject": "canUserReadProject"
"ensureUserCanWriteProjectSettings": "canUserWriteProjectSettings"
@@ -35,26 +39,25 @@ describe "AuthorizationMiddlewear", ->
project_id: @project_id
@AuthorizationManager[managerMethod] = sinon.stub()
@AuthorizationMiddlewear.redirectToRestricted = sinon.stub()
-
+
describe "with missing project_id", ->
beforeEach ->
@req.params = {}
-
+
it "should return an error to next", ->
@AuthorizationMiddlewear[middlewearMethod] @req, @res, @next
@next.calledWith(new Error()).should.equal true
describe "with logged in user", ->
beforeEach ->
- @req.session =
- user: _id: @user_id
+ @AuthenticationController.getLoggedInUserId.returns(@user_id)
describe "when user has permission", ->
beforeEach ->
@AuthorizationManager[managerMethod]
.withArgs(@user_id, @project_id)
.yields(null, true)
-
+
it "should return next", ->
@AuthorizationMiddlewear[middlewearMethod] @req, @res, @next
@next.called.should.equal true
@@ -64,49 +67,51 @@ describe "AuthorizationMiddlewear", ->
@AuthorizationManager[managerMethod]
.withArgs(@user_id, @project_id)
.yields(null, false)
-
+
it "should redirect to redirectToRestricted", ->
@AuthorizationMiddlewear[middlewearMethod] @req, @res, @next
@next.called.should.equal false
@AuthorizationMiddlewear.redirectToRestricted
.calledWith(@req, @res, @next)
.should.equal true
-
+
describe "with anonymous user", ->
describe "when user has permission", ->
beforeEach ->
+ @AuthenticationController.getLoggedInUserId.returns(null)
@AuthorizationManager[managerMethod]
.withArgs(null, @project_id)
.yields(null, true)
-
+
it "should return next", ->
@AuthorizationMiddlewear[middlewearMethod] @req, @res, @next
@next.called.should.equal true
describe "when user doesn't have permission", ->
beforeEach ->
+ @AuthenticationController.getLoggedInUserId.returns(null)
@AuthorizationManager[managerMethod]
.withArgs(null, @project_id)
.yields(null, false)
-
+
it "should redirect to redirectToRestricted", ->
@AuthorizationMiddlewear[middlewearMethod] @req, @res, @next
@next.called.should.equal false
@AuthorizationMiddlewear.redirectToRestricted
.calledWith(@req, @res, @next)
.should.equal true
-
+
describe "with malformed project id", ->
beforeEach ->
@req.params =
project_id: "blah"
@ObjectId.isValid = sinon.stub().returns false
-
+
it "should return a not found error", (done) ->
@AuthorizationMiddlewear[middlewearMethod] @req, @res, (error) ->
error.should.be.instanceof Errors.NotFoundError
done()
-
+
describe "ensureUserIsSiteAdmin", ->
beforeEach ->
@AuthorizationManager.isUserSiteAdmin = sinon.stub()
@@ -114,15 +119,14 @@ describe "AuthorizationMiddlewear", ->
describe "with logged in user", ->
beforeEach ->
- @req.session =
- user: _id: @user_id
+ @AuthenticationController.getLoggedInUserId.returns(@user_id)
describe "when user has permission", ->
beforeEach ->
@AuthorizationManager.isUserSiteAdmin
.withArgs(@user_id)
.yields(null, true)
-
+
it "should return next", ->
@AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next
@next.called.should.equal true
@@ -132,49 +136,50 @@ describe "AuthorizationMiddlewear", ->
@AuthorizationManager.isUserSiteAdmin
.withArgs(@user_id)
.yields(null, false)
-
+
it "should redirect to redirectToRestricted", ->
@AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next
@next.called.should.equal false
@AuthorizationMiddlewear.redirectToRestricted
.calledWith(@req, @res, @next)
.should.equal true
-
+
describe "with anonymous user", ->
describe "when user has permission", ->
beforeEach ->
+ @AuthenticationController.getLoggedInUserId.returns(null)
@AuthorizationManager.isUserSiteAdmin
.withArgs(null)
.yields(null, true)
-
+
it "should return next", ->
@AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next
@next.called.should.equal true
describe "when user doesn't have permission", ->
beforeEach ->
+ @AuthenticationController.getLoggedInUserId.returns(null)
@AuthorizationManager.isUserSiteAdmin
.withArgs(null)
.yields(null, false)
-
+
it "should redirect to redirectToRestricted", ->
@AuthorizationMiddlewear.ensureUserIsSiteAdmin @req, @res, @next
@next.called.should.equal false
@AuthorizationMiddlewear.redirectToRestricted
.calledWith(@req, @res, @next)
.should.equal true
-
+
describe "ensureUserCanReadMultipleProjects", ->
beforeEach ->
@AuthorizationManager.canUserReadProject = sinon.stub()
@AuthorizationMiddlewear.redirectToRestricted = sinon.stub()
@req.query =
project_ids: "project1,project2"
-
+
describe "with logged in user", ->
beforeEach ->
- @req.session =
- user: _id: @user_id
+ @AuthenticationController.getLoggedInUserId.returns(@user_id)
describe "when user has permission to access all projects", ->
beforeEach ->
@@ -184,7 +189,7 @@ describe "AuthorizationMiddlewear", ->
@AuthorizationManager.canUserReadProject
.withArgs(@user_id, "project2")
.yields(null, true)
-
+
it "should return next", ->
@AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next
@next.called.should.equal true
@@ -197,38 +202,40 @@ describe "AuthorizationMiddlewear", ->
@AuthorizationManager.canUserReadProject
.withArgs(@user_id, "project2")
.yields(null, false)
-
+
it "should redirect to redirectToRestricted", ->
@AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next
@next.called.should.equal false
@AuthorizationMiddlewear.redirectToRestricted
.calledWith(@req, @res, @next)
.should.equal true
-
+
describe "with anonymous user", ->
describe "when user has permission", ->
describe "when user has permission to access all projects", ->
beforeEach ->
+ @AuthenticationController.getLoggedInUserId.returns(null)
@AuthorizationManager.canUserReadProject
.withArgs(null, "project1")
.yields(null, true)
@AuthorizationManager.canUserReadProject
.withArgs(null, "project2")
.yields(null, true)
-
+
it "should return next", ->
@AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next
@next.called.should.equal true
describe "when user doesn't have permission to access one of the projects", ->
beforeEach ->
+ @AuthenticationController.getLoggedInUserId.returns(null)
@AuthorizationManager.canUserReadProject
.withArgs(null, "project1")
.yields(null, true)
@AuthorizationManager.canUserReadProject
.withArgs(null, "project2")
.yields(null, false)
-
+
it "should redirect to redirectToRestricted", ->
@AuthorizationMiddlewear.ensureUserCanReadMultipleProjects @req, @res, @next
@next.called.should.equal false
diff --git a/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee b/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee
index d2ebe41c63..a491e4b499 100644
--- a/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee
@@ -10,19 +10,24 @@ describe "ChatController", ->
beforeEach ->
+ @user_id = 'ier_'
@settings = {}
- @ChatHandler =
+ @ChatHandler =
sendMessage:sinon.stub()
getMessages:sinon.stub()
@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
- @query =
+ '../Authentication/AuthenticationController': @AuthenticationController
+ @query =
before:"some time"
@req =
@@ -74,4 +79,3 @@ describe "ChatController", ->
sentMessages.should.deep.equal messages
done()
@ChatController.getMessages @req, @res
-
diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
index 0144583f05..96f25906e9 100644
--- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee
@@ -11,7 +11,11 @@ ObjectId = require("mongojs").ObjectId
describe "CollaboratorsInviteController", ->
beforeEach ->
+ @user =
+ _id: 'id'
@AnalyticsManger = recordEvent: sinon.stub()
+ @AuthenticationController =
+ getSessionUser: (req) => req.session.user
@CollaboratorsInviteController = SandboxedModule.require modulePath, requires:
"../Project/ProjectGetter": @ProjectGetter = {}
'../Subscription/LimitationsManager' : @LimitationsManager = {}
@@ -22,6 +26,7 @@ describe "CollaboratorsInviteController", ->
"../Editor/EditorRealTimeController": @EditorRealTimeController = {emitToRoom: sinon.stub()}
"../Notifications/NotificationsBuilder": @NotificationsBuilder = {}
"../Analytics/AnalyticsManager": @AnalyticsManger
+ '../Authentication/AuthenticationController': @AuthenticationController
@res = new MockResponse()
@req = new MockRequest()
diff --git a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee b/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee
index d7a13578e2..9874b77265 100644
--- a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee
@@ -10,10 +10,17 @@ MockResponse = require "../helpers/MockResponse"
describe "CompileController", ->
beforeEach ->
- @CompileManager =
+ @user_id = 'wat'
+ @user =
+ _id: @user_id
+ email: 'user@example.com'
+ features:
+ compileGroup: "premium"
+ compileTimeout: 100
+ @CompileManager =
compile: sinon.stub()
@ClsiManager = {}
- @UserGetter =
+ @UserGetter =
getUser:sinon.stub()
@RateLimiter = {addCount:sinon.stub()}
@settings =
@@ -23,8 +30,13 @@ describe "CompileController", ->
clsi_priority:
url: "clsi-priority.example.com"
@jar = {cookie:"stuff"}
- @ClsiCookieManager =
+ @ClsiCookieManager =
getCookieJar:sinon.stub().callsArgWith(1, null, @jar)
+ @AuthenticationController =
+ getLoggedInUser: sinon.stub().callsArgWith(1, null, @user)
+ getLoggedInUserId: sinon.stub().returns(@user_id)
+ getSessionUser: sinon.stub().returns(@user)
+ isUserLoggedIn: sinon.stub().returns(true)
@CompileController = SandboxedModule.require modulePath, requires:
"settings-sharelatex": @settings
"request": @request = sinon.stub()
@@ -34,18 +46,13 @@ describe "CompileController", ->
"./CompileManager":@CompileManager
"../User/UserGetter":@UserGetter
"./ClsiManager": @ClsiManager
- "../Authentication/AuthenticationController": @AuthenticationController = {}
+ "../Authentication/AuthenticationController": @AuthenticationController
"../../infrastructure/RateLimiter":@RateLimiter
"./ClsiCookieManager":@ClsiCookieManager
@project_id = "project-id"
- @user =
- features:
- compileGroup: "premium"
- compileTimeout: 100
@next = sinon.stub()
@req = new MockRequest()
@res = new MockResponse()
- @AuthenticationController.getLoggedInUserId = sinon.stub().callsArgWith(1, null, @user_id = "mock-user-id")
describe "compile", ->
beforeEach ->
@@ -90,7 +97,7 @@ describe "CompileController", ->
@CompileManager.compile
.calledWith(@project_id, @user_id, { isAutoCompile: true })
.should.equal true
-
+
describe "with the draft attribute", ->
beforeEach ->
@req.body =
@@ -108,7 +115,7 @@ describe "CompileController", ->
Project_id: @project_id
@project =
getSafeProjectName: () => @safe_name = "safe-name"
-
+
@req.query = {pdfng:true}
@Project.findById = sinon.stub().callsArgWith(2, null, @project)
@@ -340,9 +347,9 @@ describe "CompileController", ->
project_id:@project_id
@CompileManager.compile.callsArgWith(3)
@CompileController.proxyToClsi = sinon.stub()
- @res =
+ @res =
send:=>
-
+
it "should call compile in the compile manager", (done)->
@CompileController.compileAndDownloadPdf @req, @res
@CompileManager.compile.calledWith(@project_id).should.equal true
diff --git a/services/web/test/UnitTests/coffee/Contact/ContactControllerTests.coffee b/services/web/test/UnitTests/coffee/Contact/ContactControllerTests.coffee
index aa01ff1a0f..0f6c6a0404 100644
--- a/services/web/test/UnitTests/coffee/Contact/ContactControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Contact/ContactControllerTests.coffee
@@ -8,12 +8,15 @@ SandboxedModule = require('sandboxed-module')
describe "ContactController", ->
beforeEach ->
+ @AuthenticationController =
+ getLoggedInUserId: sinon.stub()
@ContactController = SandboxedModule.require modulePath, requires:
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
"../User/UserGetter": @UserGetter = {}
"./ContactManager": @ContactManager = {}
"../Authentication/AuthenticationController": @AuthenticationController = {}
"../../infrastructure/Modules": @Modules = { hooks: {} }
+ '../Authentication/AuthenticationController': @AuthenticationController
@next = sinon.stub()
@req = {}
@@ -30,33 +33,33 @@ describe "ContactController", ->
{ _id: "contact-2", email: "jane@example.com", first_name: "Jane", last_name: "Example", unsued: "foo", holdingAccount: true }
{ _id: "contact-3", email: "jim@example.com", first_name: "Jim", last_name: "Example", unsued: "foo" }
]
- @AuthenticationController.getLoggedInUserId = sinon.stub().callsArgWith(1, null, @user_id)
+ @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@user_id)
@ContactManager.getContactIds = sinon.stub().callsArgWith(2, null, @contact_ids)
@UserGetter.getUsers = sinon.stub().callsArgWith(2, null, @contacts)
@Modules.hooks.fire = sinon.stub().callsArg(3)
-
+
@ContactController.getContacts @req, @res, @next
-
+
it "should look up the logged in user id", ->
@AuthenticationController.getLoggedInUserId
.calledWith(@req)
.should.equal true
-
+
it "should get the users contact ids", ->
@ContactManager.getContactIds
.calledWith(@user_id, { limit: 50 })
.should.equal true
-
+
it "should populate the users contacts ids", ->
@UserGetter.getUsers
.calledWith(@contact_ids, { email: 1, first_name: 1, last_name: 1, holdingAccount: 1 })
.should.equal true
-
+
it "should fire the getContact module hook", ->
@Modules.hooks.fire
.calledWith("getContacts", @user_id)
.should.equal true
-
+
it "should return a formatted list of contacts in contact list order, without holding accounts", ->
@res.send.args[0][0].contacts.should.deep.equal [
{ id: "contact-1", email: "joe@example.com", first_name: "Joe", last_name: "Example", type: "user" }
diff --git a/services/web/test/UnitTests/coffee/Notifications/NotificationsControllerTests.coffee b/services/web/test/UnitTests/coffee/Notifications/NotificationsControllerTests.coffee
index 083257e62c..126b223f04 100644
--- a/services/web/test/UnitTests/coffee/Notifications/NotificationsControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Notifications/NotificationsControllerTests.coffee
@@ -10,16 +10,9 @@ describe 'NotificationsController', ->
notification_id = "123njdskj9jlk"
beforeEach ->
- @handler =
+ @handler =
getUserNotifications: sinon.stub().callsArgWith(1)
markAsRead: sinon.stub().callsArgWith(2)
- @controller = SandboxedModule.require modulePath, requires:
- "./NotificationsHandler":@handler
- "underscore":@underscore =
- map:(arr)-> return arr
- 'logger-sharelatex':
- log:->
- err:->
@req =
params:
notification_id:notification_id
@@ -28,6 +21,16 @@ describe 'NotificationsController', ->
_id:user_id
i18n:
translate:->
+ @AuthenticationController =
+ getLoggedInUserId: sinon.stub().returns(@req.session.user._id)
+ @controller = SandboxedModule.require modulePath, requires:
+ "./NotificationsHandler":@handler
+ "underscore":@underscore =
+ map:(arr)-> return arr
+ 'logger-sharelatex':
+ log:->
+ err:->
+ '../Authentication/AuthenticationController': @AuthenticationController
it 'should ask the handler for all unread notifications', (done)->
allNotifications = [{_id: notification_id, user_id: user_id}]
diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee
index faa1456b51..bcb2354b55 100644
--- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee
@@ -12,12 +12,15 @@ describe "ProjectController", ->
@project_id = "123213jlkj9kdlsaj"
- @settings =
+ @user =
+ _id:"!£123213kjljkl"
+ first_name: "bjkdsjfk"
+ @settings =
apis:
chat:
url:"chat.com"
siteUrl: "mysite.com"
- @ProjectDeleter =
+ @ProjectDeleter =
archiveProject: sinon.stub().callsArg(1)
deleteProject: sinon.stub().callsArg(1)
restoreProject: sinon.stub().callsArg(1)
@@ -29,7 +32,7 @@ describe "ProjectController", ->
createBasicProject: sinon.stub().callsArgWith(2, null, {_id:@project_id})
@SubscriptionLocator =
getUsersSubscription: sinon.stub()
- @LimitationsManager =
+ @LimitationsManager =
userHasSubscriptionOrIsGroupMember: sinon.stub()
@TagsHandler =
getAllTags: sinon.stub()
@@ -39,7 +42,7 @@ describe "ProjectController", ->
findById: sinon.stub()
@AuthorizationManager =
getPrivilegeLevelForProject:sinon.stub()
- @EditorController =
+ @EditorController =
renameProject:sinon.stub()
@InactiveProjectManager =
reactivateProjectIfRequired:sinon.stub()
@@ -50,12 +53,17 @@ describe "ProjectController", ->
@ProjectGetter =
findAllUsersProjects: sinon.stub()
getProject: sinon.stub()
+ @AuthenticationController =
+ getLoggedInUser: sinon.stub().callsArgWith(1, null, @user)
+ getLoggedInUserId: sinon.stub().returns(@user._id)
+ getSessionUser: sinon.stub().returns(@user)
+ isUserLoggedIn: sinon.stub().returns(true)
@ProjectController = SandboxedModule.require modulePath, requires:
"settings-sharelatex":@settings
- "logger-sharelatex":
+ "logger-sharelatex":
log:->
err:->
- "../../infrastructure/Metrics":
+ "../../infrastructure/Metrics":
Timer:->
done:->
inc:->
@@ -73,21 +81,19 @@ describe "ProjectController", ->
"./ProjectUpdateHandler":@ProjectUpdateHandler
"../ReferencesSearch/ReferencesSearchHandler": @ReferencesSearchHandler
"./ProjectGetter": @ProjectGetter
+ '../Authentication/AuthenticationController': @AuthenticationController
- @user =
- _id:"!£123213kjljkl"
- first_name: "bjkdsjfk"
@projectName = "£12321jkj9ujkljds"
- @req =
- params:
+ @req =
+ params:
Project_id: @project_id
session:
user: @user
body:
- projectName: @projectName
+ projectName: @projectName
i18n:
translate:->
- @res =
+ @res =
locals:
jsPath:"js path here"
@@ -139,7 +145,7 @@ describe "ProjectController", ->
code.should.equal 204
done()
@ProjectController.updateProjectSettings @req, @res
-
+
describe "updateProjectAdminSettings", ->
it "should update the public access level", (done) ->
@EditorController.setPublicAccessLevel = sinon.stub().callsArg(2)
@@ -178,7 +184,7 @@ describe "ProjectController", ->
@ProjectController.restoreProject @req, @res
describe "cloneProject", ->
- it "should call the project duplicator", (done)->
+ it "should call the project duplicator", (done)->
@res.send = (json)=>
@ProjectDuplicator.duplicate.calledWith(@user, @project_id, @projectName).should.equal true
json.project_id.should.equal @project_id
@@ -214,7 +220,7 @@ describe "ProjectController", ->
@readOnly = [{lastUpdated:3, _id:3, owner_ref: "user-1"}]
@users =
- 'user-1':
+ 'user-1':
first_name: 'James'
'user-2':
first_name: 'Henry'
@@ -289,10 +295,10 @@ describe "ProjectController", ->
describe "loadEditor", ->
beforeEach ->
@settings.editorIsOpen = true
- @project =
+ @project =
name:"my proj"
_id:"213123kjlkj"
- @user =
+ @user =
_id:"123kj21k3lj"
ace:
fontSize:"massive"
@@ -351,4 +357,3 @@ describe "ProjectController", ->
@ProjectUpdateHandler.markAsOpened.calledWith(@project_id).should.equal true
done()
@ProjectController.loadEditor @req, @res
-
diff --git a/services/web/test/UnitTests/coffee/Security/RateLimiterMiddlewearTests.coffee b/services/web/test/UnitTests/coffee/Security/RateLimiterMiddlewearTests.coffee
index 05f40a2553..7066ed21ef 100644
--- a/services/web/test/UnitTests/coffee/Security/RateLimiterMiddlewearTests.coffee
+++ b/services/web/test/UnitTests/coffee/Security/RateLimiterMiddlewearTests.coffee
@@ -5,9 +5,13 @@ modulePath = require('path').join __dirname, '../../../../app/js/Features/Securi
describe "RateLimiterMiddlewear", ->
beforeEach ->
+ @AuthenticationController =
+ getLoggedInUserId: () =>
+ @req?.session?.user?._id
@RateLimiterMiddlewear = SandboxedModule.require modulePath, requires:
'../../infrastructure/RateLimiter' : @RateLimiter = {}
"logger-sharelatex": @logger = {warn: sinon.stub()}
+ '../Authentication/AuthenticationController': @AuthenticationController
@req =
params: {}
@res =
@@ -15,7 +19,7 @@ describe "RateLimiterMiddlewear", ->
write: sinon.stub()
end: sinon.stub()
@next = sinon.stub()
-
+
describe "rateLimit", ->
beforeEach ->
@rateLimiter = @RateLimiterMiddlewear.rateLimit({
@@ -28,7 +32,7 @@ describe "RateLimiterMiddlewear", ->
project_id: @project_id = "project-id"
doc_id: @doc_id = "doc-id"
}
-
+
describe "when there is no session", ->
beforeEach ->
@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
@@ -44,18 +48,18 @@ describe "RateLimiterMiddlewear", ->
subjectName: "#{@project_id}:#{@doc_id}:#{@ip}"
})
.should.equal true
-
+
it "should pass on to next()", ->
describe "when under the rate limit with logged in user", ->
beforeEach ->
@req.session =
- user :
+ user :
_id: @user_id = "user-id"
@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
@rateLimiter(@req, @res, @next)
-
+
it "should call the rate limiter backend with the user_id", ->
@RateLimiter.addCount
.calledWith({
@@ -65,16 +69,16 @@ describe "RateLimiterMiddlewear", ->
subjectName: "#{@project_id}:#{@doc_id}:#{@user_id}"
})
.should.equal true
-
+
it "should pass on to next()", ->
@next.called.should.equal true
-
+
describe "when under the rate limit with anonymous user", ->
beforeEach ->
@req.ip = @ip = "1.2.3.4"
@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true)
@rateLimiter(@req, @res, @next)
-
+
it "should call the rate limiter backend with the ip address", ->
@RateLimiter.addCount
.calledWith({
@@ -84,25 +88,25 @@ describe "RateLimiterMiddlewear", ->
subjectName: "#{@project_id}:#{@doc_id}:#{@ip}"
})
.should.equal true
-
+
it "should pass on to next()", ->
@next.called.should.equal true
-
+
describe "when over the rate limit", ->
beforeEach ->
- @req.session =
- user :
+ @req.session =
+ user :
_id: @user_id = "user-id"
@RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false)
@rateLimiter(@req, @res, @next)
-
+
it "should return a 429", ->
@res.status.calledWith(429).should.equal true
@res.end.called.should.equal true
-
+
it "should not continue", ->
@next.called.should.equal false
-
+
it "should log a warning", ->
@logger.warn
.calledWith({
@@ -112,4 +116,3 @@ describe "RateLimiterMiddlewear", ->
subjectName: "#{@project_id}:#{@doc_id}:#{@user_id}"
}, "rate limit exceeded")
.should.equal true
-
\ No newline at end of file
diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee
index db5e4e8c1c..1f935e420f 100644
--- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee
@@ -20,12 +20,15 @@ mockSubscriptions =
describe "SubscriptionController sanboxed", ->
beforeEach ->
- @user = {email:"tom@yahoo.com"}
+ @user = {email:"tom@yahoo.com", _id: 'one'}
@activeRecurlySubscription = mockSubscriptions["subscription-123-active"]
@AuthenticationController =
getLoggedInUser: sinon.stub().callsArgWith(1, null, @user)
- @SubscriptionHandler =
+ getLoggedInUserId: sinon.stub().returns(@user._id)
+ getSessionUser: sinon.stub().returns(@user)
+ isUserLoggedIn: sinon.stub().returns(true)
+ @SubscriptionHandler =
createSubscription: sinon.stub().callsArgWith(3)
updateSubscription: sinon.stub().callsArgWith(3)
reactivateSubscription: sinon.stub().callsArgWith(1)
@@ -36,19 +39,19 @@ describe "SubscriptionController sanboxed", ->
@PlansLocator =
findLocalPlanInSettings: sinon.stub()
- @LimitationsManager =
+ @LimitationsManager =
userHasSubscriptionOrIsGroupMember: sinon.stub()
userHasSubscription : sinon.stub()
- @RecurlyWrapper =
+ @RecurlyWrapper =
sign: sinon.stub().callsArgWith(1, null, "somthing")
- @SubscriptionViewModelBuilder =
+ @SubscriptionViewModelBuilder =
buildUsersSubscriptionViewModel:sinon.stub().callsArgWith(1, null, @activeRecurlySubscription)
buildViewModel: sinon.stub()
- @settings =
+ @settings =
coupon_codes:
- upgradeToAnnualPromo:
+ upgradeToAnnualPromo:
student:"STUDENTCODEHERE"
collaborator:"COLLABORATORCODEHERE"
apis:
@@ -58,8 +61,8 @@ describe "SubscriptionController sanboxed", ->
gaExperiments:{}
@GeoIpLookup =
getCurrencyCode:sinon.stub()
- @SubscriptionDomainHandler =
- getDomainLicencePage:sinon.stub()
+ @SubscriptionDomainHandler =
+ getDomainLicencePage:sinon.stub()
@SubscriptionController = SandboxedModule.require modulePath, requires:
'../Authentication/AuthenticationController': @AuthenticationController
'./SubscriptionHandler': @SubscriptionHandler
@@ -68,7 +71,7 @@ describe "SubscriptionController sanboxed", ->
"./LimitationsManager": @LimitationsManager
"../../infrastructure/GeoIpLookup":@GeoIpLookup
'./RecurlyWrapper': @RecurlyWrapper
- "logger-sharelatex":
+ "logger-sharelatex":
log:->
warn:->
"settings-sharelatex": @settings
@@ -78,7 +81,7 @@ describe "SubscriptionController sanboxed", ->
@res = new MockResponse()
@req = new MockRequest()
@req.body = {}
- @req.query =
+ @req.query =
planCode:"123123"
@stubbedCurrencyCode = "GBP"
@@ -175,7 +178,7 @@ describe "SubscriptionController sanboxed", ->
@res.render = (page, opts)=>
opts.currency.should.equal "EUR"
done()
- @SubscriptionController.paymentPage @req, @res
+ @SubscriptionController.paymentPage @req, @res
it "should use the geo ip currency if non is provided", (done)->
@@ -183,8 +186,8 @@ describe "SubscriptionController sanboxed", ->
@res.render = (page, opts)=>
opts.currency.should.equal @stubbedCurrencyCode
done()
- @SubscriptionController.paymentPage @req, @res
-
+ @SubscriptionController.paymentPage @req, @res
+
describe "successful_subscription", ->
beforeEach (done) ->
@SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(1, null, {})
@@ -226,7 +229,7 @@ describe "SubscriptionController sanboxed", ->
it "should render the dashboard", ->
@res.renderedTemplate.should.equal "subscriptions/dashboard"
-
+
describe "with a user with a paid subscription", ->
beforeEach (done) ->
@res.callback = done
@@ -238,7 +241,7 @@ describe "SubscriptionController sanboxed", ->
@res.rendered.should.equal true
@res.renderedTemplate.should.equal "subscriptions/dashboard"
done()
-
+
it "should set the correct subscription details", ->
@res.renderedVariables.subscription.should.deep.equal @activeRecurlySubscription
@@ -251,7 +254,7 @@ describe "SubscriptionController sanboxed", ->
it "should render the dashboard", ->
@res.renderedTemplate.should.equal "subscriptions/dashboard"
-
+
it "should set the correct subscription details", ->
@res.renderedVariables.subscription.should.deep.equal @activeRecurlySubscription
@@ -431,7 +434,7 @@ describe "SubscriptionController sanboxed", ->
describe "processUpgradeToAnnualPlan", ->
beforeEach ->
-
+
it "should tell the subscription handler to update the subscription with the annual plan and apply a coupon code", (done)->
@req.body =
planName:"student"
@@ -452,6 +455,3 @@ describe "SubscriptionController sanboxed", ->
done()
@SubscriptionController.processUpgradeToAnnualPlan @req, @res
-
-
-
diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupControllerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupControllerTests.coffee
index 455c441239..5c835e5cc5 100644
--- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupControllerTests.coffee
@@ -9,8 +9,19 @@ describe "SubscriptionGroupController", ->
beforeEach ->
@user = {_id:"!@312431",email:"user@email.com"}
+ @adminUserId = "123jlkj"
+ @subscription_id = "123434325412"
+ @user_email = "bob@gmail.com"
+ @req =
+ session:
+ user:
+ _id: @adminUserId
+ email:@user_email
+ params:
+ subscription_id:@subscription_id
+ query:{}
@subscription = {}
- @GroupHandler =
+ @GroupHandler =
addUserToGroup: sinon.stub().callsArgWith(2, null, @user)
removeUserFromGroup: sinon.stub().callsArgWith(2)
isUserPartOfGroup: sinon.stub()
@@ -18,15 +29,18 @@ describe "SubscriptionGroupController", ->
processGroupVerification:sinon.stub()
getPopulatedListOfMembers: sinon.stub().callsArgWith(1, null, [@user])
@SubscriptionLocator = getUsersSubscription: sinon.stub().callsArgWith(1, null, @subscription)
+ @AuthenticationController =
+ getLoggedInUserId: (req) -> req.session.user._id
+ getSessionUser: (req) -> req.session.user
- @SubscriptionDomainHandler =
+ @SubscriptionDomainHandler =
findDomainLicenceBySubscriptionId:sinon.stub()
@OneTimeTokenHandler =
getValueFromTokenAndExpire:sinon.stub()
- @ErrorsController =
+ @ErrorsController =
notFound:sinon.stub()
@Controller = SandboxedModule.require modulePath, requires:
@@ -35,18 +49,8 @@ describe "SubscriptionGroupController", ->
"./SubscriptionLocator": @SubscriptionLocator
"./SubscriptionDomainHandler":@SubscriptionDomainHandler
"../Errors/ErrorController":@ErrorsController
+ '../Authentication/AuthenticationController': @AuthenticationController
- @adminUserId = "123jlkj"
- @subscription_id = "123434325412"
- @user_email = "bob@gmail.com"
- @req =
- session:
- user:
- _id: @adminUserId
- email:@user_email
- params:
- subscription_id:@subscription_id
- query:{}
@token = "super-secret-token"
@@ -76,7 +80,7 @@ describe "SubscriptionGroupController", ->
@Controller.removeUserFromGroup @req, res
- describe "renderSubscriptionGroupAdminPage", ->
+ describe "renderSubscriptionGroupAdminPage", ->
it "should redirect you if you don't have a group account", (done)->
@subscription.groupPlan = false
@@ -177,7 +181,7 @@ describe "SubscriptionGroupController", ->
@Controller.completeJoin @req, res
- describe "exportGroupCsv", ->
+ describe "exportGroupCsv", ->
beforeEach ->
@subscription.groupPlan = true
diff --git a/services/web/test/UnitTests/coffee/Tags/TagsControllerTests.coffee b/services/web/test/UnitTests/coffee/Tags/TagsControllerTests.coffee
index dbce6c094a..059460a4c4 100644
--- a/services/web/test/UnitTests/coffee/Tags/TagsControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Tags/TagsControllerTests.coffee
@@ -11,24 +11,28 @@ describe 'TagsController', ->
tag = "some_class101"
beforeEach ->
- @handler =
+ @handler =
addProjectToTag: sinon.stub().callsArgWith(3)
removeProjectFromTag: sinon.stub().callsArgWith(3)
deleteTag: sinon.stub().callsArg(2)
renameTag: sinon.stub().callsArg(3)
createTag: sinon.stub()
+ @AuthenticationController =
+ getLoggedInUserId: (req) =>
+ req.session.user._id
@controller = SandboxedModule.require modulePath, requires:
"./TagsHandler":@handler
'logger-sharelatex':
log:->
err:->
+ '../Authentication/AuthenticationController': @AuthenticationController
@req =
params:
project_id:project_id
session:
user:
_id:user_id
-
+
@res = {}
@res.status = sinon.stub().returns @res
@res.end = sinon.stub()
@@ -49,26 +53,26 @@ describe 'TagsController', ->
@req.session.user._id = @user_id = "user-id-123"
@req.body = name: @name = "tag-name"
@controller.createTag @req, @res
-
+
it "should create the tag in the backend", ->
@handler.createTag
.calledWith(@user_id, @name)
.should.equal true
-
+
it "should return the tag", ->
@res.json.calledWith(@tag).should.equal true
-
+
describe "deleteTag", ->
beforeEach ->
@req.params.tag_id = @tag_id = "tag-id-123"
@req.session.user._id = @user_id = "user-id-123"
@controller.deleteTag @req, @res
-
+
it "should delete the tag in the backend", ->
@handler.deleteTag
.calledWith(@user_id, @tag_id)
.should.equal true
-
+
it "should return 204 status code", ->
@res.status.calledWith(204).should.equal true
@res.end.called.should.equal true
@@ -82,56 +86,55 @@ describe 'TagsController', ->
beforeEach ->
@req.body = name: @name = "new-name"
@controller.renameTag @req, @res
-
+
it "should delete the tag in the backend", ->
@handler.renameTag
.calledWith(@user_id, @tag_id, @name)
.should.equal true
-
+
it "should return 204 status code", ->
@res.status.calledWith(204).should.equal true
@res.end.called.should.equal true
-
+
describe "without a name", ->
beforeEach ->
@controller.renameTag @req, @res
-
+
it "should not call the backend", ->
@handler.renameTag.called.should.equal false
-
+
it "should return 400 (bad request) status code", ->
@res.status.calledWith(400).should.equal true
@res.end.called.should.equal true
-
+
describe "addProjectToTag", ->
beforeEach ->
@req.params.tag_id = @tag_id = "tag-id-123"
@req.params.project_id = @project_id = "project-id-123"
@req.session.user._id = @user_id = "user-id-123"
@controller.addProjectToTag @req, @res
-
+
it "should add the tag to the project in the backend", ->
@handler.addProjectToTag
.calledWith(@user_id, @tag_id, @project_id)
.should.equal true
-
+
it "should return 204 status code", ->
@res.status.calledWith(204).should.equal true
@res.end.called.should.equal true
-
+
describe "removeProjectFromTag", ->
beforeEach ->
@req.params.tag_id = @tag_id = "tag-id-123"
@req.params.project_id = @project_id = "project-id-123"
@req.session.user._id = @user_id = "user-id-123"
@controller.removeProjectFromTag @req, @res
-
+
it "should remove the tag from the project in the backend", ->
@handler.removeProjectFromTag
.calledWith(@user_id, @tag_id, @project_id)
.should.equal true
-
+
it "should return 204 status code", ->
@res.status.calledWith(204).should.equal true
@res.end.called.should.equal true
-
\ No newline at end of file
diff --git a/services/web/test/UnitTests/coffee/TrackChanges/TrackChangesControllerTests.coffee b/services/web/test/UnitTests/coffee/TrackChanges/TrackChangesControllerTests.coffee
index ad9f1ed04a..bcc57b58b8 100644
--- a/services/web/test/UnitTests/coffee/TrackChanges/TrackChangesControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/TrackChanges/TrackChangesControllerTests.coffee
@@ -6,18 +6,20 @@ SandboxedModule = require('sandboxed-module')
describe "TrackChangesController", ->
beforeEach ->
+ @user_id = "user-id-123"
+ @AuthenticationController =
+ getLoggedInUserId: sinon.stub().returns(@user_id)
@TrackChangesController = SandboxedModule.require modulePath, requires:
"request" : @request = sinon.stub()
"settings-sharelatex": @settings = {}
"logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub()}
- "../Authentication/AuthenticationController": @AuthenticationController = {}
+ "../Authentication/AuthenticationController": @AuthenticationController
describe "proxyToTrackChangesApi", ->
beforeEach ->
@req = { url: "/mock/url", method: "POST" }
@res = "mock-res"
@next = sinon.stub()
- @user_id = "user-id-123"
@settings.apis =
trackchanges:
url: "http://trackchanges.example.com"
@@ -26,7 +28,6 @@ describe "TrackChangesController", ->
pipe: sinon.stub()
on: (event, handler) -> @events[event] = handler
@request.returns @proxy
- @AuthenticationController.getLoggedInUserId = sinon.stub().callsArgWith(1, null, @user_id)
@TrackChangesController.proxyToTrackChangesApi @req, @res, @next
describe "successfully", ->
@@ -56,4 +57,3 @@ describe "TrackChangesController", ->
it "should pass the error up the call chain", ->
@next.calledWith(@error).should.equal true
-
diff --git a/services/web/test/UnitTests/coffee/Uploads/ProjectUploadControllerTests.coffee b/services/web/test/UnitTests/coffee/Uploads/ProjectUploadControllerTests.coffee
index 176ae96d24..764265424c 100644
--- a/services/web/test/UnitTests/coffee/Uploads/ProjectUploadControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Uploads/ProjectUploadControllerTests.coffee
@@ -15,13 +15,16 @@ describe "ProjectUploadController", ->
@metrics =
Timer: class Timer
done: sinon.stub()
+ @AuthenticationController =
+ getLoggedInUserId: sinon.stub().returns(@user_id)
@ProjectUploadController = SandboxedModule.require modulePath, requires:
"./ProjectUploadManager" : @ProjectUploadManager = {}
"./FileSystemImportManager" : @FileSystemImportManager = {}
"logger-sharelatex" : @logger = {log: sinon.stub(), error: sinon.stub(), err:->}
"../../infrastructure/Metrics": @metrics
+ '../Authentication/AuthenticationController': @AuthenticationController
"fs" : @fs = {}
-
+
describe "uploadProject", ->
beforeEach ->
@path = "/path/to/file/on/disk.zip"
@@ -55,13 +58,13 @@ describe "ProjectUploadController", ->
.createProjectFromZipArchive
.calledWith(sinon.match.any, "filename", sinon.match.any)
.should.equal true
-
+
it "should create a project from the zip archive", ->
@ProjectUploadManager
.createProjectFromZipArchive
.calledWith(sinon.match.any, sinon.match.any, @path)
.should.equal true
-
+
it "should return a successful response to the FileUploader client", ->
expect(@res.body).to.deep.equal
success: true
diff --git a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee
index 19d17e7ac3..63879e9c4c 100644
--- a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee
@@ -16,9 +16,18 @@ describe "UserController", ->
@user =
_id:@user_id
- save:sinon.stub().callsArgWith(0)
+ save: sinon.stub().callsArgWith(0)
ace:{}
+ @req =
+ user: {}
+ session:
+ destroy:->
+ user :
+ _id : @user_id
+ email:"old@something.com"
+ body:{}
+
@UserDeleter =
deleteUser: sinon.stub().callsArgWith(1)
@UserLocator =
@@ -31,6 +40,8 @@ describe "UserController", ->
registerNewUser: sinon.stub()
@AuthenticationController =
establishUserSession: sinon.stub().callsArg(2)
+ getLoggedInUserId: sinon.stub().returns(@user._id)
+ getSessionUser: sinon.stub().returns(@req.session.user)
@AuthenticationManager =
authenticate: sinon.stub()
setUserPassword: sinon.stub()
@@ -67,13 +78,6 @@ describe "UserController", ->
err:->
"../../infrastructure/Metrics": inc:->
- @req =
- session:
- destroy:->
- user :
- _id : @user_id
- email:"old@something.com"
- body:{}
@res =
send: sinon.stub()
json: sinon.stub()
@@ -172,7 +176,7 @@ describe "UserController", ->
cb(null, @user)
@res.sendStatus = (code)=>
code.should.equal 200
- @req.session.user.email.should.equal @newEmail
+ @req.user.email.should.equal @newEmail
done()
@UserController.updateUserSettings @req, @res
diff --git a/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee
index 4b36ace39c..0df1d65fe6 100644
--- a/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/User/UserPagesControllerTests.coffee
@@ -11,7 +11,7 @@ describe "UserPagesController", ->
beforeEach ->
@settings = {}
- @user =
+ @user =
_id: @user_id = "kwjewkl"
features:{}
email: "joe@example.com"
@@ -25,6 +25,8 @@ describe "UserPagesController", ->
getUserRegistrationStatus : sinon.stub().callsArgWith(1, null, @dropboxStatus)
@ErrorController =
notFound: sinon.stub()
+ @AuthenticationController =
+ getLoggedInUserId: sinon.stub().returns(@user._id)
@UserPagesController = SandboxedModule.require modulePath, requires:
"settings-sharelatex":@settings
"logger-sharelatex": log:->
@@ -32,7 +34,8 @@ describe "UserPagesController", ->
"./UserGetter": @UserGetter
"../Errors/ErrorController": @ErrorController
'../Dropbox/DropboxHandler': @DropboxHandler
- @req =
+ '../Authentication/AuthenticationController': @AuthenticationController
+ @req =
query:{}
session:
user:@user
@@ -111,24 +114,24 @@ describe "UserPagesController", ->
opts.user.should.equal @user
done()
@UserPagesController.settingsPage @req, @res
-
+
describe "activateAccountPage", ->
beforeEach ->
@req.query.user_id = @user_id
@req.query.token = @token = "mock-token-123"
-
+
it "should 404 without a user_id", (done) ->
delete @req.query.user_id
@ErrorController.notFound = () ->
done()
@UserPagesController.activateAccountPage @req, @res
-
+
it "should 404 without a token", (done) ->
delete @req.query.token
@ErrorController.notFound = () ->
done()
@UserPagesController.activateAccountPage @req, @res
-
+
it "should 404 without a valid user_id", (done) ->
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, null)
@ErrorController.notFound = () ->
@@ -142,7 +145,7 @@ describe "UserPagesController", ->
url.should.equal "/login?email=#{encodeURIComponent(@user.email)}"
done()
@UserPagesController.activateAccountPage @req, @res
-
+
it "render the activation page if the user has not logged in before", (done) ->
@user.loginCount = 0
@res.render = (page, opts) =>
@@ -150,4 +153,4 @@ describe "UserPagesController", ->
opts.email.should.equal @user.email
opts.token.should.equal @token
done()
- @UserPagesController.activateAccountPage @req, @res
\ No newline at end of file
+ @UserPagesController.activateAccountPage @req, @res
From 1714b014bf9fe5533084a185982e4376e1156d46 Mon Sep 17 00:00:00 2001
From: James Allen
Date: Tue, 13 Sep 2016 10:29:14 +0100
Subject: [PATCH 253/378] Force true or false for userHasNoSubscription, not
blank
---
services/web/app/views/project/list/side-bar.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/project/list/side-bar.jade b/services/web/app/views/project/list/side-bar.jade
index 96365c813e..3738e9a4ae 100644
--- a/services/web/app/views/project/list/side-bar.jade
+++ b/services/web/app/views/project/list/side-bar.jade
@@ -155,6 +155,6 @@
| #{translate("or_unlock_features_bonus")}
a(href="/user/bonus") #{translate("sharing_sl")} .
script.
- window.userHasNoSubscription = #{settings.enableSubscriptions && !hasSubscription}
+ window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)}
From cc9791d3f452904e239e2464f56bfc5e290372cf Mon Sep 17 00:00:00 2001
From: James Allen
Date: Tue, 13 Sep 2016 11:23:47 +0100
Subject: [PATCH 254/378] Redirect to login with a redirect back to the page we
want if not logged in
---
.../Authorization/AuthorizationMiddlewear.coffee | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee
index 4888db0c8a..7f2f67e03c 100644
--- a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee
+++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee
@@ -99,13 +99,17 @@ module.exports = AuthorizationMiddlewear =
callback null, user_id
redirectToRestricted: (req, res, next) ->
- res.redirect "/restricted"
+ res.redirect "/restricted?from=#{encodeURIComponent(req.url)}"
restricted : (req, res, next)->
if req.session.user?
res.render 'user/restricted',
title:'restricted'
else
- logger.log "user not logged in and trying to access #{req.url}, being redirected to login"
- res.redirect '/register'
+ from = req.query.from
+ logger.log {from: from}, "redirecting to login"
+ redirect_to = "/login"
+ if from?
+ redirect_to += "?redir=#{encodeURIComponent(from)}"
+ res.redirect redirect_to
\ No newline at end of file
From a45aa7b80eb5d4a9ff61516ea15ea43671e89262 Mon Sep 17 00:00:00 2001
From: James Allen
Date: Tue, 13 Sep 2016 15:54:10 +0100
Subject: [PATCH 255/378] Show word count error messages
---
services/web/app/views/project/editor/left-menu.jade | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/services/web/app/views/project/editor/left-menu.jade b/services/web/app/views/project/editor/left-menu.jade
index 05fa19542f..90f7e20c5e 100644
--- a/services/web/app/views/project/editor/left-menu.jade
+++ b/services/web/app/views/project/editor/left-menu.jade
@@ -197,6 +197,10 @@ script(type='text/ng-template', id='wordCountModalTemplate')
)
div(ng-if="!status.loading")
.container-fluid
+ .row(ng-show='data.messages.length > 0')
+ .col-xs-12
+ .alert.alert-danger
+ p {{data.messages}}
.row
.col-xs-4
.pull-right #{translate("total_words")} :
From 53b3e3831d889f6b9d3e9c9cde3618a9684d3f49 Mon Sep 17 00:00:00 2001
From: James Allen
Date: Wed, 14 Sep 2016 10:51:19 +0100
Subject: [PATCH 256/378] Update cancellation survey link
---
services/web/app/coffee/Features/Email/EmailBuilder.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee
index f7a7a78a05..0ac465a712 100644
--- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee
+++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee
@@ -26,7 +26,7 @@ templates.canceledSubscription =
compiledTemplate: _.template '''
Hi <%= first_name %>,
-I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via this survey?
+I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via this survey?
Thank you in advance.
From 9019b20d50c6a4d0e0368d993ee9f8f6828ab4a2 Mon Sep 17 00:00:00 2001
From: James Allen
Date: Wed, 14 Sep 2016 11:21:00 +0100
Subject: [PATCH 257/378] Preserve line formatting in word count error message
---
services/web/app/views/project/editor/left-menu.jade | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/web/app/views/project/editor/left-menu.jade b/services/web/app/views/project/editor/left-menu.jade
index 90f7e20c5e..695e24d05a 100644
--- a/services/web/app/views/project/editor/left-menu.jade
+++ b/services/web/app/views/project/editor/left-menu.jade
@@ -200,7 +200,7 @@ script(type='text/ng-template', id='wordCountModalTemplate')
.row(ng-show='data.messages.length > 0')
.col-xs-12
.alert.alert-danger
- p {{data.messages}}
+ p(style="white-space: pre-wrap") {{data.messages}}
.row
.col-xs-4
.pull-right #{translate("total_words")} :
From c9a17982cf298a62e26688c914cdeda6ce59d0ef Mon Sep 17 00:00:00 2001
From: James Allen
Date: Wed, 14 Sep 2016 17:08:26 +0100
Subject: [PATCH 258/378] Add canonical url tag and don't include query string
---
services/web/app/coffee/infrastructure/ExpressLocals.coffee | 4 +++-
services/web/app/views/layout.jade | 1 +
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index d3b9d9a24b..b57556a42d 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -123,7 +123,9 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.translate = (key, vars = {}) ->
vars.appName = Settings.appName
req.i18n.translate(key, vars)
- res.locals.currentUrl = req.originalUrl
+ # Don't include the query string parameters, otherwise Google
+ # treats ?nocdn=true as the canonical version
+ res.locals.currentUrl = Url.parse(req.originalUrl).pathname
next()
webRouter.use (req, res, next)->
diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade
index 88fe4187fe..7d986e5abf 100644
--- a/services/web/app/views/layout.jade
+++ b/services/web/app/views/layout.jade
@@ -21,6 +21,7 @@ html(itemscope, itemtype='http://schema.org/Product')
link(rel="icon", href="/favicon.ico")
link(rel='stylesheet', href=buildCssPath('/style.css'))
+ link(rel="canonical", href=settings.siteUrl+currentUrl)
if settings.i18n.subdomainLang
each subdomainDetails in settings.i18n.subdomainLang
if !subdomainDetails.hide
From 06d67835e437118f2bb123f56c4a6c2b7799a81c Mon Sep 17 00:00:00 2001
From: James Allen
Date: Thu, 15 Sep 2016 10:21:12 +0100
Subject: [PATCH 259/378] Don't redirect Google to nocdn=true, and remove
canonical URL until we sort out translated domains
---
services/web/app/views/layout.jade | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade
index 7d986e5abf..30b6b2294b 100644
--- a/services/web/app/views/layout.jade
+++ b/services/web/app/views/layout.jade
@@ -21,7 +21,6 @@ html(itemscope, itemtype='http://schema.org/Product')
link(rel="icon", href="/favicon.ico")
link(rel='stylesheet', href=buildCssPath('/style.css'))
- link(rel="canonical", href=settings.siteUrl+currentUrl)
if settings.i18n.subdomainLang
each subdomainDetails in settings.i18n.subdomainLang
if !subdomainDetails.hide
@@ -58,7 +57,7 @@ html(itemscope, itemtype='http://schema.org/Product')
var noCdnKey = "nocdn=true"
var cdnBlocked = typeof jQuery === 'undefined'
var noCdnAlreadyInUrl = window.location.href.indexOf(noCdnKey) != -1 //prevent loops
- if (cdnBlocked && !noCdnAlreadyInUrl) {
+ if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf("Googlebot") == -1) {
window.location.search += '&'+noCdnKey;
}
script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
From 2119dcbb589f2946f6ed107426eca9a927c936ff Mon Sep 17 00:00:00 2001
From: Shane Kilkelly
Date: Thu, 15 Sep 2016 14:36:11 +0100
Subject: [PATCH 260/378] Finalise login workflow, works with login form again.
---
.../AuthenticationController.coffee | 78 +++++--------------
services/web/app/coffee/router.coffee | 7 +-
.../AuthenticationControllerTests.coffee | 60 +++++++++++++-
3 files changed, 80 insertions(+), 65 deletions(-)
diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
index 8d12acfb64..4342da9d77 100644
--- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -11,6 +11,7 @@ basicAuth = require('basic-auth-connect')
UserHandler = require("../User/UserHandler")
UserSessionsManager = require("../User/UserSessionsManager")
Analytics = require "../Analytics/AnalyticsManager"
+passport = require 'passport'
module.exports = AuthenticationController =
@@ -32,6 +33,22 @@ module.exports = AuthenticationController =
deserializeUser: (user, cb) ->
cb(null, user)
+ passportLogin: (req, res, next) ->
+ # This function is middleware which wraps the passport.authenticate middleware,
+ # so we can send back our custom `{message: {text: "", type: ""}}` responses on failure,
+ # and send a `{redir: ""}` response on success
+ passport.authenticate('local', (err, user, info) ->
+ # `user` is either a user object or false
+ if err?
+ return next(err)
+ if user
+ req.login user, (err) ->
+ res.json {redir: req._redir}
+ else
+ res.json message: info
+ )(req, res, next)
+
+
doPassportLogin: (req, username, password, done) ->
email = username.toLowerCase()
redir = Url.parse(req?.body?.redir or "/project").path
@@ -39,7 +56,7 @@ module.exports = AuthenticationController =
return done(err) if err?
if !isAllowed
logger.log email:email, "too many login requests"
- return done(null, null, {message: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'})
+ return done(null, null, {text: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'})
AuthenticationManager.authenticate email: email, password, (error, user) ->
return done(error) if error?
if user?
@@ -58,7 +75,7 @@ module.exports = AuthenticationController =
else
AuthenticationController._recordFailedLogin()
logger.log email: email, "failed log in"
- return done(null, false, {message: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'})
+ return done(null, false, {text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'})
isUserLoggedIn: (req) ->
user_id = AuthenticationController.getLoggedInUserId(req)
@@ -152,60 +169,3 @@ module.exports = AuthenticationController =
_recordFailedLogin: (callback = (error) ->) ->
Metrics.inc "user.login.failed"
callback()
-
- # establishUserSession: (req, user, callback = (error) ->) ->
- # dienow
- # lightUser =
- # _id: user._id
- # first_name: user.first_name
- # last_name: user.last_name
- # isAdmin: user.isAdmin
- # email: user.email
- # referal_id: user.referal_id
- # session_created: (new Date()).toISOString()
- # ip_address: req.ip
- # # Regenerate the session to get a new sessionID (cookie value) to
- # # protect against session fixation attacks
- # oldSession = req.session
- # req.session.destroy()
- # req.sessionStore.generate(req)
- # for key, value of oldSession
- # req.session[key] = value
-
- # req.session.user = lightUser
-
- # UserSessionsManager.trackSession(user, req.sessionID, () ->)
- # callback()
-
-
- # doLogin: (options, req, res, next) ->
- # dienow
- # email = options.email?.toLowerCase()
- # password = options.password
- # redir = Url.parse(options.redir or "/project").path
- # LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
- # if !isAllowed
- # logger.log email:email, "too many login requests"
- # res.statusCode = 429
- # return res.send
- # message:
- # text: req.i18n.translate("to_many_login_requests_2_mins"),
- # type: 'error'
- # AuthenticationManager.authenticate email: email, password, (error, user) ->
- # return next(error) if error?
- # if user?
- # UserHandler.setupLoginData user, ->
- # LoginRateLimiter.recordSuccessfulLogin email
- # AuthenticationController._recordSuccessfulLogin user._id
- # AuthenticationController.establishUserSession req, user, (error) ->
- # return next(error) if error?
- # req.session.justLoggedIn = true
- # logger.log email: email, user_id: user._id.toString(), "successful log in"
- # Analytics.recordEvent user._id, "user-logged-in"
- # res.json redir: redir
- # else
- # AuthenticationController._recordFailedLogin()
- # logger.log email: email, "failed log in"
- # res.json message:
- # text: req.i18n.translate("email_or_password_wrong_try_again"),
- # type: 'error'
diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee
index 86a97b0a40..5b8f0b49f9 100644
--- a/services/web/app/coffee/router.coffee
+++ b/services/web/app/coffee/router.coffee
@@ -43,7 +43,6 @@ AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
logger = require("logger-sharelatex")
_ = require("underscore")
-passport = require('passport')
module.exports = class Router
constructor: (webRouter, apiRouter)->
@@ -54,10 +53,8 @@ module.exports = class Router
webRouter.get '/login', UserPagesController.loginPage
AuthenticationController.addEndpointToLoginWhitelist '/login'
- # webRouter.post '/login', AuthenticationController.login
- webRouter.post '/login', passport.authenticate('local'), (req, res) ->
- console.log ">> login done", req._redir
- res.json {redir: req._redir}
+ webRouter.post '/login', AuthenticationController.passportLogin
+
webRouter.get '/logout', UserController.logout
webRouter.get '/restricted', AuthorizationMiddlewear.restricted
diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
index 3e0812d3ff..07d9b60567 100644
--- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee
@@ -22,6 +22,8 @@ describe "AuthenticationController", ->
"../Analytics/AnalyticsManager": @AnalyticsManager = { recordEvent: sinon.stub() }
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
"settings-sharelatex": {}
+ "passport": @passport =
+ authenticate: sinon.stub().returns(sinon.stub())
"../User/UserSessionsManager": @UserSessionsManager =
trackSession: sinon.stub()
untrackSession: sinon.stub()
@@ -42,6 +44,62 @@ describe "AuthenticationController", ->
afterEach ->
tk.reset()
+ describe 'passportLogin', ->
+
+ beforeEach ->
+ @info = null
+ @req.login = sinon.stub().callsArgWith(1, null)
+ @res.json = sinon.stub()
+ @passport.authenticate.callsArgWith(1, null, @user, @info)
+
+ it 'should call passport.authenticate', () ->
+ @AuthenticationController.passportLogin @req, @res, @next
+ @passport.authenticate.callCount.should.equal 1
+
+ describe 'when authenticate produces an error', ->
+
+ beforeEach ->
+ @err = new Error('woops')
+ @passport.authenticate.callsArgWith(1, @err)
+
+ it 'should return next with an error', () ->
+ @AuthenticationController.passportLogin @req, @res, @next
+ @next.calledWith(@err).should.equal true
+
+ describe 'when authenticate produces a user', ->
+
+ beforeEach ->
+ @req._redir = 'some_redirect'
+ @passport.authenticate.callsArgWith(1, null, @user, @info)
+
+ afterEach ->
+ delete @req._redir
+
+ it 'should call req.login', () ->
+ @AuthenticationController.passportLogin @req, @res, @next
+ @req.login.callCount.should.equal 1
+ @req.login.calledWith(@user).should.equal true
+
+ it 'should send a json response with redirect', () ->
+ @AuthenticationController.passportLogin @req, @res, @next
+ @res.json.callCount.should.equal 1
+ @res.json.calledWith({redir: @req._redir}).should.equal true
+
+ describe 'when authenticate does not produce a user', ->
+
+ beforeEach ->
+ @info = {text: 'a', type: 'b'}
+ @passport.authenticate.callsArgWith(1, null, false, @info)
+
+ it 'should not call req.login', () ->
+ @AuthenticationController.passportLogin @req, @res, @next
+ @req.login.callCount.should.equal 0
+
+ it 'should send a json response with redirect', () ->
+ @AuthenticationController.passportLogin @req, @res, @next
+ @res.json.callCount.should.equal 1
+ @res.json.calledWith({message: @info}).should.equal true
+
describe 'getSessionUser', ->
it 'should get the user object from session', ->
@@ -132,7 +190,7 @@ describe "AuthenticationController", ->
@cb.callCount.should.equal 1
@cb.calledWith(null, false)
# @res.body.should.exist
- expect(@cb.lastCall.args[2]).to.contain.all.keys ['message']
+ expect(@cb.lastCall.args[2]).to.contain.all.keys ['text', 'type']
# message:
# text: 'Your email or password were incorrect. Please try again',
# type: 'error'
From dd4a509d510b933fea0bb4ff0a05df6c55ac311c Mon Sep 17 00:00:00 2001
From: Brian Gough
Date: Mon, 19 Sep 2016 10:29:55 +0100
Subject: [PATCH 261/378] add copy of unpatched pdfjs-1.3.91
---
.../js/libs/pdfjs-1.3.91p1/compatibility.js | 593 +
.../web/public/js/libs/pdfjs-1.3.91p1/pdf.js | 9534 ++++
.../js/libs/pdfjs-1.3.91p1/pdf.worker.js | 40692 ++++++++++++++++
3 files changed, 50819 insertions(+)
create mode 100644 services/web/public/js/libs/pdfjs-1.3.91p1/compatibility.js
create mode 100644 services/web/public/js/libs/pdfjs-1.3.91p1/pdf.js
create mode 100644 services/web/public/js/libs/pdfjs-1.3.91p1/pdf.worker.js
diff --git a/services/web/public/js/libs/pdfjs-1.3.91p1/compatibility.js b/services/web/public/js/libs/pdfjs-1.3.91p1/compatibility.js
new file mode 100644
index 0000000000..1119a2742a
--- /dev/null
+++ b/services/web/public/js/libs/pdfjs-1.3.91p1/compatibility.js
@@ -0,0 +1,593 @@
+/* Copyright 2012 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* globals VBArray, PDFJS */
+
+'use strict';
+
+// Initializing PDFJS global object here, it case if we need to change/disable
+// some PDF.js features, e.g. range requests
+if (typeof PDFJS === 'undefined') {
+ (typeof window !== 'undefined' ? window : this).PDFJS = {};
+}
+
+// Checking if the typed arrays are supported
+// Support: iOS<6.0 (subarray), IE<10, Android<4.0
+(function checkTypedArrayCompatibility() {
+ if (typeof Uint8Array !== 'undefined') {
+ // Support: iOS<6.0
+ if (typeof Uint8Array.prototype.subarray === 'undefined') {
+ Uint8Array.prototype.subarray = function subarray(start, end) {
+ return new Uint8Array(this.slice(start, end));
+ };
+ Float32Array.prototype.subarray = function subarray(start, end) {
+ return new Float32Array(this.slice(start, end));
+ };
+ }
+
+ // Support: Android<4.1
+ if (typeof Float64Array === 'undefined') {
+ window.Float64Array = Float32Array;
+ }
+ return;
+ }
+
+ function subarray(start, end) {
+ return new TypedArray(this.slice(start, end));
+ }
+
+ function setArrayOffset(array, offset) {
+ if (arguments.length < 2) {
+ offset = 0;
+ }
+ for (var i = 0, n = array.length; i < n; ++i, ++offset) {
+ this[offset] = array[i] & 0xFF;
+ }
+ }
+
+ function TypedArray(arg1) {
+ var result, i, n;
+ if (typeof arg1 === 'number') {
+ result = [];
+ for (i = 0; i < arg1; ++i) {
+ result[i] = 0;
+ }
+ } else if ('slice' in arg1) {
+ result = arg1.slice(0);
+ } else {
+ result = [];
+ for (i = 0, n = arg1.length; i < n; ++i) {
+ result[i] = arg1[i];
+ }
+ }
+
+ result.subarray = subarray;
+ result.buffer = result;
+ result.byteLength = result.length;
+ result.set = setArrayOffset;
+
+ if (typeof arg1 === 'object' && arg1.buffer) {
+ result.buffer = arg1.buffer;
+ }
+ return result;
+ }
+
+ window.Uint8Array = TypedArray;
+ window.Int8Array = TypedArray;
+
+ // we don't need support for set, byteLength for 32-bit array
+ // so we can use the TypedArray as well
+ window.Uint32Array = TypedArray;
+ window.Int32Array = TypedArray;
+ window.Uint16Array = TypedArray;
+ window.Float32Array = TypedArray;
+ window.Float64Array = TypedArray;
+})();
+
+// URL = URL || webkitURL
+// Support: Safari<7, Android 4.2+
+(function normalizeURLObject() {
+ if (!window.URL) {
+ window.URL = window.webkitURL;
+ }
+})();
+
+// Object.defineProperty()?
+// Support: Android<4.0, Safari<5.1
+(function checkObjectDefinePropertyCompatibility() {
+ if (typeof Object.defineProperty !== 'undefined') {
+ var definePropertyPossible = true;
+ try {
+ // some browsers (e.g. safari) cannot use defineProperty() on DOM objects
+ // and thus the native version is not sufficient
+ Object.defineProperty(new Image(), 'id', { value: 'test' });
+ // ... another test for android gb browser for non-DOM objects
+ var Test = function Test() {};
+ Test.prototype = { get id() { } };
+ Object.defineProperty(new Test(), 'id',
+ { value: '', configurable: true, enumerable: true, writable: false });
+ } catch (e) {
+ definePropertyPossible = false;
+ }
+ if (definePropertyPossible) {
+ return;
+ }
+ }
+
+ Object.defineProperty = function objectDefineProperty(obj, name, def) {
+ delete obj[name];
+ if ('get' in def) {
+ obj.__defineGetter__(name, def['get']);
+ }
+ if ('set' in def) {
+ obj.__defineSetter__(name, def['set']);
+ }
+ if ('value' in def) {
+ obj.__defineSetter__(name, function objectDefinePropertySetter(value) {
+ this.__defineGetter__(name, function objectDefinePropertyGetter() {
+ return value;
+ });
+ return value;
+ });
+ obj[name] = def.value;
+ }
+ };
+})();
+
+
+// No XMLHttpRequest#response?
+// Support: IE<11, Android <4.0
+(function checkXMLHttpRequestResponseCompatibility() {
+ var xhrPrototype = XMLHttpRequest.prototype;
+ var xhr = new XMLHttpRequest();
+ if (!('overrideMimeType' in xhr)) {
+ // IE10 might have response, but not overrideMimeType
+ // Support: IE10
+ Object.defineProperty(xhrPrototype, 'overrideMimeType', {
+ value: function xmlHttpRequestOverrideMimeType(mimeType) {}
+ });
+ }
+ if ('responseType' in xhr) {
+ return;
+ }
+
+ // The worker will be using XHR, so we can save time and disable worker.
+ PDFJS.disableWorker = true;
+
+ Object.defineProperty(xhrPrototype, 'responseType', {
+ get: function xmlHttpRequestGetResponseType() {
+ return this._responseType || 'text';
+ },
+ set: function xmlHttpRequestSetResponseType(value) {
+ if (value === 'text' || value === 'arraybuffer') {
+ this._responseType = value;
+ if (value === 'arraybuffer' &&
+ typeof this.overrideMimeType === 'function') {
+ this.overrideMimeType('text/plain; charset=x-user-defined');
+ }
+ }
+ }
+ });
+
+ // Support: IE9
+ if (typeof VBArray !== 'undefined') {
+ Object.defineProperty(xhrPrototype, 'response', {
+ get: function xmlHttpRequestResponseGet() {
+ if (this.responseType === 'arraybuffer') {
+ return new Uint8Array(new VBArray(this.responseBody).toArray());
+ } else {
+ return this.responseText;
+ }
+ }
+ });
+ return;
+ }
+
+ Object.defineProperty(xhrPrototype, 'response', {
+ get: function xmlHttpRequestResponseGet() {
+ if (this.responseType !== 'arraybuffer') {
+ return this.responseText;
+ }
+ var text = this.responseText;
+ var i, n = text.length;
+ var result = new Uint8Array(n);
+ for (i = 0; i < n; ++i) {
+ result[i] = text.charCodeAt(i) & 0xFF;
+ }
+ return result.buffer;
+ }
+ });
+})();
+
+// window.btoa (base64 encode function) ?
+// Support: IE<10
+(function checkWindowBtoaCompatibility() {
+ if ('btoa' in window) {
+ return;
+ }
+
+ var digits =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+
+ window.btoa = function windowBtoa(chars) {
+ var buffer = '';
+ var i, n;
+ for (i = 0, n = chars.length; i < n; i += 3) {
+ var b1 = chars.charCodeAt(i) & 0xFF;
+ var b2 = chars.charCodeAt(i + 1) & 0xFF;
+ var b3 = chars.charCodeAt(i + 2) & 0xFF;
+ var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4);
+ var d3 = i + 1 < n ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64;
+ var d4 = i + 2 < n ? (b3 & 0x3F) : 64;
+ buffer += (digits.charAt(d1) + digits.charAt(d2) +
+ digits.charAt(d3) + digits.charAt(d4));
+ }
+ return buffer;
+ };
+})();
+
+// window.atob (base64 encode function)?
+// Support: IE<10
+(function checkWindowAtobCompatibility() {
+ if ('atob' in window) {
+ return;
+ }
+
+ // https://github.com/davidchambers/Base64.js
+ var digits =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+ window.atob = function (input) {
+ input = input.replace(/=+$/, '');
+ if (input.length % 4 === 1) {
+ throw new Error('bad atob input');
+ }
+ for (
+ // initialize result and counters
+ var bc = 0, bs, buffer, idx = 0, output = '';
+ // get next character
+ buffer = input.charAt(idx++);
+ // character found in table?
+ // initialize bit storage and add its ascii value
+ ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
+ // and if not first of each 4 characters,
+ // convert the first 8 bits to one ascii character
+ bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
+ ) {
+ // try to find character in table (0-63, not found => -1)
+ buffer = digits.indexOf(buffer);
+ }
+ return output;
+ };
+})();
+
+// Function.prototype.bind?
+// Support: Android<4.0, iOS<6.0
+(function checkFunctionPrototypeBindCompatibility() {
+ if (typeof Function.prototype.bind !== 'undefined') {
+ return;
+ }
+
+ Function.prototype.bind = function functionPrototypeBind(obj) {
+ var fn = this, headArgs = Array.prototype.slice.call(arguments, 1);
+ var bound = function functionPrototypeBindBound() {
+ var args = headArgs.concat(Array.prototype.slice.call(arguments));
+ return fn.apply(obj, args);
+ };
+ return bound;
+ };
+})();
+
+// HTMLElement dataset property
+// Support: IE<11, Safari<5.1, Android<4.0
+(function checkDatasetProperty() {
+ var div = document.createElement('div');
+ if ('dataset' in div) {
+ return; // dataset property exists
+ }
+
+ Object.defineProperty(HTMLElement.prototype, 'dataset', {
+ get: function() {
+ if (this._dataset) {
+ return this._dataset;
+ }
+
+ var dataset = {};
+ for (var j = 0, jj = this.attributes.length; j < jj; j++) {
+ var attribute = this.attributes[j];
+ if (attribute.name.substring(0, 5) !== 'data-') {
+ continue;
+ }
+ var key = attribute.name.substring(5).replace(/\-([a-z])/g,
+ function(all, ch) {
+ return ch.toUpperCase();
+ });
+ dataset[key] = attribute.value;
+ }
+
+ Object.defineProperty(this, '_dataset', {
+ value: dataset,
+ writable: false,
+ enumerable: false
+ });
+ return dataset;
+ },
+ enumerable: true
+ });
+})();
+
+// HTMLElement classList property
+// Support: IE<10, Android<4.0, iOS<5.0
+(function checkClassListProperty() {
+ var div = document.createElement('div');
+ if ('classList' in div) {
+ return; // classList property exists
+ }
+
+ function changeList(element, itemName, add, remove) {
+ var s = element.className || '';
+ var list = s.split(/\s+/g);
+ if (list[0] === '') {
+ list.shift();
+ }
+ var index = list.indexOf(itemName);
+ if (index < 0 && add) {
+ list.push(itemName);
+ }
+ if (index >= 0 && remove) {
+ list.splice(index, 1);
+ }
+ element.className = list.join(' ');
+ return (index >= 0);
+ }
+
+ var classListPrototype = {
+ add: function(name) {
+ changeList(this.element, name, true, false);
+ },
+ contains: function(name) {
+ return changeList(this.element, name, false, false);
+ },
+ remove: function(name) {
+ changeList(this.element, name, false, true);
+ },
+ toggle: function(name) {
+ changeList(this.element, name, true, true);
+ }
+ };
+
+ Object.defineProperty(HTMLElement.prototype, 'classList', {
+ get: function() {
+ if (this._classList) {
+ return this._classList;
+ }
+
+ var classList = Object.create(classListPrototype, {
+ element: {
+ value: this,
+ writable: false,
+ enumerable: true
+ }
+ });
+ Object.defineProperty(this, '_classList', {
+ value: classList,
+ writable: false,
+ enumerable: false
+ });
+ return classList;
+ },
+ enumerable: true
+ });
+})();
+
+// Check console compatibility
+// In older IE versions the console object is not available
+// unless console is open.
+// Support: IE<10
+(function checkConsoleCompatibility() {
+ if (!('console' in window)) {
+ window.console = {
+ log: function() {},
+ error: function() {},
+ warn: function() {}
+ };
+ } else if (!('bind' in console.log)) {
+ // native functions in IE9 might not have bind
+ console.log = (function(fn) {
+ return function(msg) { return fn(msg); };
+ })(console.log);
+ console.error = (function(fn) {
+ return function(msg) { return fn(msg); };
+ })(console.error);
+ console.warn = (function(fn) {
+ return function(msg) { return fn(msg); };
+ })(console.warn);
+ }
+})();
+
+// Check onclick compatibility in Opera
+// Support: Opera<15
+(function checkOnClickCompatibility() {
+ // workaround for reported Opera bug DSK-354448:
+ // onclick fires on disabled buttons with opaque content
+ function ignoreIfTargetDisabled(event) {
+ if (isDisabled(event.target)) {
+ event.stopPropagation();
+ }
+ }
+ function isDisabled(node) {
+ return node.disabled || (node.parentNode && isDisabled(node.parentNode));
+ }
+ if (navigator.userAgent.indexOf('Opera') !== -1) {
+ // use browser detection since we cannot feature-check this bug
+ document.addEventListener('click', ignoreIfTargetDisabled, true);
+ }
+})();
+
+// Checks if possible to use URL.createObjectURL()
+// Support: IE
+(function checkOnBlobSupport() {
+ // sometimes IE loosing the data created with createObjectURL(), see #3977
+ if (navigator.userAgent.indexOf('Trident') >= 0) {
+ PDFJS.disableCreateObjectURL = true;
+ }
+})();
+
+// Checks if navigator.language is supported
+(function checkNavigatorLanguage() {
+ if ('language' in navigator) {
+ return;
+ }
+ PDFJS.locale = navigator.userLanguage || 'en-US';
+})();
+
+(function checkRangeRequests() {
+ // Safari has issues with cached range requests see:
+ // https://github.com/mozilla/pdf.js/issues/3260
+ // Last tested with version 6.0.4.
+ // Support: Safari 6.0+
+ var isSafari = Object.prototype.toString.call(
+ window.HTMLElement).indexOf('Constructor') > 0;
+
+ // Older versions of Android (pre 3.0) has issues with range requests, see:
+ // https://github.com/mozilla/pdf.js/issues/3381.
+ // Make sure that we only match webkit-based Android browsers,
+ // since Firefox/Fennec works as expected.
+ // Support: Android<3.0
+ var regex = /Android\s[0-2][^\d]/;
+ var isOldAndroid = regex.test(navigator.userAgent);
+
+ // Range requests are broken in Chrome 39 and 40, https://crbug.com/442318
+ var isChromeWithRangeBug = /Chrome\/(39|40)\./.test(navigator.userAgent);
+
+ if (isSafari || isOldAndroid || isChromeWithRangeBug) {
+ PDFJS.disableRange = true;
+ PDFJS.disableStream = true;
+ }
+})();
+
+// Check if the browser supports manipulation of the history.
+// Support: IE<10, Android<4.2
+(function checkHistoryManipulation() {
+ // Android 2.x has so buggy pushState support that it was removed in
+ // Android 3.0 and restored as late as in Android 4.2.
+ // Support: Android 2.x
+ if (!history.pushState || navigator.userAgent.indexOf('Android 2.') >= 0) {
+ PDFJS.disableHistory = true;
+ }
+})();
+
+// Support: IE<11, Chrome<21, Android<4.4, Safari<6
+(function checkSetPresenceInImageData() {
+ // IE < 11 will use window.CanvasPixelArray which lacks set function.
+ if (window.CanvasPixelArray) {
+ if (typeof window.CanvasPixelArray.prototype.set !== 'function') {
+ window.CanvasPixelArray.prototype.set = function(arr) {
+ for (var i = 0, ii = this.length; i < ii; i++) {
+ this[i] = arr[i];
+ }
+ };
+ }
+ } else {
+ // Old Chrome and Android use an inaccessible CanvasPixelArray prototype.
+ // Because we cannot feature detect it, we rely on user agent parsing.
+ var polyfill = false, versionMatch;
+ if (navigator.userAgent.indexOf('Chrom') >= 0) {
+ versionMatch = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
+ // Chrome < 21 lacks the set function.
+ polyfill = versionMatch && parseInt(versionMatch[2]) < 21;
+ } else if (navigator.userAgent.indexOf('Android') >= 0) {
+ // Android < 4.4 lacks the set function.
+ // Android >= 4.4 will contain Chrome in the user agent,
+ // thus pass the Chrome check above and not reach this block.
+ polyfill = /Android\s[0-4][^\d]/g.test(navigator.userAgent);
+ } else if (navigator.userAgent.indexOf('Safari') >= 0) {
+ versionMatch = navigator.userAgent.
+ match(/Version\/([0-9]+)\.([0-9]+)\.([0-9]+) Safari\//);
+ // Safari < 6 lacks the set function.
+ polyfill = versionMatch && parseInt(versionMatch[1]) < 6;
+ }
+
+ if (polyfill) {
+ var contextPrototype = window.CanvasRenderingContext2D.prototype;
+ var createImageData = contextPrototype.createImageData;
+ contextPrototype.createImageData = function(w, h) {
+ var imageData = createImageData.call(this, w, h);
+ imageData.data.set = function(arr) {
+ for (var i = 0, ii = this.length; i < ii; i++) {
+ this[i] = arr[i];
+ }
+ };
+ return imageData;
+ };
+ // this closure will be kept referenced, so clear its vars
+ contextPrototype = null;
+ }
+ }
+})();
+
+// Support: IE<10, Android<4.0, iOS
+(function checkRequestAnimationFrame() {
+ function fakeRequestAnimationFrame(callback) {
+ window.setTimeout(callback, 20);
+ }
+
+ var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
+ if (isIOS) {
+ // requestAnimationFrame on iOS is broken, replacing with fake one.
+ window.requestAnimationFrame = fakeRequestAnimationFrame;
+ return;
+ }
+ if ('requestAnimationFrame' in window) {
+ return;
+ }
+ window.requestAnimationFrame =
+ window.mozRequestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ fakeRequestAnimationFrame;
+})();
+
+(function checkCanvasSizeLimitation() {
+ var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
+ var isAndroid = /Android/g.test(navigator.userAgent);
+ if (isIOS || isAndroid) {
+ // 5MP
+ PDFJS.maxCanvasPixels = 5242880;
+ }
+})();
+
+// Disable fullscreen support for certain problematic configurations.
+// Support: IE11+ (when embedded).
+(function checkFullscreenSupport() {
+ var isEmbeddedIE = (navigator.userAgent.indexOf('Trident') >= 0 &&
+ window.parent !== window);
+ if (isEmbeddedIE) {
+ PDFJS.disableFullscreen = true;
+ }
+})();
+
+// Provides document.currentScript support
+// Support: IE, Chrome<29.
+(function checkCurrentScript() {
+ if ('currentScript' in document) {
+ return;
+ }
+ Object.defineProperty(document, 'currentScript', {
+ get: function () {
+ var scripts = document.getElementsByTagName('script');
+ return scripts[scripts.length - 1];
+ },
+ enumerable: true,
+ configurable: true
+ });
+})();
diff --git a/services/web/public/js/libs/pdfjs-1.3.91p1/pdf.js b/services/web/public/js/libs/pdfjs-1.3.91p1/pdf.js
new file mode 100644
index 0000000000..5079c5dcbf
--- /dev/null
+++ b/services/web/public/js/libs/pdfjs-1.3.91p1/pdf.js
@@ -0,0 +1,9534 @@
+/* Copyright 2012 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*jshint globalstrict: false */
+/* globals PDFJS */
+
+// Initializing PDFJS global object (if still undefined)
+if (typeof PDFJS === 'undefined') {
+ (typeof window !== 'undefined' ? window : this).PDFJS = {};
+}
+
+PDFJS.version = '1.3.91';
+PDFJS.build = 'd1e83b5';
+
+(function pdfjsWrapper() {
+ // Use strict in our context only - users might not want it
+ 'use strict';
+
+
+
+var globalScope = (typeof window === 'undefined') ? this : window;
+
+var isWorker = (typeof window === 'undefined');
+
+var FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
+
+var TextRenderingMode = {
+ FILL: 0,
+ STROKE: 1,
+ FILL_STROKE: 2,
+ INVISIBLE: 3,
+ FILL_ADD_TO_PATH: 4,
+ STROKE_ADD_TO_PATH: 5,
+ FILL_STROKE_ADD_TO_PATH: 6,
+ ADD_TO_PATH: 7,
+ FILL_STROKE_MASK: 3,
+ ADD_TO_PATH_FLAG: 4
+};
+
+var ImageKind = {
+ GRAYSCALE_1BPP: 1,
+ RGB_24BPP: 2,
+ RGBA_32BPP: 3
+};
+
+var AnnotationType = {
+ TEXT: 1,
+ LINK: 2,
+ FREETEXT: 3,
+ LINE: 4,
+ SQUARE: 5,
+ CIRCLE: 6,
+ POLYGON: 7,
+ POLYLINE: 8,
+ HIGHLIGHT: 9,
+ UNDERLINE: 10,
+ SQUIGGLY: 11,
+ STRIKEOUT: 12,
+ STAMP: 13,
+ CARET: 14,
+ INK: 15,
+ POPUP: 16,
+ FILEATTACHMENT: 17,
+ SOUND: 18,
+ MOVIE: 19,
+ WIDGET: 20,
+ SCREEN: 21,
+ PRINTERMARK: 22,
+ TRAPNET: 23,
+ WATERMARK: 24,
+ THREED: 25,
+ REDACT: 26
+};
+
+var AnnotationFlag = {
+ INVISIBLE: 0x01,
+ HIDDEN: 0x02,
+ PRINT: 0x04,
+ NOZOOM: 0x08,
+ NOROTATE: 0x10,
+ NOVIEW: 0x20,
+ READONLY: 0x40,
+ LOCKED: 0x80,
+ TOGGLENOVIEW: 0x100,
+ LOCKEDCONTENTS: 0x200
+};
+
+var AnnotationBorderStyleType = {
+ SOLID: 1,
+ DASHED: 2,
+ BEVELED: 3,
+ INSET: 4,
+ UNDERLINE: 5
+};
+
+var StreamType = {
+ UNKNOWN: 0,
+ FLATE: 1,
+ LZW: 2,
+ DCT: 3,
+ JPX: 4,
+ JBIG: 5,
+ A85: 6,
+ AHX: 7,
+ CCF: 8,
+ RL: 9
+};
+
+var FontType = {
+ UNKNOWN: 0,
+ TYPE1: 1,
+ TYPE1C: 2,
+ CIDFONTTYPE0: 3,
+ CIDFONTTYPE0C: 4,
+ TRUETYPE: 5,
+ CIDFONTTYPE2: 6,
+ TYPE3: 7,
+ OPENTYPE: 8,
+ TYPE0: 9,
+ MMTYPE1: 10
+};
+
+// The global PDFJS object exposes the API
+// In production, it will be declared outside a global wrapper
+// In development, it will be declared here
+if (!globalScope.PDFJS) {
+ globalScope.PDFJS = {};
+}
+
+globalScope.PDFJS.pdfBug = false;
+
+PDFJS.VERBOSITY_LEVELS = {
+ errors: 0,
+ warnings: 1,
+ infos: 5
+};
+
+// All the possible operations for an operator list.
+var OPS = PDFJS.OPS = {
+ // Intentionally start from 1 so it is easy to spot bad operators that will be
+ // 0's.
+ dependency: 1,
+ setLineWidth: 2,
+ setLineCap: 3,
+ setLineJoin: 4,
+ setMiterLimit: 5,
+ setDash: 6,
+ setRenderingIntent: 7,
+ setFlatness: 8,
+ setGState: 9,
+ save: 10,
+ restore: 11,
+ transform: 12,
+ moveTo: 13,
+ lineTo: 14,
+ curveTo: 15,
+ curveTo2: 16,
+ curveTo3: 17,
+ closePath: 18,
+ rectangle: 19,
+ stroke: 20,
+ closeStroke: 21,
+ fill: 22,
+ eoFill: 23,
+ fillStroke: 24,
+ eoFillStroke: 25,
+ closeFillStroke: 26,
+ closeEOFillStroke: 27,
+ endPath: 28,
+ clip: 29,
+ eoClip: 30,
+ beginText: 31,
+ endText: 32,
+ setCharSpacing: 33,
+ setWordSpacing: 34,
+ setHScale: 35,
+ setLeading: 36,
+ setFont: 37,
+ setTextRenderingMode: 38,
+ setTextRise: 39,
+ moveText: 40,
+ setLeadingMoveText: 41,
+ setTextMatrix: 42,
+ nextLine: 43,
+ showText: 44,
+ showSpacedText: 45,
+ nextLineShowText: 46,
+ nextLineSetSpacingShowText: 47,
+ setCharWidth: 48,
+ setCharWidthAndBounds: 49,
+ setStrokeColorSpace: 50,
+ setFillColorSpace: 51,
+ setStrokeColor: 52,
+ setStrokeColorN: 53,
+ setFillColor: 54,
+ setFillColorN: 55,
+ setStrokeGray: 56,
+ setFillGray: 57,
+ setStrokeRGBColor: 58,
+ setFillRGBColor: 59,
+ setStrokeCMYKColor: 60,
+ setFillCMYKColor: 61,
+ shadingFill: 62,
+ beginInlineImage: 63,
+ beginImageData: 64,
+ endInlineImage: 65,
+ paintXObject: 66,
+ markPoint: 67,
+ markPointProps: 68,
+ beginMarkedContent: 69,
+ beginMarkedContentProps: 70,
+ endMarkedContent: 71,
+ beginCompat: 72,
+ endCompat: 73,
+ paintFormXObjectBegin: 74,
+ paintFormXObjectEnd: 75,
+ beginGroup: 76,
+ endGroup: 77,
+ beginAnnotations: 78,
+ endAnnotations: 79,
+ beginAnnotation: 80,
+ endAnnotation: 81,
+ paintJpegXObject: 82,
+ paintImageMaskXObject: 83,
+ paintImageMaskXObjectGroup: 84,
+ paintImageXObject: 85,
+ paintInlineImageXObject: 86,
+ paintInlineImageXObjectGroup: 87,
+ paintImageXObjectRepeat: 88,
+ paintImageMaskXObjectRepeat: 89,
+ paintSolidColorImageMask: 90,
+ constructPath: 91
+};
+
+// A notice for devs. These are good for things that are helpful to devs, such
+// as warning that Workers were disabled, which is important to devs but not
+// end users.
+function info(msg) {
+ if (PDFJS.verbosity >= PDFJS.VERBOSITY_LEVELS.infos) {
+ console.log('Info: ' + msg);
+ }
+}
+
+// Non-fatal warnings.
+function warn(msg) {
+ if (PDFJS.verbosity >= PDFJS.VERBOSITY_LEVELS.warnings) {
+ console.log('Warning: ' + msg);
+ }
+}
+
+// Deprecated API function -- treated as warnings.
+function deprecated(details) {
+ warn('Deprecated API usage: ' + details);
+}
+
+// Fatal errors that should trigger the fallback UI and halt execution by
+// throwing an exception.
+function error(msg) {
+ if (PDFJS.verbosity >= PDFJS.VERBOSITY_LEVELS.errors) {
+ console.log('Error: ' + msg);
+ console.log(backtrace());
+ }
+ throw new Error(msg);
+}
+
+function backtrace() {
+ try {
+ throw new Error();
+ } catch (e) {
+ return e.stack ? e.stack.split('\n').slice(2).join('\n') : '';
+ }
+}
+
+function assert(cond, msg) {
+ if (!cond) {
+ error(msg);
+ }
+}
+
+var UNSUPPORTED_FEATURES = PDFJS.UNSUPPORTED_FEATURES = {
+ unknown: 'unknown',
+ forms: 'forms',
+ javaScript: 'javaScript',
+ smask: 'smask',
+ shadingPattern: 'shadingPattern',
+ font: 'font'
+};
+
+// Combines two URLs. The baseUrl shall be absolute URL. If the url is an
+// absolute URL, it will be returned as is.
+function combineUrl(baseUrl, url) {
+ if (!url) {
+ return baseUrl;
+ }
+ return new URL(url, baseUrl).href;
+}
+
+// Validates if URL is safe and allowed, e.g. to avoid XSS.
+function isValidUrl(url, allowRelative) {
+ if (!url) {
+ return false;
+ }
+ // RFC 3986 (http://tools.ietf.org/html/rfc3986#section-3.1)
+ // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
+ var protocol = /^[a-z][a-z0-9+\-.]*(?=:)/i.exec(url);
+ if (!protocol) {
+ return allowRelative;
+ }
+ protocol = protocol[0].toLowerCase();
+ switch (protocol) {
+ case 'http':
+ case 'https':
+ case 'ftp':
+ case 'mailto':
+ case 'tel':
+ return true;
+ default:
+ return false;
+ }
+}
+PDFJS.isValidUrl = isValidUrl;
+
+function shadow(obj, prop, value) {
+ Object.defineProperty(obj, prop, { value: value,
+ enumerable: true,
+ configurable: true,
+ writable: false });
+ return value;
+}
+PDFJS.shadow = shadow;
+
+var LinkTarget = PDFJS.LinkTarget = {
+ NONE: 0, // Default value.
+ SELF: 1,
+ BLANK: 2,
+ PARENT: 3,
+ TOP: 4,
+};
+var LinkTargetStringMap = [
+ '',
+ '_self',
+ '_blank',
+ '_parent',
+ '_top'
+];
+
+function isExternalLinkTargetSet() {
+ if (PDFJS.openExternalLinksInNewWindow) {
+ deprecated('PDFJS.openExternalLinksInNewWindow, please use ' +
+ '"PDFJS.externalLinkTarget = PDFJS.LinkTarget.BLANK" instead.');
+ if (PDFJS.externalLinkTarget === LinkTarget.NONE) {
+ PDFJS.externalLinkTarget = LinkTarget.BLANK;
+ }
+ // Reset the deprecated parameter, to suppress further warnings.
+ PDFJS.openExternalLinksInNewWindow = false;
+ }
+ switch (PDFJS.externalLinkTarget) {
+ case LinkTarget.NONE:
+ return false;
+ case LinkTarget.SELF:
+ case LinkTarget.BLANK:
+ case LinkTarget.PARENT:
+ case LinkTarget.TOP:
+ return true;
+ }
+ warn('PDFJS.externalLinkTarget is invalid: ' + PDFJS.externalLinkTarget);
+ // Reset the external link target, to suppress further warnings.
+ PDFJS.externalLinkTarget = LinkTarget.NONE;
+ return false;
+}
+PDFJS.isExternalLinkTargetSet = isExternalLinkTargetSet;
+
+var PasswordResponses = PDFJS.PasswordResponses = {
+ NEED_PASSWORD: 1,
+ INCORRECT_PASSWORD: 2
+};
+
+var PasswordException = (function PasswordExceptionClosure() {
+ function PasswordException(msg, code) {
+ this.name = 'PasswordException';
+ this.message = msg;
+ this.code = code;
+ }
+
+ PasswordException.prototype = new Error();
+ PasswordException.constructor = PasswordException;
+
+ return PasswordException;
+})();
+PDFJS.PasswordException = PasswordException;
+
+var UnknownErrorException = (function UnknownErrorExceptionClosure() {
+ function UnknownErrorException(msg, details) {
+ this.name = 'UnknownErrorException';
+ this.message = msg;
+ this.details = details;
+ }
+
+ UnknownErrorException.prototype = new Error();
+ UnknownErrorException.constructor = UnknownErrorException;
+
+ return UnknownErrorException;
+})();
+PDFJS.UnknownErrorException = UnknownErrorException;
+
+var InvalidPDFException = (function InvalidPDFExceptionClosure() {
+ function InvalidPDFException(msg) {
+ this.name = 'InvalidPDFException';
+ this.message = msg;
+ }
+
+ InvalidPDFException.prototype = new Error();
+ InvalidPDFException.constructor = InvalidPDFException;
+
+ return InvalidPDFException;
+})();
+PDFJS.InvalidPDFException = InvalidPDFException;
+
+var MissingPDFException = (function MissingPDFExceptionClosure() {
+ function MissingPDFException(msg) {
+ this.name = 'MissingPDFException';
+ this.message = msg;
+ }
+
+ MissingPDFException.prototype = new Error();
+ MissingPDFException.constructor = MissingPDFException;
+
+ return MissingPDFException;
+})();
+PDFJS.MissingPDFException = MissingPDFException;
+
+var UnexpectedResponseException =
+ (function UnexpectedResponseExceptionClosure() {
+ function UnexpectedResponseException(msg, status) {
+ this.name = 'UnexpectedResponseException';
+ this.message = msg;
+ this.status = status;
+ }
+
+ UnexpectedResponseException.prototype = new Error();
+ UnexpectedResponseException.constructor = UnexpectedResponseException;
+
+ return UnexpectedResponseException;
+})();
+PDFJS.UnexpectedResponseException = UnexpectedResponseException;
+
+var NotImplementedException = (function NotImplementedExceptionClosure() {
+ function NotImplementedException(msg) {
+ this.message = msg;
+ }
+
+ NotImplementedException.prototype = new Error();
+ NotImplementedException.prototype.name = 'NotImplementedException';
+ NotImplementedException.constructor = NotImplementedException;
+
+ return NotImplementedException;
+})();
+
+var MissingDataException = (function MissingDataExceptionClosure() {
+ function MissingDataException(begin, end) {
+ this.begin = begin;
+ this.end = end;
+ this.message = 'Missing data [' + begin + ', ' + end + ')';
+ }
+
+ MissingDataException.prototype = new Error();
+ MissingDataException.prototype.name = 'MissingDataException';
+ MissingDataException.constructor = MissingDataException;
+
+ return MissingDataException;
+})();
+
+var XRefParseException = (function XRefParseExceptionClosure() {
+ function XRefParseException(msg) {
+ this.message = msg;
+ }
+
+ XRefParseException.prototype = new Error();
+ XRefParseException.prototype.name = 'XRefParseException';
+ XRefParseException.constructor = XRefParseException;
+
+ return XRefParseException;
+})();
+
+
+function bytesToString(bytes) {
+ assert(bytes !== null && typeof bytes === 'object' &&
+ bytes.length !== undefined, 'Invalid argument for bytesToString');
+ var length = bytes.length;
+ var MAX_ARGUMENT_COUNT = 8192;
+ if (length < MAX_ARGUMENT_COUNT) {
+ return String.fromCharCode.apply(null, bytes);
+ }
+ var strBuf = [];
+ for (var i = 0; i < length; i += MAX_ARGUMENT_COUNT) {
+ var chunkEnd = Math.min(i + MAX_ARGUMENT_COUNT, length);
+ var chunk = bytes.subarray(i, chunkEnd);
+ strBuf.push(String.fromCharCode.apply(null, chunk));
+ }
+ return strBuf.join('');
+}
+
+function stringToBytes(str) {
+ assert(typeof str === 'string', 'Invalid argument for stringToBytes');
+ var length = str.length;
+ var bytes = new Uint8Array(length);
+ for (var i = 0; i < length; ++i) {
+ bytes[i] = str.charCodeAt(i) & 0xFF;
+ }
+ return bytes;
+}
+
+function string32(value) {
+ return String.fromCharCode((value >> 24) & 0xff, (value >> 16) & 0xff,
+ (value >> 8) & 0xff, value & 0xff);
+}
+
+function log2(x) {
+ var n = 1, i = 0;
+ while (x > n) {
+ n <<= 1;
+ i++;
+ }
+ return i;
+}
+
+function readInt8(data, start) {
+ return (data[start] << 24) >> 24;
+}
+
+function readUint16(data, offset) {
+ return (data[offset] << 8) | data[offset + 1];
+}
+
+function readUint32(data, offset) {
+ return ((data[offset] << 24) | (data[offset + 1] << 16) |
+ (data[offset + 2] << 8) | data[offset + 3]) >>> 0;
+}
+
+// Lazy test the endianness of the platform
+// NOTE: This will be 'true' for simulated TypedArrays
+function isLittleEndian() {
+ var buffer8 = new Uint8Array(2);
+ buffer8[0] = 1;
+ var buffer16 = new Uint16Array(buffer8.buffer);
+ return (buffer16[0] === 1);
+}
+
+Object.defineProperty(PDFJS, 'isLittleEndian', {
+ configurable: true,
+ get: function PDFJS_isLittleEndian() {
+ return shadow(PDFJS, 'isLittleEndian', isLittleEndian());
+ }
+});
+
+ // Lazy test if the userAgent support CanvasTypedArrays
+function hasCanvasTypedArrays() {
+ var canvas = document.createElement('canvas');
+ canvas.width = canvas.height = 1;
+ var ctx = canvas.getContext('2d');
+ var imageData = ctx.createImageData(1, 1);
+ return (typeof imageData.data.buffer !== 'undefined');
+}
+
+Object.defineProperty(PDFJS, 'hasCanvasTypedArrays', {
+ configurable: true,
+ get: function PDFJS_hasCanvasTypedArrays() {
+ return shadow(PDFJS, 'hasCanvasTypedArrays', hasCanvasTypedArrays());
+ }
+});
+
+var Uint32ArrayView = (function Uint32ArrayViewClosure() {
+
+ function Uint32ArrayView(buffer, length) {
+ this.buffer = buffer;
+ this.byteLength = buffer.length;
+ this.length = length === undefined ? (this.byteLength >> 2) : length;
+ ensureUint32ArrayViewProps(this.length);
+ }
+ Uint32ArrayView.prototype = Object.create(null);
+
+ var uint32ArrayViewSetters = 0;
+ function createUint32ArrayProp(index) {
+ return {
+ get: function () {
+ var buffer = this.buffer, offset = index << 2;
+ return (buffer[offset] | (buffer[offset + 1] << 8) |
+ (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24)) >>> 0;
+ },
+ set: function (value) {
+ var buffer = this.buffer, offset = index << 2;
+ buffer[offset] = value & 255;
+ buffer[offset + 1] = (value >> 8) & 255;
+ buffer[offset + 2] = (value >> 16) & 255;
+ buffer[offset + 3] = (value >>> 24) & 255;
+ }
+ };
+ }
+
+ function ensureUint32ArrayViewProps(length) {
+ while (uint32ArrayViewSetters < length) {
+ Object.defineProperty(Uint32ArrayView.prototype,
+ uint32ArrayViewSetters,
+ createUint32ArrayProp(uint32ArrayViewSetters));
+ uint32ArrayViewSetters++;
+ }
+ }
+
+ return Uint32ArrayView;
+})();
+
+var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
+
+var Util = PDFJS.Util = (function UtilClosure() {
+ function Util() {}
+
+ var rgbBuf = ['rgb(', 0, ',', 0, ',', 0, ')'];
+
+ // makeCssRgb() can be called thousands of times. Using |rgbBuf| avoids
+ // creating many intermediate strings.
+ Util.makeCssRgb = function Util_makeCssRgb(r, g, b) {
+ rgbBuf[1] = r;
+ rgbBuf[3] = g;
+ rgbBuf[5] = b;
+ return rgbBuf.join('');
+ };
+
+ // Concatenates two transformation matrices together and returns the result.
+ Util.transform = function Util_transform(m1, m2) {
+ return [
+ m1[0] * m2[0] + m1[2] * m2[1],
+ m1[1] * m2[0] + m1[3] * m2[1],
+ m1[0] * m2[2] + m1[2] * m2[3],
+ m1[1] * m2[2] + m1[3] * m2[3],
+ m1[0] * m2[4] + m1[2] * m2[5] + m1[4],
+ m1[1] * m2[4] + m1[3] * m2[5] + m1[5]
+ ];
+ };
+
+ // For 2d affine transforms
+ Util.applyTransform = function Util_applyTransform(p, m) {
+ var xt = p[0] * m[0] + p[1] * m[2] + m[4];
+ var yt = p[0] * m[1] + p[1] * m[3] + m[5];
+ return [xt, yt];
+ };
+
+ Util.applyInverseTransform = function Util_applyInverseTransform(p, m) {
+ var d = m[0] * m[3] - m[1] * m[2];
+ var xt = (p[0] * m[3] - p[1] * m[2] + m[2] * m[5] - m[4] * m[3]) / d;
+ var yt = (-p[0] * m[1] + p[1] * m[0] + m[4] * m[1] - m[5] * m[0]) / d;
+ return [xt, yt];
+ };
+
+ // Applies the transform to the rectangle and finds the minimum axially
+ // aligned bounding box.
+ Util.getAxialAlignedBoundingBox =
+ function Util_getAxialAlignedBoundingBox(r, m) {
+
+ var p1 = Util.applyTransform(r, m);
+ var p2 = Util.applyTransform(r.slice(2, 4), m);
+ var p3 = Util.applyTransform([r[0], r[3]], m);
+ var p4 = Util.applyTransform([r[2], r[1]], m);
+ return [
+ Math.min(p1[0], p2[0], p3[0], p4[0]),
+ Math.min(p1[1], p2[1], p3[1], p4[1]),
+ Math.max(p1[0], p2[0], p3[0], p4[0]),
+ Math.max(p1[1], p2[1], p3[1], p4[1])
+ ];
+ };
+
+ Util.inverseTransform = function Util_inverseTransform(m) {
+ var d = m[0] * m[3] - m[1] * m[2];
+ return [m[3] / d, -m[1] / d, -m[2] / d, m[0] / d,
+ (m[2] * m[5] - m[4] * m[3]) / d, (m[4] * m[1] - m[5] * m[0]) / d];
+ };
+
+ // Apply a generic 3d matrix M on a 3-vector v:
+ // | a b c | | X |
+ // | d e f | x | Y |
+ // | g h i | | Z |
+ // M is assumed to be serialized as [a,b,c,d,e,f,g,h,i],
+ // with v as [X,Y,Z]
+ Util.apply3dTransform = function Util_apply3dTransform(m, v) {
+ return [
+ m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
+ m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
+ m[6] * v[0] + m[7] * v[1] + m[8] * v[2]
+ ];
+ };
+
+ // This calculation uses Singular Value Decomposition.
+ // The SVD can be represented with formula A = USV. We are interested in the
+ // matrix S here because it represents the scale values.
+ Util.singularValueDecompose2dScale =
+ function Util_singularValueDecompose2dScale(m) {
+
+ var transpose = [m[0], m[2], m[1], m[3]];
+
+ // Multiply matrix m with its transpose.
+ var a = m[0] * transpose[0] + m[1] * transpose[2];
+ var b = m[0] * transpose[1] + m[1] * transpose[3];
+ var c = m[2] * transpose[0] + m[3] * transpose[2];
+ var d = m[2] * transpose[1] + m[3] * transpose[3];
+
+ // Solve the second degree polynomial to get roots.
+ var first = (a + d) / 2;
+ var second = Math.sqrt((a + d) * (a + d) - 4 * (a * d - c * b)) / 2;
+ var sx = first + second || 1;
+ var sy = first - second || 1;
+
+ // Scale values are the square roots of the eigenvalues.
+ return [Math.sqrt(sx), Math.sqrt(sy)];
+ };
+
+ // Normalize rectangle rect=[x1, y1, x2, y2] so that (x1,y1) < (x2,y2)
+ // For coordinate systems whose origin lies in the bottom-left, this
+ // means normalization to (BL,TR) ordering. For systems with origin in the
+ // top-left, this means (TL,BR) ordering.
+ Util.normalizeRect = function Util_normalizeRect(rect) {
+ var r = rect.slice(0); // clone rect
+ if (rect[0] > rect[2]) {
+ r[0] = rect[2];
+ r[2] = rect[0];
+ }
+ if (rect[1] > rect[3]) {
+ r[1] = rect[3];
+ r[3] = rect[1];
+ }
+ return r;
+ };
+
+ // Returns a rectangle [x1, y1, x2, y2] corresponding to the
+ // intersection of rect1 and rect2. If no intersection, returns 'false'
+ // The rectangle coordinates of rect1, rect2 should be [x1, y1, x2, y2]
+ Util.intersect = function Util_intersect(rect1, rect2) {
+ function compare(a, b) {
+ return a - b;
+ }
+
+ // Order points along the axes
+ var orderedX = [rect1[0], rect1[2], rect2[0], rect2[2]].sort(compare),
+ orderedY = [rect1[1], rect1[3], rect2[1], rect2[3]].sort(compare),
+ result = [];
+
+ rect1 = Util.normalizeRect(rect1);
+ rect2 = Util.normalizeRect(rect2);
+
+ // X: first and second points belong to different rectangles?
+ if ((orderedX[0] === rect1[0] && orderedX[1] === rect2[0]) ||
+ (orderedX[0] === rect2[0] && orderedX[1] === rect1[0])) {
+ // Intersection must be between second and third points
+ result[0] = orderedX[1];
+ result[2] = orderedX[2];
+ } else {
+ return false;
+ }
+
+ // Y: first and second points belong to different rectangles?
+ if ((orderedY[0] === rect1[1] && orderedY[1] === rect2[1]) ||
+ (orderedY[0] === rect2[1] && orderedY[1] === rect1[1])) {
+ // Intersection must be between second and third points
+ result[1] = orderedY[1];
+ result[3] = orderedY[2];
+ } else {
+ return false;
+ }
+
+ return result;
+ };
+
+ Util.sign = function Util_sign(num) {
+ return num < 0 ? -1 : 1;
+ };
+
+ Util.appendToArray = function Util_appendToArray(arr1, arr2) {
+ Array.prototype.push.apply(arr1, arr2);
+ };
+
+ Util.prependToArray = function Util_prependToArray(arr1, arr2) {
+ Array.prototype.unshift.apply(arr1, arr2);
+ };
+
+ Util.extendObj = function extendObj(obj1, obj2) {
+ for (var key in obj2) {
+ obj1[key] = obj2[key];
+ }
+ };
+
+ Util.getInheritableProperty = function Util_getInheritableProperty(dict,
+ name) {
+ while (dict && !dict.has(name)) {
+ dict = dict.get('Parent');
+ }
+ if (!dict) {
+ return null;
+ }
+ return dict.get(name);
+ };
+
+ Util.inherit = function Util_inherit(sub, base, prototype) {
+ sub.prototype = Object.create(base.prototype);
+ sub.prototype.constructor = sub;
+ for (var prop in prototype) {
+ sub.prototype[prop] = prototype[prop];
+ }
+ };
+
+ Util.loadScript = function Util_loadScript(src, callback) {
+ var script = document.createElement('script');
+ var loaded = false;
+ script.setAttribute('src', src);
+ if (callback) {
+ script.onload = function() {
+ if (!loaded) {
+ callback();
+ }
+ loaded = true;
+ };
+ }
+ document.getElementsByTagName('head')[0].appendChild(script);
+ };
+
+ return Util;
+})();
+
+/**
+ * PDF page viewport created based on scale, rotation and offset.
+ * @class
+ * @alias PDFJS.PageViewport
+ */
+var PageViewport = PDFJS.PageViewport = (function PageViewportClosure() {
+ /**
+ * @constructor
+ * @private
+ * @param viewBox {Array} xMin, yMin, xMax and yMax coordinates.
+ * @param scale {number} scale of the viewport.
+ * @param rotation {number} rotations of the viewport in degrees.
+ * @param offsetX {number} offset X
+ * @param offsetY {number} offset Y
+ * @param dontFlip {boolean} if true, axis Y will not be flipped.
+ */
+ function PageViewport(viewBox, scale, rotation, offsetX, offsetY, dontFlip) {
+ this.viewBox = viewBox;
+ this.scale = scale;
+ this.rotation = rotation;
+ this.offsetX = offsetX;
+ this.offsetY = offsetY;
+
+ // creating transform to convert pdf coordinate system to the normal
+ // canvas like coordinates taking in account scale and rotation
+ var centerX = (viewBox[2] + viewBox[0]) / 2;
+ var centerY = (viewBox[3] + viewBox[1]) / 2;
+ var rotateA, rotateB, rotateC, rotateD;
+ rotation = rotation % 360;
+ rotation = rotation < 0 ? rotation + 360 : rotation;
+ switch (rotation) {
+ case 180:
+ rotateA = -1; rotateB = 0; rotateC = 0; rotateD = 1;
+ break;
+ case 90:
+ rotateA = 0; rotateB = 1; rotateC = 1; rotateD = 0;
+ break;
+ case 270:
+ rotateA = 0; rotateB = -1; rotateC = -1; rotateD = 0;
+ break;
+ //case 0:
+ default:
+ rotateA = 1; rotateB = 0; rotateC = 0; rotateD = -1;
+ break;
+ }
+
+ if (dontFlip) {
+ rotateC = -rotateC; rotateD = -rotateD;
+ }
+
+ var offsetCanvasX, offsetCanvasY;
+ var width, height;
+ if (rotateA === 0) {
+ offsetCanvasX = Math.abs(centerY - viewBox[1]) * scale + offsetX;
+ offsetCanvasY = Math.abs(centerX - viewBox[0]) * scale + offsetY;
+ width = Math.abs(viewBox[3] - viewBox[1]) * scale;
+ height = Math.abs(viewBox[2] - viewBox[0]) * scale;
+ } else {
+ offsetCanvasX = Math.abs(centerX - viewBox[0]) * scale + offsetX;
+ offsetCanvasY = Math.abs(centerY - viewBox[1]) * scale + offsetY;
+ width = Math.abs(viewBox[2] - viewBox[0]) * scale;
+ height = Math.abs(viewBox[3] - viewBox[1]) * scale;
+ }
+ // creating transform for the following operations:
+ // translate(-centerX, -centerY), rotate and flip vertically,
+ // scale, and translate(offsetCanvasX, offsetCanvasY)
+ this.transform = [
+ rotateA * scale,
+ rotateB * scale,
+ rotateC * scale,
+ rotateD * scale,
+ offsetCanvasX - rotateA * scale * centerX - rotateC * scale * centerY,
+ offsetCanvasY - rotateB * scale * centerX - rotateD * scale * centerY
+ ];
+
+ this.width = width;
+ this.height = height;
+ this.fontScale = scale;
+ }
+ PageViewport.prototype = /** @lends PDFJS.PageViewport.prototype */ {
+ /**
+ * Clones viewport with additional properties.
+ * @param args {Object} (optional) If specified, may contain the 'scale' or
+ * 'rotation' properties to override the corresponding properties in
+ * the cloned viewport.
+ * @returns {PDFJS.PageViewport} Cloned viewport.
+ */
+ clone: function PageViewPort_clone(args) {
+ args = args || {};
+ var scale = 'scale' in args ? args.scale : this.scale;
+ var rotation = 'rotation' in args ? args.rotation : this.rotation;
+ return new PageViewport(this.viewBox.slice(), scale, rotation,
+ this.offsetX, this.offsetY, args.dontFlip);
+ },
+ /**
+ * Converts PDF point to the viewport coordinates. For examples, useful for
+ * converting PDF location into canvas pixel coordinates.
+ * @param x {number} X coordinate.
+ * @param y {number} Y coordinate.
+ * @returns {Object} Object that contains 'x' and 'y' properties of the
+ * point in the viewport coordinate space.
+ * @see {@link convertToPdfPoint}
+ * @see {@link convertToViewportRectangle}
+ */
+ convertToViewportPoint: function PageViewport_convertToViewportPoint(x, y) {
+ return Util.applyTransform([x, y], this.transform);
+ },
+ /**
+ * Converts PDF rectangle to the viewport coordinates.
+ * @param rect {Array} xMin, yMin, xMax and yMax coordinates.
+ * @returns {Array} Contains corresponding coordinates of the rectangle
+ * in the viewport coordinate space.
+ * @see {@link convertToViewportPoint}
+ */
+ convertToViewportRectangle:
+ function PageViewport_convertToViewportRectangle(rect) {
+ var tl = Util.applyTransform([rect[0], rect[1]], this.transform);
+ var br = Util.applyTransform([rect[2], rect[3]], this.transform);
+ return [tl[0], tl[1], br[0], br[1]];
+ },
+ /**
+ * Converts viewport coordinates to the PDF location. For examples, useful
+ * for converting canvas pixel location into PDF one.
+ * @param x {number} X coordinate.
+ * @param y {number} Y coordinate.
+ * @returns {Object} Object that contains 'x' and 'y' properties of the
+ * point in the PDF coordinate space.
+ * @see {@link convertToViewportPoint}
+ */
+ convertToPdfPoint: function PageViewport_convertToPdfPoint(x, y) {
+ return Util.applyInverseTransform([x, y], this.transform);
+ }
+ };
+ return PageViewport;
+})();
+
+var PDFStringTranslateTable = [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0x2D8, 0x2C7, 0x2C6, 0x2D9, 0x2DD, 0x2DB, 0x2DA, 0x2DC, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2022, 0x2020, 0x2021, 0x2026, 0x2014,
+ 0x2013, 0x192, 0x2044, 0x2039, 0x203A, 0x2212, 0x2030, 0x201E, 0x201C,
+ 0x201D, 0x2018, 0x2019, 0x201A, 0x2122, 0xFB01, 0xFB02, 0x141, 0x152, 0x160,
+ 0x178, 0x17D, 0x131, 0x142, 0x153, 0x161, 0x17E, 0, 0x20AC
+];
+
+function stringToPDFString(str) {
+ var i, n = str.length, strBuf = [];
+ if (str[0] === '\xFE' && str[1] === '\xFF') {
+ // UTF16BE BOM
+ for (i = 2; i < n; i += 2) {
+ strBuf.push(String.fromCharCode(
+ (str.charCodeAt(i) << 8) | str.charCodeAt(i + 1)));
+ }
+ } else {
+ for (i = 0; i < n; ++i) {
+ var code = PDFStringTranslateTable[str.charCodeAt(i)];
+ strBuf.push(code ? String.fromCharCode(code) : str.charAt(i));
+ }
+ }
+ return strBuf.join('');
+}
+
+function stringToUTF8String(str) {
+ return decodeURIComponent(escape(str));
+}
+
+function utf8StringToString(str) {
+ return unescape(encodeURIComponent(str));
+}
+
+function isEmptyObj(obj) {
+ for (var key in obj) {
+ return false;
+ }
+ return true;
+}
+
+function isBool(v) {
+ return typeof v === 'boolean';
+}
+
+function isInt(v) {
+ return typeof v === 'number' && ((v | 0) === v);
+}
+
+function isNum(v) {
+ return typeof v === 'number';
+}
+
+function isString(v) {
+ return typeof v === 'string';
+}
+
+function isName(v) {
+ return v instanceof Name;
+}
+
+function isCmd(v, cmd) {
+ return v instanceof Cmd && (cmd === undefined || v.cmd === cmd);
+}
+
+function isDict(v, type) {
+ if (!(v instanceof Dict)) {
+ return false;
+ }
+ if (!type) {
+ return true;
+ }
+ var dictType = v.get('Type');
+ return isName(dictType) && dictType.name === type;
+}
+
+function isArray(v) {
+ return v instanceof Array;
+}
+
+function isStream(v) {
+ return typeof v === 'object' && v !== null && v.getBytes !== undefined;
+}
+
+function isArrayBuffer(v) {
+ return typeof v === 'object' && v !== null && v.byteLength !== undefined;
+}
+
+function isRef(v) {
+ return v instanceof Ref;
+}
+
+/**
+ * Promise Capability object.
+ *
+ * @typedef {Object} PromiseCapability
+ * @property {Promise} promise - A promise object.
+ * @property {function} resolve - Fullfills the promise.
+ * @property {function} reject - Rejects the promise.
+ */
+
+/**
+ * Creates a promise capability object.
+ * @alias PDFJS.createPromiseCapability
+ *
+ * @return {PromiseCapability} A capability object contains:
+ * - a Promise, resolve and reject methods.
+ */
+function createPromiseCapability() {
+ var capability = {};
+ capability.promise = new Promise(function (resolve, reject) {
+ capability.resolve = resolve;
+ capability.reject = reject;
+ });
+ return capability;
+}
+
+PDFJS.createPromiseCapability = createPromiseCapability;
+
+/**
+ * Polyfill for Promises:
+ * The following promise implementation tries to generally implement the
+ * Promise/A+ spec. Some notable differences from other promise libaries are:
+ * - There currently isn't a seperate deferred and promise object.
+ * - Unhandled rejections eventually show an error if they aren't handled.
+ *
+ * Based off of the work in:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=810490
+ */
+(function PromiseClosure() {
+ if (globalScope.Promise) {
+ // Promises existing in the DOM/Worker, checking presence of all/resolve
+ if (typeof globalScope.Promise.all !== 'function') {
+ globalScope.Promise.all = function (iterable) {
+ var count = 0, results = [], resolve, reject;
+ var promise = new globalScope.Promise(function (resolve_, reject_) {
+ resolve = resolve_;
+ reject = reject_;
+ });
+ iterable.forEach(function (p, i) {
+ count++;
+ p.then(function (result) {
+ results[i] = result;
+ count--;
+ if (count === 0) {
+ resolve(results);
+ }
+ }, reject);
+ });
+ if (count === 0) {
+ resolve(results);
+ }
+ return promise;
+ };
+ }
+ if (typeof globalScope.Promise.resolve !== 'function') {
+ globalScope.Promise.resolve = function (value) {
+ return new globalScope.Promise(function (resolve) { resolve(value); });
+ };
+ }
+ if (typeof globalScope.Promise.reject !== 'function') {
+ globalScope.Promise.reject = function (reason) {
+ return new globalScope.Promise(function (resolve, reject) {
+ reject(reason);
+ });
+ };
+ }
+ if (typeof globalScope.Promise.prototype.catch !== 'function') {
+ globalScope.Promise.prototype.catch = function (onReject) {
+ return globalScope.Promise.prototype.then(undefined, onReject);
+ };
+ }
+ return;
+ }
+ var STATUS_PENDING = 0;
+ var STATUS_RESOLVED = 1;
+ var STATUS_REJECTED = 2;
+
+ // In an attempt to avoid silent exceptions, unhandled rejections are
+ // tracked and if they aren't handled in a certain amount of time an
+ // error is logged.
+ var REJECTION_TIMEOUT = 500;
+
+ var HandlerManager = {
+ handlers: [],
+ running: false,
+ unhandledRejections: [],
+ pendingRejectionCheck: false,
+
+ scheduleHandlers: function scheduleHandlers(promise) {
+ if (promise._status === STATUS_PENDING) {
+ return;
+ }
+
+ this.handlers = this.handlers.concat(promise._handlers);
+ promise._handlers = [];
+
+ if (this.running) {
+ return;
+ }
+ this.running = true;
+
+ setTimeout(this.runHandlers.bind(this), 0);
+ },
+
+ runHandlers: function runHandlers() {
+ var RUN_TIMEOUT = 1; // ms
+ var timeoutAt = Date.now() + RUN_TIMEOUT;
+ while (this.handlers.length > 0) {
+ var handler = this.handlers.shift();
+
+ var nextStatus = handler.thisPromise._status;
+ var nextValue = handler.thisPromise._value;
+
+ try {
+ if (nextStatus === STATUS_RESOLVED) {
+ if (typeof handler.onResolve === 'function') {
+ nextValue = handler.onResolve(nextValue);
+ }
+ } else if (typeof handler.onReject === 'function') {
+ nextValue = handler.onReject(nextValue);
+ nextStatus = STATUS_RESOLVED;
+
+ if (handler.thisPromise._unhandledRejection) {
+ this.removeUnhandeledRejection(handler.thisPromise);
+ }
+ }
+ } catch (ex) {
+ nextStatus = STATUS_REJECTED;
+ nextValue = ex;
+ }
+
+ handler.nextPromise._updateStatus(nextStatus, nextValue);
+ if (Date.now() >= timeoutAt) {
+ break;
+ }
+ }
+
+ if (this.handlers.length > 0) {
+ setTimeout(this.runHandlers.bind(this), 0);
+ return;
+ }
+
+ this.running = false;
+ },
+
+ addUnhandledRejection: function addUnhandledRejection(promise) {
+ this.unhandledRejections.push({
+ promise: promise,
+ time: Date.now()
+ });
+ this.scheduleRejectionCheck();
+ },
+
+ removeUnhandeledRejection: function removeUnhandeledRejection(promise) {
+ promise._unhandledRejection = false;
+ for (var i = 0; i < this.unhandledRejections.length; i++) {
+ if (this.unhandledRejections[i].promise === promise) {
+ this.unhandledRejections.splice(i);
+ i--;
+ }
+ }
+ },
+
+ scheduleRejectionCheck: function scheduleRejectionCheck() {
+ if (this.pendingRejectionCheck) {
+ return;
+ }
+ this.pendingRejectionCheck = true;
+ setTimeout(function rejectionCheck() {
+ this.pendingRejectionCheck = false;
+ var now = Date.now();
+ for (var i = 0; i < this.unhandledRejections.length; i++) {
+ if (now - this.unhandledRejections[i].time > REJECTION_TIMEOUT) {
+ var unhandled = this.unhandledRejections[i].promise._value;
+ var msg = 'Unhandled rejection: ' + unhandled;
+ if (unhandled.stack) {
+ msg += '\n' + unhandled.stack;
+ }
+ warn(msg);
+ this.unhandledRejections.splice(i);
+ i--;
+ }
+ }
+ if (this.unhandledRejections.length) {
+ this.scheduleRejectionCheck();
+ }
+ }.bind(this), REJECTION_TIMEOUT);
+ }
+ };
+
+ function Promise(resolver) {
+ this._status = STATUS_PENDING;
+ this._handlers = [];
+ try {
+ resolver.call(this, this._resolve.bind(this), this._reject.bind(this));
+ } catch (e) {
+ this._reject(e);
+ }
+ }
+ /**
+ * Builds a promise that is resolved when all the passed in promises are
+ * resolved.
+ * @param {array} array of data and/or promises to wait for.
+ * @return {Promise} New dependant promise.
+ */
+ Promise.all = function Promise_all(promises) {
+ var resolveAll, rejectAll;
+ var deferred = new Promise(function (resolve, reject) {
+ resolveAll = resolve;
+ rejectAll = reject;
+ });
+ var unresolved = promises.length;
+ var results = [];
+ if (unresolved === 0) {
+ resolveAll(results);
+ return deferred;
+ }
+ function reject(reason) {
+ if (deferred._status === STATUS_REJECTED) {
+ return;
+ }
+ results = [];
+ rejectAll(reason);
+ }
+ for (var i = 0, ii = promises.length; i < ii; ++i) {
+ var promise = promises[i];
+ var resolve = (function(i) {
+ return function(value) {
+ if (deferred._status === STATUS_REJECTED) {
+ return;
+ }
+ results[i] = value;
+ unresolved--;
+ if (unresolved === 0) {
+ resolveAll(results);
+ }
+ };
+ })(i);
+ if (Promise.isPromise(promise)) {
+ promise.then(resolve, reject);
+ } else {
+ resolve(promise);
+ }
+ }
+ return deferred;
+ };
+
+ /**
+ * Checks if the value is likely a promise (has a 'then' function).
+ * @return {boolean} true if value is thenable
+ */
+ Promise.isPromise = function Promise_isPromise(value) {
+ return value && typeof value.then === 'function';
+ };
+
+ /**
+ * Creates resolved promise
+ * @param value resolve value
+ * @returns {Promise}
+ */
+ Promise.resolve = function Promise_resolve(value) {
+ return new Promise(function (resolve) { resolve(value); });
+ };
+
+ /**
+ * Creates rejected promise
+ * @param reason rejection value
+ * @returns {Promise}
+ */
+ Promise.reject = function Promise_reject(reason) {
+ return new Promise(function (resolve, reject) { reject(reason); });
+ };
+
+ Promise.prototype = {
+ _status: null,
+ _value: null,
+ _handlers: null,
+ _unhandledRejection: null,
+
+ _updateStatus: function Promise__updateStatus(status, value) {
+ if (this._status === STATUS_RESOLVED ||
+ this._status === STATUS_REJECTED) {
+ return;
+ }
+
+ if (status === STATUS_RESOLVED &&
+ Promise.isPromise(value)) {
+ value.then(this._updateStatus.bind(this, STATUS_RESOLVED),
+ this._updateStatus.bind(this, STATUS_REJECTED));
+ return;
+ }
+
+ this._status = status;
+ this._value = value;
+
+ if (status === STATUS_REJECTED && this._handlers.length === 0) {
+ this._unhandledRejection = true;
+ HandlerManager.addUnhandledRejection(this);
+ }
+
+ HandlerManager.scheduleHandlers(this);
+ },
+
+ _resolve: function Promise_resolve(value) {
+ this._updateStatus(STATUS_RESOLVED, value);
+ },
+
+ _reject: function Promise_reject(reason) {
+ this._updateStatus(STATUS_REJECTED, reason);
+ },
+
+ then: function Promise_then(onResolve, onReject) {
+ var nextPromise = new Promise(function (resolve, reject) {
+ this.resolve = resolve;
+ this.reject = reject;
+ });
+ this._handlers.push({
+ thisPromise: this,
+ onResolve: onResolve,
+ onReject: onReject,
+ nextPromise: nextPromise
+ });
+ HandlerManager.scheduleHandlers(this);
+ return nextPromise;
+ },
+
+ catch: function Promise_catch(onReject) {
+ return this.then(undefined, onReject);
+ }
+ };
+
+ globalScope.Promise = Promise;
+})();
+
+var StatTimer = (function StatTimerClosure() {
+ function rpad(str, pad, length) {
+ while (str.length < length) {
+ str += pad;
+ }
+ return str;
+ }
+ function StatTimer() {
+ this.started = {};
+ this.times = [];
+ this.enabled = true;
+ }
+ StatTimer.prototype = {
+ time: function StatTimer_time(name) {
+ if (!this.enabled) {
+ return;
+ }
+ if (name in this.started) {
+ warn('Timer is already running for ' + name);
+ }
+ this.started[name] = Date.now();
+ },
+ timeEnd: function StatTimer_timeEnd(name) {
+ if (!this.enabled) {
+ return;
+ }
+ if (!(name in this.started)) {
+ warn('Timer has not been started for ' + name);
+ }
+ this.times.push({
+ 'name': name,
+ 'start': this.started[name],
+ 'end': Date.now()
+ });
+ // Remove timer from started so it can be called again.
+ delete this.started[name];
+ },
+ toString: function StatTimer_toString() {
+ var i, ii;
+ var times = this.times;
+ var out = '';
+ // Find the longest name for padding purposes.
+ var longest = 0;
+ for (i = 0, ii = times.length; i < ii; ++i) {
+ var name = times[i]['name'];
+ if (name.length > longest) {
+ longest = name.length;
+ }
+ }
+ for (i = 0, ii = times.length; i < ii; ++i) {
+ var span = times[i];
+ var duration = span.end - span.start;
+ out += rpad(span['name'], ' ', longest) + ' ' + duration + 'ms\n';
+ }
+ return out;
+ }
+ };
+ return StatTimer;
+})();
+
+PDFJS.createBlob = function createBlob(data, contentType) {
+ if (typeof Blob !== 'undefined') {
+ return new Blob([data], { type: contentType });
+ }
+ // Blob builder is deprecated in FF14 and removed in FF18.
+ var bb = new MozBlobBuilder();
+ bb.append(data);
+ return bb.getBlob(contentType);
+};
+
+PDFJS.createObjectURL = (function createObjectURLClosure() {
+ // Blob/createObjectURL is not available, falling back to data schema.
+ var digits =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+
+ return function createObjectURL(data, contentType) {
+ if (!PDFJS.disableCreateObjectURL &&
+ typeof URL !== 'undefined' && URL.createObjectURL) {
+ var blob = PDFJS.createBlob(data, contentType);
+ return URL.createObjectURL(blob);
+ }
+
+ var buffer = 'data:' + contentType + ';base64,';
+ for (var i = 0, ii = data.length; i < ii; i += 3) {
+ var b1 = data[i] & 0xFF;
+ var b2 = data[i + 1] & 0xFF;
+ var b3 = data[i + 2] & 0xFF;
+ var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4);
+ var d3 = i + 1 < ii ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64;
+ var d4 = i + 2 < ii ? (b3 & 0x3F) : 64;
+ buffer += digits[d1] + digits[d2] + digits[d3] + digits[d4];
+ }
+ return buffer;
+ };
+})();
+
+function MessageHandler(sourceName, targetName, comObj) {
+ this.sourceName = sourceName;
+ this.targetName = targetName;
+ this.comObj = comObj;
+ this.callbackIndex = 1;
+ this.postMessageTransfers = true;
+ var callbacksCapabilities = this.callbacksCapabilities = {};
+ var ah = this.actionHandler = {};
+
+ this._onComObjOnMessage = function messageHandlerComObjOnMessage(event) {
+ var data = event.data;
+ if (data.targetName !== this.sourceName) {
+ return;
+ }
+ if (data.isReply) {
+ var callbackId = data.callbackId;
+ if (data.callbackId in callbacksCapabilities) {
+ var callback = callbacksCapabilities[callbackId];
+ delete callbacksCapabilities[callbackId];
+ if ('error' in data) {
+ callback.reject(data.error);
+ } else {
+ callback.resolve(data.data);
+ }
+ } else {
+ error('Cannot resolve callback ' + callbackId);
+ }
+ } else if (data.action in ah) {
+ var action = ah[data.action];
+ if (data.callbackId) {
+ var sourceName = this.sourceName;
+ var targetName = data.sourceName;
+ Promise.resolve().then(function () {
+ return action[0].call(action[1], data.data);
+ }).then(function (result) {
+ comObj.postMessage({
+ sourceName: sourceName,
+ targetName: targetName,
+ isReply: true,
+ callbackId: data.callbackId,
+ data: result
+ });
+ }, function (reason) {
+ if (reason instanceof Error) {
+ // Serialize error to avoid "DataCloneError"
+ reason = reason + '';
+ }
+ comObj.postMessage({
+ sourceName: sourceName,
+ targetName: targetName,
+ isReply: true,
+ callbackId: data.callbackId,
+ error: reason
+ });
+ });
+ } else {
+ action[0].call(action[1], data.data);
+ }
+ } else {
+ error('Unknown action from worker: ' + data.action);
+ }
+ }.bind(this);
+ comObj.addEventListener('message', this._onComObjOnMessage);
+}
+
+MessageHandler.prototype = {
+ on: function messageHandlerOn(actionName, handler, scope) {
+ var ah = this.actionHandler;
+ if (ah[actionName]) {
+ error('There is already an actionName called "' + actionName + '"');
+ }
+ ah[actionName] = [handler, scope];
+ },
+ /**
+ * Sends a message to the comObj to invoke the action with the supplied data.
+ * @param {String} actionName Action to call.
+ * @param {JSON} data JSON data to send.
+ * @param {Array} [transfers] Optional list of transfers/ArrayBuffers
+ */
+ send: function messageHandlerSend(actionName, data, transfers) {
+ var message = {
+ sourceName: this.sourceName,
+ targetName: this.targetName,
+ action: actionName,
+ data: data
+ };
+ this.postMessage(message, transfers);
+ },
+ /**
+ * Sends a message to the comObj to invoke the action with the supplied data.
+ * Expects that other side will callback with the response.
+ * @param {String} actionName Action to call.
+ * @param {JSON} data JSON data to send.
+ * @param {Array} [transfers] Optional list of transfers/ArrayBuffers.
+ * @returns {Promise} Promise to be resolved with response data.
+ */
+ sendWithPromise:
+ function messageHandlerSendWithPromise(actionName, data, transfers) {
+ var callbackId = this.callbackIndex++;
+ var message = {
+ sourceName: this.sourceName,
+ targetName: this.targetName,
+ action: actionName,
+ data: data,
+ callbackId: callbackId
+ };
+ var capability = createPromiseCapability();
+ this.callbacksCapabilities[callbackId] = capability;
+ try {
+ this.postMessage(message, transfers);
+ } catch (e) {
+ capability.reject(e);
+ }
+ return capability.promise;
+ },
+ /**
+ * Sends raw message to the comObj.
+ * @private
+ * @param message {Object} Raw message.
+ * @param transfers List of transfers/ArrayBuffers, or undefined.
+ */
+ postMessage: function (message, transfers) {
+ if (transfers && this.postMessageTransfers) {
+ this.comObj.postMessage(message, transfers);
+ } else {
+ this.comObj.postMessage(message);
+ }
+ },
+
+ destroy: function () {
+ this.comObj.removeEventListener('message', this._onComObjOnMessage);
+ }
+};
+
+function loadJpegStream(id, imageUrl, objs) {
+ var img = new Image();
+ img.onload = (function loadJpegStream_onloadClosure() {
+ objs.resolve(id, img);
+ });
+ img.onerror = (function loadJpegStream_onerrorClosure() {
+ objs.resolve(id, null);
+ warn('Error during JPEG image loading');
+ });
+ img.src = imageUrl;
+}
+
+ // Polyfill from https://github.com/Polymer/URL
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+(function checkURLConstructor(scope) {
+ /* jshint ignore:start */
+
+ // feature detect for URL constructor
+ var hasWorkingUrl = false;
+ if (typeof URL === 'function' && ('origin' in URL.prototype)) {
+ try {
+ var u = new URL('b', 'http://a');
+ u.pathname = 'c%20d';
+ hasWorkingUrl = u.href === 'http://a/c%20d';
+ } catch(e) {}
+ }
+
+ if (hasWorkingUrl)
+ return;
+
+ var relative = Object.create(null);
+ relative['ftp'] = 21;
+ relative['file'] = 0;
+ relative['gopher'] = 70;
+ relative['http'] = 80;
+ relative['https'] = 443;
+ relative['ws'] = 80;
+ relative['wss'] = 443;
+
+ var relativePathDotMapping = Object.create(null);
+ relativePathDotMapping['%2e'] = '.';
+ relativePathDotMapping['.%2e'] = '..';
+ relativePathDotMapping['%2e.'] = '..';
+ relativePathDotMapping['%2e%2e'] = '..';
+
+ function isRelativeScheme(scheme) {
+ return relative[scheme] !== undefined;
+ }
+
+ function invalid() {
+ clear.call(this);
+ this._isInvalid = true;
+ }
+
+ function IDNAToASCII(h) {
+ if ('' == h) {
+ invalid.call(this)
+ }
+ // XXX
+ return h.toLowerCase()
+ }
+
+ function percentEscape(c) {
+ var unicode = c.charCodeAt(0);
+ if (unicode > 0x20 &&
+ unicode < 0x7F &&
+ // " # < > ? `
+ [0x22, 0x23, 0x3C, 0x3E, 0x3F, 0x60].indexOf(unicode) == -1
+ ) {
+ return c;
+ }
+ return encodeURIComponent(c);
+ }
+
+ function percentEscapeQuery(c) {
+ // XXX This actually needs to encode c using encoding and then
+ // convert the bytes one-by-one.
+
+ var unicode = c.charCodeAt(0);
+ if (unicode > 0x20 &&
+ unicode < 0x7F &&
+ // " # < > ` (do not escape '?')
+ [0x22, 0x23, 0x3C, 0x3E, 0x60].indexOf(unicode) == -1
+ ) {
+ return c;
+ }
+ return encodeURIComponent(c);
+ }
+
+ var EOF = undefined,
+ ALPHA = /[a-zA-Z]/,
+ ALPHANUMERIC = /[a-zA-Z0-9\+\-\.]/;
+
+ function parse(input, stateOverride, base) {
+ function err(message) {
+ errors.push(message)
+ }
+
+ var state = stateOverride || 'scheme start',
+ cursor = 0,
+ buffer = '',
+ seenAt = false,
+ seenBracket = false,
+ errors = [];
+
+ loop: while ((input[cursor - 1] != EOF || cursor == 0) && !this._isInvalid) {
+ var c = input[cursor];
+ switch (state) {
+ case 'scheme start':
+ if (c && ALPHA.test(c)) {
+ buffer += c.toLowerCase(); // ASCII-safe
+ state = 'scheme';
+ } else if (!stateOverride) {
+ buffer = '';
+ state = 'no scheme';
+ continue;
+ } else {
+ err('Invalid scheme.');
+ break loop;
+ }
+ break;
+
+ case 'scheme':
+ if (c && ALPHANUMERIC.test(c)) {
+ buffer += c.toLowerCase(); // ASCII-safe
+ } else if (':' == c) {
+ this._scheme = buffer;
+ buffer = '';
+ if (stateOverride) {
+ break loop;
+ }
+ if (isRelativeScheme(this._scheme)) {
+ this._isRelative = true;
+ }
+ if ('file' == this._scheme) {
+ state = 'relative';
+ } else if (this._isRelative && base && base._scheme == this._scheme) {
+ state = 'relative or authority';
+ } else if (this._isRelative) {
+ state = 'authority first slash';
+ } else {
+ state = 'scheme data';
+ }
+ } else if (!stateOverride) {
+ buffer = '';
+ cursor = 0;
+ state = 'no scheme';
+ continue;
+ } else if (EOF == c) {
+ break loop;
+ } else {
+ err('Code point not allowed in scheme: ' + c)
+ break loop;
+ }
+ break;
+
+ case 'scheme data':
+ if ('?' == c) {
+ this._query = '?';
+ state = 'query';
+ } else if ('#' == c) {
+ this._fragment = '#';
+ state = 'fragment';
+ } else {
+ // XXX error handling
+ if (EOF != c && '\t' != c && '\n' != c && '\r' != c) {
+ this._schemeData += percentEscape(c);
+ }
+ }
+ break;
+
+ case 'no scheme':
+ if (!base || !(isRelativeScheme(base._scheme))) {
+ err('Missing scheme.');
+ invalid.call(this);
+ } else {
+ state = 'relative';
+ continue;
+ }
+ break;
+
+ case 'relative or authority':
+ if ('/' == c && '/' == input[cursor+1]) {
+ state = 'authority ignore slashes';
+ } else {
+ err('Expected /, got: ' + c);
+ state = 'relative';
+ continue
+ }
+ break;
+
+ case 'relative':
+ this._isRelative = true;
+ if ('file' != this._scheme)
+ this._scheme = base._scheme;
+ if (EOF == c) {
+ this._host = base._host;
+ this._port = base._port;
+ this._path = base._path.slice();
+ this._query = base._query;
+ this._username = base._username;
+ this._password = base._password;
+ break loop;
+ } else if ('/' == c || '\\' == c) {
+ if ('\\' == c)
+ err('\\ is an invalid code point.');
+ state = 'relative slash';
+ } else if ('?' == c) {
+ this._host = base._host;
+ this._port = base._port;
+ this._path = base._path.slice();
+ this._query = '?';
+ this._username = base._username;
+ this._password = base._password;
+ state = 'query';
+ } else if ('#' == c) {
+ this._host = base._host;
+ this._port = base._port;
+ this._path = base._path.slice();
+ this._query = base._query;
+ this._fragment = '#';
+ this._username = base._username;
+ this._password = base._password;
+ state = 'fragment';
+ } else {
+ var nextC = input[cursor+1]
+ var nextNextC = input[cursor+2]
+ if (
+ 'file' != this._scheme || !ALPHA.test(c) ||
+ (nextC != ':' && nextC != '|') ||
+ (EOF != nextNextC && '/' != nextNextC && '\\' != nextNextC && '?' != nextNextC && '#' != nextNextC)) {
+ this._host = base._host;
+ this._port = base._port;
+ this._username = base._username;
+ this._password = base._password;
+ this._path = base._path.slice();
+ this._path.pop();
+ }
+ state = 'relative path';
+ continue;
+ }
+ break;
+
+ case 'relative slash':
+ if ('/' == c || '\\' == c) {
+ if ('\\' == c) {
+ err('\\ is an invalid code point.');
+ }
+ if ('file' == this._scheme) {
+ state = 'file host';
+ } else {
+ state = 'authority ignore slashes';
+ }
+ } else {
+ if ('file' != this._scheme) {
+ this._host = base._host;
+ this._port = base._port;
+ this._username = base._username;
+ this._password = base._password;
+ }
+ state = 'relative path';
+ continue;
+ }
+ break;
+
+ case 'authority first slash':
+ if ('/' == c) {
+ state = 'authority second slash';
+ } else {
+ err("Expected '/', got: " + c);
+ state = 'authority ignore slashes';
+ continue;
+ }
+ break;
+
+ case 'authority second slash':
+ state = 'authority ignore slashes';
+ if ('/' != c) {
+ err("Expected '/', got: " + c);
+ continue;
+ }
+ break;
+
+ case 'authority ignore slashes':
+ if ('/' != c && '\\' != c) {
+ state = 'authority';
+ continue;
+ } else {
+ err('Expected authority, got: ' + c);
+ }
+ break;
+
+ case 'authority':
+ if ('@' == c) {
+ if (seenAt) {
+ err('@ already seen.');
+ buffer += '%40';
+ }
+ seenAt = true;
+ for (var i = 0; i < buffer.length; i++) {
+ var cp = buffer[i];
+ if ('\t' == cp || '\n' == cp || '\r' == cp) {
+ err('Invalid whitespace in authority.');
+ continue;
+ }
+ // XXX check URL code points
+ if (':' == cp && null === this._password) {
+ this._password = '';
+ continue;
+ }
+ var tempC = percentEscape(cp);
+ (null !== this._password) ? this._password += tempC : this._username += tempC;
+ }
+ buffer = '';
+ } else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) {
+ cursor -= buffer.length;
+ buffer = '';
+ state = 'host';
+ continue;
+ } else {
+ buffer += c;
+ }
+ break;
+
+ case 'file host':
+ if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) {
+ if (buffer.length == 2 && ALPHA.test(buffer[0]) && (buffer[1] == ':' || buffer[1] == '|')) {
+ state = 'relative path';
+ } else if (buffer.length == 0) {
+ state = 'relative path start';
+ } else {
+ this._host = IDNAToASCII.call(this, buffer);
+ buffer = '';
+ state = 'relative path start';
+ }
+ continue;
+ } else if ('\t' == c || '\n' == c || '\r' == c) {
+ err('Invalid whitespace in file host.');
+ } else {
+ buffer += c;
+ }
+ break;
+
+ case 'host':
+ case 'hostname':
+ if (':' == c && !seenBracket) {
+ // XXX host parsing
+ this._host = IDNAToASCII.call(this, buffer);
+ buffer = '';
+ state = 'port';
+ if ('hostname' == stateOverride) {
+ break loop;
+ }
+ } else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) {
+ this._host = IDNAToASCII.call(this, buffer);
+ buffer = '';
+ state = 'relative path start';
+ if (stateOverride) {
+ break loop;
+ }
+ continue;
+ } else if ('\t' != c && '\n' != c && '\r' != c) {
+ if ('[' == c) {
+ seenBracket = true;
+ } else if (']' == c) {
+ seenBracket = false;
+ }
+ buffer += c;
+ } else {
+ err('Invalid code point in host/hostname: ' + c);
+ }
+ break;
+
+ case 'port':
+ if (/[0-9]/.test(c)) {
+ buffer += c;
+ } else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c || stateOverride) {
+ if ('' != buffer) {
+ var temp = parseInt(buffer, 10);
+ if (temp != relative[this._scheme]) {
+ this._port = temp + '';
+ }
+ buffer = '';
+ }
+ if (stateOverride) {
+ break loop;
+ }
+ state = 'relative path start';
+ continue;
+ } else if ('\t' == c || '\n' == c || '\r' == c) {
+ err('Invalid code point in port: ' + c);
+ } else {
+ invalid.call(this);
+ }
+ break;
+
+ case 'relative path start':
+ if ('\\' == c)
+ err("'\\' not allowed in path.");
+ state = 'relative path';
+ if ('/' != c && '\\' != c) {
+ continue;
+ }
+ break;
+
+ case 'relative path':
+ if (EOF == c || '/' == c || '\\' == c || (!stateOverride && ('?' == c || '#' == c))) {
+ if ('\\' == c) {
+ err('\\ not allowed in relative path.');
+ }
+ var tmp;
+ if (tmp = relativePathDotMapping[buffer.toLowerCase()]) {
+ buffer = tmp;
+ }
+ if ('..' == buffer) {
+ this._path.pop();
+ if ('/' != c && '\\' != c) {
+ this._path.push('');
+ }
+ } else if ('.' == buffer && '/' != c && '\\' != c) {
+ this._path.push('');
+ } else if ('.' != buffer) {
+ if ('file' == this._scheme && this._path.length == 0 && buffer.length == 2 && ALPHA.test(buffer[0]) && buffer[1] == '|') {
+ buffer = buffer[0] + ':';
+ }
+ this._path.push(buffer);
+ }
+ buffer = '';
+ if ('?' == c) {
+ this._query = '?';
+ state = 'query';
+ } else if ('#' == c) {
+ this._fragment = '#';
+ state = 'fragment';
+ }
+ } else if ('\t' != c && '\n' != c && '\r' != c) {
+ buffer += percentEscape(c);
+ }
+ break;
+
+ case 'query':
+ if (!stateOverride && '#' == c) {
+ this._fragment = '#';
+ state = 'fragment';
+ } else if (EOF != c && '\t' != c && '\n' != c && '\r' != c) {
+ this._query += percentEscapeQuery(c);
+ }
+ break;
+
+ case 'fragment':
+ if (EOF != c && '\t' != c && '\n' != c && '\r' != c) {
+ this._fragment += c;
+ }
+ break;
+ }
+
+ cursor++;
+ }
+ }
+
+ function clear() {
+ this._scheme = '';
+ this._schemeData = '';
+ this._username = '';
+ this._password = null;
+ this._host = '';
+ this._port = '';
+ this._path = [];
+ this._query = '';
+ this._fragment = '';
+ this._isInvalid = false;
+ this._isRelative = false;
+ }
+
+ // Does not process domain names or IP addresses.
+ // Does not handle encoding for the query parameter.
+ function jURL(url, base /* , encoding */) {
+ if (base !== undefined && !(base instanceof jURL))
+ base = new jURL(String(base));
+
+ this._url = url;
+ clear.call(this);
+
+ var input = url.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g, '');
+ // encoding = encoding || 'utf-8'
+
+ parse.call(this, input, null, base);
+ }
+
+ jURL.prototype = {
+ toString: function() {
+ return this.href;
+ },
+ get href() {
+ if (this._isInvalid)
+ return this._url;
+
+ var authority = '';
+ if ('' != this._username || null != this._password) {
+ authority = this._username +
+ (null != this._password ? ':' + this._password : '') + '@';
+ }
+
+ return this.protocol +
+ (this._isRelative ? '//' + authority + this.host : '') +
+ this.pathname + this._query + this._fragment;
+ },
+ set href(href) {
+ clear.call(this);
+ parse.call(this, href);
+ },
+
+ get protocol() {
+ return this._scheme + ':';
+ },
+ set protocol(protocol) {
+ if (this._isInvalid)
+ return;
+ parse.call(this, protocol + ':', 'scheme start');
+ },
+
+ get host() {
+ return this._isInvalid ? '' : this._port ?
+ this._host + ':' + this._port : this._host;
+ },
+ set host(host) {
+ if (this._isInvalid || !this._isRelative)
+ return;
+ parse.call(this, host, 'host');
+ },
+
+ get hostname() {
+ return this._host;
+ },
+ set hostname(hostname) {
+ if (this._isInvalid || !this._isRelative)
+ return;
+ parse.call(this, hostname, 'hostname');
+ },
+
+ get port() {
+ return this._port;
+ },
+ set port(port) {
+ if (this._isInvalid || !this._isRelative)
+ return;
+ parse.call(this, port, 'port');
+ },
+
+ get pathname() {
+ return this._isInvalid ? '' : this._isRelative ?
+ '/' + this._path.join('/') : this._schemeData;
+ },
+ set pathname(pathname) {
+ if (this._isInvalid || !this._isRelative)
+ return;
+ this._path = [];
+ parse.call(this, pathname, 'relative path start');
+ },
+
+ get search() {
+ return this._isInvalid || !this._query || '?' == this._query ?
+ '' : this._query;
+ },
+ set search(search) {
+ if (this._isInvalid || !this._isRelative)
+ return;
+ this._query = '?';
+ if ('?' == search[0])
+ search = search.slice(1);
+ parse.call(this, search, 'query');
+ },
+
+ get hash() {
+ return this._isInvalid || !this._fragment || '#' == this._fragment ?
+ '' : this._fragment;
+ },
+ set hash(hash) {
+ if (this._isInvalid)
+ return;
+ this._fragment = '#';
+ if ('#' == hash[0])
+ hash = hash.slice(1);
+ parse.call(this, hash, 'fragment');
+ },
+
+ get origin() {
+ var host;
+ if (this._isInvalid || !this._scheme) {
+ return '';
+ }
+ // javascript: Gecko returns String(""), WebKit/Blink String("null")
+ // Gecko throws error for "data://"
+ // data: Gecko returns "", Blink returns "data://", WebKit returns "null"
+ // Gecko returns String("") for file: mailto:
+ // WebKit/Blink returns String("SCHEME://") for file: mailto:
+ switch (this._scheme) {
+ case 'data':
+ case 'file':
+ case 'javascript':
+ case 'mailto':
+ return 'null';
+ }
+ host = this.host;
+ if (!host) {
+ return '';
+ }
+ return this._scheme + '://' + host;
+ }
+ };
+
+ // Copy over the static methods
+ var OriginalURL = scope.URL;
+ if (OriginalURL) {
+ jURL.createObjectURL = function(blob) {
+ // IE extension allows a second optional options argument.
+ // http://msdn.microsoft.com/en-us/library/ie/hh772302(v=vs.85).aspx
+ return OriginalURL.createObjectURL.apply(OriginalURL, arguments);
+ };
+ jURL.revokeObjectURL = function(url) {
+ OriginalURL.revokeObjectURL(url);
+ };
+ }
+
+ scope.URL = jURL;
+ /* jshint ignore:end */
+})(globalScope);
+
+
+var DEFAULT_RANGE_CHUNK_SIZE = 65536; // 2^16 = 65536
+
+/**
+ * The maximum allowed image size in total pixels e.g. width * height. Images
+ * above this value will not be drawn. Use -1 for no limit.
+ * @var {number}
+ */
+PDFJS.maxImageSize = (PDFJS.maxImageSize === undefined ?
+ -1 : PDFJS.maxImageSize);
+
+/**
+ * The url of where the predefined Adobe CMaps are located. Include trailing
+ * slash.
+ * @var {string}
+ */
+PDFJS.cMapUrl = (PDFJS.cMapUrl === undefined ? null : PDFJS.cMapUrl);
+
+/**
+ * Specifies if CMaps are binary packed.
+ * @var {boolean}
+ */
+PDFJS.cMapPacked = PDFJS.cMapPacked === undefined ? false : PDFJS.cMapPacked;
+
+/**
+ * By default fonts are converted to OpenType fonts and loaded via font face
+ * rules. If disabled, the font will be rendered using a built in font renderer
+ * that constructs the glyphs with primitive path commands.
+ * @var {boolean}
+ */
+PDFJS.disableFontFace = (PDFJS.disableFontFace === undefined ?
+ false : PDFJS.disableFontFace);
+
+/**
+ * Path for image resources, mainly for annotation icons. Include trailing
+ * slash.
+ * @var {string}
+ */
+PDFJS.imageResourcesPath = (PDFJS.imageResourcesPath === undefined ?
+ '' : PDFJS.imageResourcesPath);
+
+/**
+ * Disable the web worker and run all code on the main thread. This will happen
+ * automatically if the browser doesn't support workers or sending typed arrays
+ * to workers.
+ * @var {boolean}
+ */
+PDFJS.disableWorker = (PDFJS.disableWorker === undefined ?
+ false : PDFJS.disableWorker);
+
+/**
+ * Path and filename of the worker file. Required when the worker is enabled in
+ * development mode. If unspecified in the production build, the worker will be
+ * loaded based on the location of the pdf.js file. It is recommended that
+ * the workerSrc is set in a custom application to prevent issues caused by
+ * third-party frameworks and libraries.
+ * @var {string}
+ */
+PDFJS.workerSrc = (PDFJS.workerSrc === undefined ? null : PDFJS.workerSrc);
+
+/**
+ * Disable range request loading of PDF files. When enabled and if the server
+ * supports partial content requests then the PDF will be fetched in chunks.
+ * Enabled (false) by default.
+ * @var {boolean}
+ */
+PDFJS.disableRange = (PDFJS.disableRange === undefined ?
+ false : PDFJS.disableRange);
+
+/**
+ * Disable streaming of PDF file data. By default PDF.js attempts to load PDF
+ * in chunks. This default behavior can be disabled.
+ * @var {boolean}
+ */
+PDFJS.disableStream = (PDFJS.disableStream === undefined ?
+ false : PDFJS.disableStream);
+
+/**
+ * Disable pre-fetching of PDF file data. When range requests are enabled PDF.js
+ * will automatically keep fetching more data even if it isn't needed to display
+ * the current page. This default behavior can be disabled.
+ *
+ * NOTE: It is also necessary to disable streaming, see above,
+ * in order for disabling of pre-fetching to work correctly.
+ * @var {boolean}
+ */
+PDFJS.disableAutoFetch = (PDFJS.disableAutoFetch === undefined ?
+ false : PDFJS.disableAutoFetch);
+
+/**
+ * Enables special hooks for debugging PDF.js.
+ * @var {boolean}
+ */
+PDFJS.pdfBug = (PDFJS.pdfBug === undefined ? false : PDFJS.pdfBug);
+
+/**
+ * Enables transfer usage in postMessage for ArrayBuffers.
+ * @var {boolean}
+ */
+PDFJS.postMessageTransfers = (PDFJS.postMessageTransfers === undefined ?
+ true : PDFJS.postMessageTransfers);
+
+/**
+ * Disables URL.createObjectURL usage.
+ * @var {boolean}
+ */
+PDFJS.disableCreateObjectURL = (PDFJS.disableCreateObjectURL === undefined ?
+ false : PDFJS.disableCreateObjectURL);
+
+/**
+ * Disables WebGL usage.
+ * @var {boolean}
+ */
+PDFJS.disableWebGL = (PDFJS.disableWebGL === undefined ?
+ true : PDFJS.disableWebGL);
+
+/**
+ * Disables fullscreen support, and by extension Presentation Mode,
+ * in browsers which support the fullscreen API.
+ * @var {boolean}
+ */
+PDFJS.disableFullscreen = (PDFJS.disableFullscreen === undefined ?
+ false : PDFJS.disableFullscreen);
+
+/**
+ * Enables CSS only zooming.
+ * @var {boolean}
+ */
+PDFJS.useOnlyCssZoom = (PDFJS.useOnlyCssZoom === undefined ?
+ false : PDFJS.useOnlyCssZoom);
+
+/**
+ * Controls the logging level.
+ * The constants from PDFJS.VERBOSITY_LEVELS should be used:
+ * - errors
+ * - warnings [default]
+ * - infos
+ * @var {number}
+ */
+PDFJS.verbosity = (PDFJS.verbosity === undefined ?
+ PDFJS.VERBOSITY_LEVELS.warnings : PDFJS.verbosity);
+
+/**
+ * The maximum supported canvas size in total pixels e.g. width * height.
+ * The default value is 4096 * 4096. Use -1 for no limit.
+ * @var {number}
+ */
+PDFJS.maxCanvasPixels = (PDFJS.maxCanvasPixels === undefined ?
+ 16777216 : PDFJS.maxCanvasPixels);
+
+/**
+ * (Deprecated) Opens external links in a new window if enabled.
+ * The default behavior opens external links in the PDF.js window.
+ *
+ * NOTE: This property has been deprecated, please use
+ * `PDFJS.externalLinkTarget = PDFJS.LinkTarget.BLANK` instead.
+ * @var {boolean}
+ */
+PDFJS.openExternalLinksInNewWindow = (
+ PDFJS.openExternalLinksInNewWindow === undefined ?
+ false : PDFJS.openExternalLinksInNewWindow);
+
+/**
+ * Specifies the |target| attribute for external links.
+ * The constants from PDFJS.LinkTarget should be used:
+ * - NONE [default]
+ * - SELF
+ * - BLANK
+ * - PARENT
+ * - TOP
+ * @var {number}
+ */
+PDFJS.externalLinkTarget = (PDFJS.externalLinkTarget === undefined ?
+ PDFJS.LinkTarget.NONE : PDFJS.externalLinkTarget);
+
+/**
+ * Determines if we can eval strings as JS. Primarily used to improve
+ * performance for font rendering.
+ * @var {boolean}
+ */
+PDFJS.isEvalSupported = (PDFJS.isEvalSupported === undefined ?
+ true : PDFJS.isEvalSupported);
+
+/**
+ * Document initialization / loading parameters object.
+ *
+ * @typedef {Object} DocumentInitParameters
+ * @property {string} url - The URL of the PDF.
+ * @property {TypedArray|Array|string} data - Binary PDF data. Use typed arrays
+ * (Uint8Array) to improve the memory usage. If PDF data is BASE64-encoded,
+ * use atob() to convert it to a binary string first.
+ * @property {Object} httpHeaders - Basic authentication headers.
+ * @property {boolean} withCredentials - Indicates whether or not cross-site
+ * Access-Control requests should be made using credentials such as cookies
+ * or authorization headers. The default is false.
+ * @property {string} password - For decrypting password-protected PDFs.
+ * @property {TypedArray} initialData - A typed array with the first portion or
+ * all of the pdf data. Used by the extension since some data is already
+ * loaded before the switch to range requests.
+ * @property {number} length - The PDF file length. It's used for progress
+ * reports and range requests operations.
+ * @property {PDFDataRangeTransport} range
+ * @property {number} rangeChunkSize - Optional parameter to specify
+ * maximum number of bytes fetched per range request. The default value is
+ * 2^16 = 65536.
+ * @property {PDFWorker} worker - The worker that will be used for the loading
+ * and parsing of the PDF data.
+ */
+
+/**
+ * @typedef {Object} PDFDocumentStats
+ * @property {Array} streamTypes - Used stream types in the document (an item
+ * is set to true if specific stream ID was used in the document).
+ * @property {Array} fontTypes - Used font type in the document (an item is set
+ * to true if specific font ID was used in the document).
+ */
+
+/**
+ * This is the main entry point for loading a PDF and interacting with it.
+ * NOTE: If a URL is used to fetch the PDF data a standard XMLHttpRequest(XHR)
+ * is used, which means it must follow the same origin rules that any XHR does
+ * e.g. No cross domain requests without CORS.
+ *
+ * @param {string|TypedArray|DocumentInitParameters|PDFDataRangeTransport} src
+ * Can be a url to where a PDF is located, a typed array (Uint8Array)
+ * already populated with data or parameter object.
+ *
+ * @param {PDFDataRangeTransport} pdfDataRangeTransport (deprecated) It is used
+ * if you want to manually serve range requests for data in the PDF.
+ *
+ * @param {function} passwordCallback (deprecated) It is used to request a
+ * password if wrong or no password was provided. The callback receives two
+ * parameters: function that needs to be called with new password and reason
+ * (see {PasswordResponses}).
+ *
+ * @param {function} progressCallback (deprecated) It is used to be able to
+ * monitor the loading progress of the PDF file (necessary to implement e.g.
+ * a loading bar). The callback receives an {Object} with the properties:
+ * {number} loaded and {number} total.
+ *
+ * @return {PDFDocumentLoadingTask}
+ */
+PDFJS.getDocument = function getDocument(src,
+ pdfDataRangeTransport,
+ passwordCallback,
+ progressCallback) {
+ var task = new PDFDocumentLoadingTask();
+
+ // Support of the obsolete arguments (for compatibility with API v1.0)
+ if (arguments.length > 1) {
+ deprecated('getDocument is called with pdfDataRangeTransport, ' +
+ 'passwordCallback or progressCallback argument');
+ }
+ if (pdfDataRangeTransport) {
+ if (!(pdfDataRangeTransport instanceof PDFDataRangeTransport)) {
+ // Not a PDFDataRangeTransport instance, trying to add missing properties.
+ pdfDataRangeTransport = Object.create(pdfDataRangeTransport);
+ pdfDataRangeTransport.length = src.length;
+ pdfDataRangeTransport.initialData = src.initialData;
+ if (!pdfDataRangeTransport.abort) {
+ pdfDataRangeTransport.abort = function () {};
+ }
+ }
+ src = Object.create(src);
+ src.range = pdfDataRangeTransport;
+ }
+ task.onPassword = passwordCallback || null;
+ task.onProgress = progressCallback || null;
+
+ var source;
+ if (typeof src === 'string') {
+ source = { url: src };
+ } else if (isArrayBuffer(src)) {
+ source = { data: src };
+ } else if (src instanceof PDFDataRangeTransport) {
+ source = { range: src };
+ } else {
+ if (typeof src !== 'object') {
+ error('Invalid parameter in getDocument, need either Uint8Array, ' +
+ 'string or a parameter object');
+ }
+ if (!src.url && !src.data && !src.range) {
+ error('Invalid parameter object: need either .data, .range or .url');
+ }
+
+ source = src;
+ }
+
+ var params = {};
+ var rangeTransport = null;
+ var worker = null;
+ for (var key in source) {
+ if (key === 'url' && typeof window !== 'undefined') {
+ // The full path is required in the 'url' field.
+ params[key] = combineUrl(window.location.href, source[key]);
+ continue;
+ } else if (key === 'range') {
+ rangeTransport = source[key];
+ continue;
+ } else if (key === 'worker') {
+ worker = source[key];
+ continue;
+ } else if (key === 'data' && !(source[key] instanceof Uint8Array)) {
+ // Converting string or array-like data to Uint8Array.
+ var pdfBytes = source[key];
+ if (typeof pdfBytes === 'string') {
+ params[key] = stringToBytes(pdfBytes);
+ } else if (typeof pdfBytes === 'object' && pdfBytes !== null &&
+ !isNaN(pdfBytes.length)) {
+ params[key] = new Uint8Array(pdfBytes);
+ } else if (isArrayBuffer(pdfBytes)) {
+ params[key] = new Uint8Array(pdfBytes);
+ } else {
+ error('Invalid PDF binary data: either typed array, string or ' +
+ 'array-like object is expected in the data property.');
+ }
+ continue;
+ }
+ params[key] = source[key];
+ }
+
+ params.rangeChunkSize = params.rangeChunkSize || DEFAULT_RANGE_CHUNK_SIZE;
+
+ if (!worker) {
+ // Worker was not provided -- creating and owning our own.
+ worker = new PDFWorker();
+ task._worker = worker;
+ }
+ var docId = task.docId;
+ worker.promise.then(function () {
+ if (task.destroyed) {
+ throw new Error('Loading aborted');
+ }
+ return _fetchDocument(worker, params, rangeTransport, docId).then(
+ function (workerId) {
+ if (task.destroyed) {
+ throw new Error('Loading aborted');
+ }
+ var messageHandler = new MessageHandler(docId, workerId, worker.port);
+ messageHandler.send('Ready', null);
+ var transport = new WorkerTransport(messageHandler, task, rangeTransport);
+ task._transport = transport;
+ });
+ }, task._capability.reject);
+
+ return task;
+};
+
+/**
+ * Starts fetching of specified PDF document/data.
+ * @param {PDFWorker} worker
+ * @param {Object} source
+ * @param {PDFDataRangeTransport} pdfDataRangeTransport
+ * @param {string} docId Unique document id, used as MessageHandler id.
+ * @returns {Promise} The promise, which is resolved when worker id of
+ * MessageHandler is known.
+ * @private
+ */
+function _fetchDocument(worker, source, pdfDataRangeTransport, docId) {
+ if (worker.destroyed) {
+ return Promise.reject(new Error('Worker was destroyed'));
+ }
+
+ source.disableAutoFetch = PDFJS.disableAutoFetch;
+ source.disableStream = PDFJS.disableStream;
+ source.chunkedViewerLoading = !!pdfDataRangeTransport;
+ if (pdfDataRangeTransport) {
+ source.length = pdfDataRangeTransport.length;
+ source.initialData = pdfDataRangeTransport.initialData;
+ }
+ return worker.messageHandler.sendWithPromise('GetDocRequest', {
+ docId: docId,
+ source: source,
+ disableRange: PDFJS.disableRange,
+ maxImageSize: PDFJS.maxImageSize,
+ cMapUrl: PDFJS.cMapUrl,
+ cMapPacked: PDFJS.cMapPacked,
+ disableFontFace: PDFJS.disableFontFace,
+ disableCreateObjectURL: PDFJS.disableCreateObjectURL,
+ verbosity: PDFJS.verbosity
+ }).then(function (workerId) {
+ if (worker.destroyed) {
+ throw new Error('Worker was destroyed');
+ }
+ return workerId;
+ });
+}
+
+/**
+ * PDF document loading operation.
+ * @class
+ * @alias PDFDocumentLoadingTask
+ */
+var PDFDocumentLoadingTask = (function PDFDocumentLoadingTaskClosure() {
+ var nextDocumentId = 0;
+
+ /** @constructs PDFDocumentLoadingTask */
+ function PDFDocumentLoadingTask() {
+ this._capability = createPromiseCapability();
+ this._transport = null;
+ this._worker = null;
+
+ /**
+ * Unique document loading task id -- used in MessageHandlers.
+ * @type {string}
+ */
+ this.docId = 'd' + (nextDocumentId++);
+
+ /**
+ * Shows if loading task is destroyed.
+ * @type {boolean}
+ */
+ this.destroyed = false;
+
+ /**
+ * Callback to request a password if wrong or no password was provided.
+ * The callback receives two parameters: function that needs to be called
+ * with new password and reason (see {PasswordResponses}).
+ */
+ this.onPassword = null;
+
+ /**
+ * Callback to be able to monitor the loading progress of the PDF file
+ * (necessary to implement e.g. a loading bar). The callback receives
+ * an {Object} with the properties: {number} loaded and {number} total.
+ */
+ this.onProgress = null;
+
+ /**
+ * Callback to when unsupported feature is used. The callback receives
+ * an {PDFJS.UNSUPPORTED_FEATURES} argument.
+ */
+ this.onUnsupportedFeature = null;
+ }
+
+ PDFDocumentLoadingTask.prototype =
+ /** @lends PDFDocumentLoadingTask.prototype */ {
+ /**
+ * @return {Promise}
+ */
+ get promise() {
+ return this._capability.promise;
+ },
+
+ /**
+ * Aborts all network requests and destroys worker.
+ * @return {Promise} A promise that is resolved after destruction activity
+ * is completed.
+ */
+ destroy: function () {
+ this.destroyed = true;
+
+ var transportDestroyed = !this._transport ? Promise.resolve() :
+ this._transport.destroy();
+ return transportDestroyed.then(function () {
+ this._transport = null;
+ if (this._worker) {
+ this._worker.destroy();
+ this._worker = null;
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Registers callbacks to indicate the document loading completion.
+ *
+ * @param {function} onFulfilled The callback for the loading completion.
+ * @param {function} onRejected The callback for the loading failure.
+ * @return {Promise} A promise that is resolved after the onFulfilled or
+ * onRejected callback.
+ */
+ then: function PDFDocumentLoadingTask_then(onFulfilled, onRejected) {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+ };
+
+ return PDFDocumentLoadingTask;
+})();
+
+/**
+ * Abstract class to support range requests file loading.
+ * @class
+ * @alias PDFJS.PDFDataRangeTransport
+ * @param {number} length
+ * @param {Uint8Array} initialData
+ */
+var PDFDataRangeTransport = (function pdfDataRangeTransportClosure() {
+ function PDFDataRangeTransport(length, initialData) {
+ this.length = length;
+ this.initialData = initialData;
+
+ this._rangeListeners = [];
+ this._progressListeners = [];
+ this._progressiveReadListeners = [];
+ this._readyCapability = createPromiseCapability();
+ }
+ PDFDataRangeTransport.prototype =
+ /** @lends PDFDataRangeTransport.prototype */ {
+ addRangeListener:
+ function PDFDataRangeTransport_addRangeListener(listener) {
+ this._rangeListeners.push(listener);
+ },
+
+ addProgressListener:
+ function PDFDataRangeTransport_addProgressListener(listener) {
+ this._progressListeners.push(listener);
+ },
+
+ addProgressiveReadListener:
+ function PDFDataRangeTransport_addProgressiveReadListener(listener) {
+ this._progressiveReadListeners.push(listener);
+ },
+
+ onDataRange: function PDFDataRangeTransport_onDataRange(begin, chunk) {
+ var listeners = this._rangeListeners;
+ for (var i = 0, n = listeners.length; i < n; ++i) {
+ listeners[i](begin, chunk);
+ }
+ },
+
+ onDataProgress: function PDFDataRangeTransport_onDataProgress(loaded) {
+ this._readyCapability.promise.then(function () {
+ var listeners = this._progressListeners;
+ for (var i = 0, n = listeners.length; i < n; ++i) {
+ listeners[i](loaded);
+ }
+ }.bind(this));
+ },
+
+ onDataProgressiveRead:
+ function PDFDataRangeTransport_onDataProgress(chunk) {
+ this._readyCapability.promise.then(function () {
+ var listeners = this._progressiveReadListeners;
+ for (var i = 0, n = listeners.length; i < n; ++i) {
+ listeners[i](chunk);
+ }
+ }.bind(this));
+ },
+
+ transportReady: function PDFDataRangeTransport_transportReady() {
+ this._readyCapability.resolve();
+ },
+
+ requestDataRange:
+ function PDFDataRangeTransport_requestDataRange(begin, end) {
+ throw new Error('Abstract method PDFDataRangeTransport.requestDataRange');
+ },
+
+ abort: function PDFDataRangeTransport_abort() {
+ }
+ };
+ return PDFDataRangeTransport;
+})();
+
+PDFJS.PDFDataRangeTransport = PDFDataRangeTransport;
+
+/**
+ * Proxy to a PDFDocument in the worker thread. Also, contains commonly used
+ * properties that can be read synchronously.
+ * @class
+ * @alias PDFDocumentProxy
+ */
+var PDFDocumentProxy = (function PDFDocumentProxyClosure() {
+ function PDFDocumentProxy(pdfInfo, transport, loadingTask) {
+ this.pdfInfo = pdfInfo;
+ this.transport = transport;
+ this.loadingTask = loadingTask;
+ }
+ PDFDocumentProxy.prototype = /** @lends PDFDocumentProxy.prototype */ {
+ /**
+ * @return {number} Total number of pages the PDF contains.
+ */
+ get numPages() {
+ return this.pdfInfo.numPages;
+ },
+ /**
+ * @return {string} A unique ID to identify a PDF. Not guaranteed to be
+ * unique.
+ */
+ get fingerprint() {
+ return this.pdfInfo.fingerprint;
+ },
+ /**
+ * @param {number} pageNumber The page number to get. The first page is 1.
+ * @return {Promise} A promise that is resolved with a {@link PDFPageProxy}
+ * object.
+ */
+ getPage: function PDFDocumentProxy_getPage(pageNumber) {
+ return this.transport.getPage(pageNumber);
+ },
+ /**
+ * @param {{num: number, gen: number}} ref The page reference. Must have
+ * the 'num' and 'gen' properties.
+ * @return {Promise} A promise that is resolved with the page index that is
+ * associated with the reference.
+ */
+ getPageIndex: function PDFDocumentProxy_getPageIndex(ref) {
+ return this.transport.getPageIndex(ref);
+ },
+ /**
+ * @return {Promise} A promise that is resolved with a lookup table for
+ * mapping named destinations to reference numbers.
+ *
+ * This can be slow for large documents: use getDestination instead
+ */
+ getDestinations: function PDFDocumentProxy_getDestinations() {
+ return this.transport.getDestinations();
+ },
+ /**
+ * @param {string} id The named destination to get.
+ * @return {Promise} A promise that is resolved with all information
+ * of the given named destination.
+ */
+ getDestination: function PDFDocumentProxy_getDestination(id) {
+ return this.transport.getDestination(id);
+ },
+ /**
+ * @return {Promise} A promise that is resolved with a lookup table for
+ * mapping named attachments to their content.
+ */
+ getAttachments: function PDFDocumentProxy_getAttachments() {
+ return this.transport.getAttachments();
+ },
+ /**
+ * @return {Promise} A promise that is resolved with an array of all the
+ * JavaScript strings in the name tree.
+ */
+ getJavaScript: function PDFDocumentProxy_getJavaScript() {
+ return this.transport.getJavaScript();
+ },
+ /**
+ * @return {Promise} A promise that is resolved with an {Array} that is a
+ * tree outline (if it has one) of the PDF. The tree is in the format of:
+ * [
+ * {
+ * title: string,
+ * bold: boolean,
+ * italic: boolean,
+ * color: rgb array,
+ * dest: dest obj,
+ * items: array of more items like this
+ * },
+ * ...
+ * ].
+ */
+ getOutline: function PDFDocumentProxy_getOutline() {
+ return this.transport.getOutline();
+ },
+ /**
+ * @return {Promise} A promise that is resolved with an {Object} that has
+ * info and metadata properties. Info is an {Object} filled with anything
+ * available in the information dictionary and similarly metadata is a
+ * {Metadata} object with information from the metadata section of the PDF.
+ */
+ getMetadata: function PDFDocumentProxy_getMetadata() {
+ return this.transport.getMetadata();
+ },
+ /**
+ * @return {Promise} A promise that is resolved with a TypedArray that has
+ * the raw data from the PDF.
+ */
+ getData: function PDFDocumentProxy_getData() {
+ return this.transport.getData();
+ },
+ /**
+ * @return {Promise} A promise that is resolved when the document's data
+ * is loaded. It is resolved with an {Object} that contains the length
+ * property that indicates size of the PDF data in bytes.
+ */
+ getDownloadInfo: function PDFDocumentProxy_getDownloadInfo() {
+ return this.transport.downloadInfoCapability.promise;
+ },
+ /**
+ * @return {Promise} A promise this is resolved with current stats about
+ * document structures (see {@link PDFDocumentStats}).
+ */
+ getStats: function PDFDocumentProxy_getStats() {
+ return this.transport.getStats();
+ },
+ /**
+ * Cleans up resources allocated by the document, e.g. created @font-face.
+ */
+ cleanup: function PDFDocumentProxy_cleanup() {
+ this.transport.startCleanup();
+ },
+ /**
+ * Destroys current document instance and terminates worker.
+ */
+ destroy: function PDFDocumentProxy_destroy() {
+ return this.loadingTask.destroy();
+ }
+ };
+ return PDFDocumentProxy;
+})();
+
+/**
+ * Page getTextContent parameters.
+ *
+ * @typedef {Object} getTextContentParameters
+ * @param {boolean} normalizeWhitespace - replaces all occurrences of
+ * whitespace with standard spaces (0x20). The default value is `false`.
+ */
+
+/**
+ * Page text content.
+ *
+ * @typedef {Object} TextContent
+ * @property {array} items - array of {@link TextItem}
+ * @property {Object} styles - {@link TextStyles} objects, indexed by font
+ * name.
+ */
+
+/**
+ * Page text content part.
+ *
+ * @typedef {Object} TextItem
+ * @property {string} str - text content.
+ * @property {string} dir - text direction: 'ttb', 'ltr' or 'rtl'.
+ * @property {array} transform - transformation matrix.
+ * @property {number} width - width in device space.
+ * @property {number} height - height in device space.
+ * @property {string} fontName - font name used by pdf.js for converted font.
+ */
+
+/**
+ * Text style.
+ *
+ * @typedef {Object} TextStyle
+ * @property {number} ascent - font ascent.
+ * @property {number} descent - font descent.
+ * @property {boolean} vertical - text is in vertical mode.
+ * @property {string} fontFamily - possible font family
+ */
+
+/**
+ * Page annotation parameters.
+ *
+ * @typedef {Object} GetAnnotationsParameters
+ * @param {string} intent - Determines the annotations that will be fetched,
+ * can be either 'display' (viewable annotations) or 'print'
+ * (printable annotations).
+ * If the parameter is omitted, all annotations are fetched.
+ */
+
+/**
+ * Page render parameters.
+ *
+ * @typedef {Object} RenderParameters
+ * @property {Object} canvasContext - A 2D context of a DOM Canvas object.
+ * @property {PDFJS.PageViewport} viewport - Rendering viewport obtained by
+ * calling of PDFPage.getViewport method.
+ * @property {string} intent - Rendering intent, can be 'display' or 'print'
+ * (default value is 'display').
+ * @property {Array} transform - (optional) Additional transform, applied
+ * just before viewport transform.
+ * @property {Object} imageLayer - (optional) An object that has beginLayout,
+ * endLayout and appendImage functions.
+ * @property {function} continueCallback - (deprecated) A function that will be
+ * called each time the rendering is paused. To continue
+ * rendering call the function that is the first argument
+ * to the callback.
+ */
+
+/**
+ * PDF page operator list.
+ *
+ * @typedef {Object} PDFOperatorList
+ * @property {Array} fnArray - Array containing the operator functions.
+ * @property {Array} argsArray - Array containing the arguments of the
+ * functions.
+ */
+
+/**
+ * Proxy to a PDFPage in the worker thread.
+ * @class
+ * @alias PDFPageProxy
+ */
+var PDFPageProxy = (function PDFPageProxyClosure() {
+ function PDFPageProxy(pageIndex, pageInfo, transport) {
+ this.pageIndex = pageIndex;
+ this.pageInfo = pageInfo;
+ this.transport = transport;
+ this.stats = new StatTimer();
+ this.stats.enabled = !!globalScope.PDFJS.enableStats;
+ this.commonObjs = transport.commonObjs;
+ this.objs = new PDFObjects();
+ this.cleanupAfterRender = false;
+ this.pendingCleanup = false;
+ this.intentStates = {};
+ this.destroyed = false;
+ }
+ PDFPageProxy.prototype = /** @lends PDFPageProxy.prototype */ {
+ /**
+ * @return {number} Page number of the page. First page is 1.
+ */
+ get pageNumber() {
+ return this.pageIndex + 1;
+ },
+ /**
+ * @return {number} The number of degrees the page is rotated clockwise.
+ */
+ get rotate() {
+ return this.pageInfo.rotate;
+ },
+ /**
+ * @return {Object} The reference that points to this page. It has 'num' and
+ * 'gen' properties.
+ */
+ get ref() {
+ return this.pageInfo.ref;
+ },
+ /**
+ * @return {Array} An array of the visible portion of the PDF page in the
+ * user space units - [x1, y1, x2, y2].
+ */
+ get view() {
+ return this.pageInfo.view;
+ },
+ /**
+ * @param {number} scale The desired scale of the viewport.
+ * @param {number} rotate Degrees to rotate the viewport. If omitted this
+ * defaults to the page rotation.
+ * @return {PDFJS.PageViewport} Contains 'width' and 'height' properties
+ * along with transforms required for rendering.
+ */
+ getViewport: function PDFPageProxy_getViewport(scale, rotate) {
+ if (arguments.length < 2) {
+ rotate = this.rotate;
+ }
+ return new PDFJS.PageViewport(this.view, scale, rotate, 0, 0);
+ },
+ /**
+ * @param {GetAnnotationsParameters} params - Annotation parameters.
+ * @return {Promise} A promise that is resolved with an {Array} of the
+ * annotation objects.
+ */
+ getAnnotations: function PDFPageProxy_getAnnotations(params) {
+ var intent = (params && params.intent) || null;
+
+ if (!this.annotationsPromise || this.annotationsIntent !== intent) {
+ this.annotationsPromise = this.transport.getAnnotations(this.pageIndex,
+ intent);
+ this.annotationsIntent = intent;
+ }
+ return this.annotationsPromise;
+ },
+ /**
+ * Begins the process of rendering a page to the desired context.
+ * @param {RenderParameters} params Page render parameters.
+ * @return {RenderTask} An object that contains the promise, which
+ * is resolved when the page finishes rendering.
+ */
+ render: function PDFPageProxy_render(params) {
+ var stats = this.stats;
+ stats.time('Overall');
+
+ // If there was a pending destroy cancel it so no cleanup happens during
+ // this call to render.
+ this.pendingCleanup = false;
+
+ var renderingIntent = (params.intent === 'print' ? 'print' : 'display');
+
+ if (!this.intentStates[renderingIntent]) {
+ this.intentStates[renderingIntent] = {};
+ }
+ var intentState = this.intentStates[renderingIntent];
+
+ // If there's no displayReadyCapability yet, then the operatorList
+ // was never requested before. Make the request and create the promise.
+ if (!intentState.displayReadyCapability) {
+ intentState.receivingOperatorList = true;
+ intentState.displayReadyCapability = createPromiseCapability();
+ intentState.operatorList = {
+ fnArray: [],
+ argsArray: [],
+ lastChunk: false
+ };
+
+ this.stats.time('Page Request');
+ this.transport.messageHandler.send('RenderPageRequest', {
+ pageIndex: this.pageNumber - 1,
+ intent: renderingIntent
+ });
+ }
+
+ var internalRenderTask = new InternalRenderTask(complete, params,
+ this.objs,
+ this.commonObjs,
+ intentState.operatorList,
+ this.pageNumber);
+ internalRenderTask.useRequestAnimationFrame = renderingIntent !== 'print';
+ if (!intentState.renderTasks) {
+ intentState.renderTasks = [];
+ }
+ intentState.renderTasks.push(internalRenderTask);
+ var renderTask = internalRenderTask.task;
+
+ // Obsolete parameter support
+ if (params.continueCallback) {
+ deprecated('render is used with continueCallback parameter');
+ renderTask.onContinue = params.continueCallback;
+ }
+
+ var self = this;
+ intentState.displayReadyCapability.promise.then(
+ function pageDisplayReadyPromise(transparency) {
+ if (self.pendingCleanup) {
+ complete();
+ return;
+ }
+ stats.time('Rendering');
+ internalRenderTask.initalizeGraphics(transparency);
+ internalRenderTask.operatorListChanged();
+ },
+ function pageDisplayReadPromiseError(reason) {
+ complete(reason);
+ }
+ );
+
+ function complete(error) {
+ var i = intentState.renderTasks.indexOf(internalRenderTask);
+ if (i >= 0) {
+ intentState.renderTasks.splice(i, 1);
+ }
+
+ if (self.cleanupAfterRender) {
+ self.pendingCleanup = true;
+ }
+ self._tryCleanup();
+
+ if (error) {
+ internalRenderTask.capability.reject(error);
+ } else {
+ internalRenderTask.capability.resolve();
+ }
+ stats.timeEnd('Rendering');
+ stats.timeEnd('Overall');
+ }
+
+ return renderTask;
+ },
+
+ /**
+ * @return {Promise} A promise resolved with an {@link PDFOperatorList}
+ * object that represents page's operator list.
+ */
+ getOperatorList: function PDFPageProxy_getOperatorList() {
+ function operatorListChanged() {
+ if (intentState.operatorList.lastChunk) {
+ intentState.opListReadCapability.resolve(intentState.operatorList);
+ }
+ }
+
+ var renderingIntent = 'oplist';
+ if (!this.intentStates[renderingIntent]) {
+ this.intentStates[renderingIntent] = {};
+ }
+ var intentState = this.intentStates[renderingIntent];
+
+ if (!intentState.opListReadCapability) {
+ var opListTask = {};
+ opListTask.operatorListChanged = operatorListChanged;
+ intentState.receivingOperatorList = true;
+ intentState.opListReadCapability = createPromiseCapability();
+ intentState.renderTasks = [];
+ intentState.renderTasks.push(opListTask);
+ intentState.operatorList = {
+ fnArray: [],
+ argsArray: [],
+ lastChunk: false
+ };
+
+ this.transport.messageHandler.send('RenderPageRequest', {
+ pageIndex: this.pageIndex,
+ intent: renderingIntent
+ });
+ }
+ return intentState.opListReadCapability.promise;
+ },
+
+ /**
+ * @param {getTextContentParameters} params - getTextContent parameters.
+ * @return {Promise} That is resolved a {@link TextContent}
+ * object that represent the page text content.
+ */
+ getTextContent: function PDFPageProxy_getTextContent(params) {
+ var normalizeWhitespace = (params && params.normalizeWhitespace) || false;
+
+ return this.transport.messageHandler.sendWithPromise('GetTextContent', {
+ pageIndex: this.pageNumber - 1,
+ normalizeWhitespace: normalizeWhitespace,
+ });
+ },
+
+ /**
+ * Destroys page object.
+ */
+ _destroy: function PDFPageProxy_destroy() {
+ this.destroyed = true;
+ this.transport.pageCache[this.pageIndex] = null;
+
+ var waitOn = [];
+ Object.keys(this.intentStates).forEach(function(intent) {
+ var intentState = this.intentStates[intent];
+ intentState.renderTasks.forEach(function(renderTask) {
+ var renderCompleted = renderTask.capability.promise.
+ catch(function () {}); // ignoring failures
+ waitOn.push(renderCompleted);
+ renderTask.cancel();
+ });
+ }, this);
+ this.objs.clear();
+ this.annotationsPromise = null;
+ this.pendingCleanup = false;
+ return Promise.all(waitOn);
+ },
+
+ /**
+ * Cleans up resources allocated by the page. (deprecated)
+ */
+ destroy: function() {
+ deprecated('page destroy method, use cleanup() instead');
+ this.cleanup();
+ },
+
+ /**
+ * Cleans up resources allocated by the page.
+ */
+ cleanup: function PDFPageProxy_cleanup() {
+ this.pendingCleanup = true;
+ this._tryCleanup();
+ },
+ /**
+ * For internal use only. Attempts to clean up if rendering is in a state
+ * where that's possible.
+ * @ignore
+ */
+ _tryCleanup: function PDFPageProxy_tryCleanup() {
+ if (!this.pendingCleanup ||
+ Object.keys(this.intentStates).some(function(intent) {
+ var intentState = this.intentStates[intent];
+ return (intentState.renderTasks.length !== 0 ||
+ intentState.receivingOperatorList);
+ }, this)) {
+ return;
+ }
+
+ Object.keys(this.intentStates).forEach(function(intent) {
+ delete this.intentStates[intent];
+ }, this);
+ this.objs.clear();
+ this.annotationsPromise = null;
+ this.pendingCleanup = false;
+ },
+ /**
+ * For internal use only.
+ * @ignore
+ */
+ _startRenderPage: function PDFPageProxy_startRenderPage(transparency,
+ intent) {
+ var intentState = this.intentStates[intent];
+ // TODO Refactor RenderPageRequest to separate rendering
+ // and operator list logic
+ if (intentState.displayReadyCapability) {
+ intentState.displayReadyCapability.resolve(transparency);
+ }
+ },
+ /**
+ * For internal use only.
+ * @ignore
+ */
+ _renderPageChunk: function PDFPageProxy_renderPageChunk(operatorListChunk,
+ intent) {
+ var intentState = this.intentStates[intent];
+ var i, ii;
+ // Add the new chunk to the current operator list.
+ for (i = 0, ii = operatorListChunk.length; i < ii; i++) {
+ intentState.operatorList.fnArray.push(operatorListChunk.fnArray[i]);
+ intentState.operatorList.argsArray.push(
+ operatorListChunk.argsArray[i]);
+ }
+ intentState.operatorList.lastChunk = operatorListChunk.lastChunk;
+
+ // Notify all the rendering tasks there are more operators to be consumed.
+ for (i = 0; i < intentState.renderTasks.length; i++) {
+ intentState.renderTasks[i].operatorListChanged();
+ }
+
+ if (operatorListChunk.lastChunk) {
+ intentState.receivingOperatorList = false;
+ this._tryCleanup();
+ }
+ }
+ };
+ return PDFPageProxy;
+})();
+
+/**
+ * PDF.js web worker abstraction, it controls instantiation of PDF documents and
+ * WorkerTransport for them. If creation of a web worker is not possible,
+ * a "fake" worker will be used instead.
+ * @class
+ */
+var PDFWorker = (function PDFWorkerClosure() {
+ var nextFakeWorkerId = 0;
+
+ // Loads worker code into main thread.
+ function setupFakeWorkerGlobal() {
+ if (!PDFJS.fakeWorkerFilesLoadedCapability) {
+ PDFJS.fakeWorkerFilesLoadedCapability = createPromiseCapability();
+ // In the developer build load worker_loader which in turn loads all the
+ // other files and resolves the promise. In production only the
+ // pdf.worker.js file is needed.
+ Util.loadScript(PDFJS.workerSrc, function() {
+ PDFJS.fakeWorkerFilesLoadedCapability.resolve();
+ });
+ }
+ return PDFJS.fakeWorkerFilesLoadedCapability.promise;
+ }
+
+ function PDFWorker(name) {
+ this.name = name;
+ this.destroyed = false;
+
+ this._readyCapability = createPromiseCapability();
+ this._port = null;
+ this._webWorker = null;
+ this._messageHandler = null;
+ this._initialize();
+ }
+
+ PDFWorker.prototype = /** @lends PDFWorker.prototype */ {
+ get promise() {
+ return this._readyCapability.promise;
+ },
+
+ get port() {
+ return this._port;
+ },
+
+ get messageHandler() {
+ return this._messageHandler;
+ },
+
+ _initialize: function PDFWorker_initialize() {
+ // If worker support isn't disabled explicit and the browser has worker
+ // support, create a new web worker and test if it/the browser fullfills
+ // all requirements to run parts of pdf.js in a web worker.
+ // Right now, the requirement is, that an Uint8Array is still an
+ // Uint8Array as it arrives on the worker. (Chrome added this with v.15.)
+ if (!globalScope.PDFJS.disableWorker && typeof Worker !== 'undefined') {
+ var workerSrc = PDFJS.workerSrc;
+ if (!workerSrc) {
+ error('No PDFJS.workerSrc specified');
+ }
+
+ try {
+ // Some versions of FF can't create a worker on localhost, see:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=683280
+ var worker = new Worker(workerSrc);
+ var messageHandler = new MessageHandler('main', 'worker', worker);
+
+ messageHandler.on('test', function PDFWorker_test(data) {
+ if (this.destroyed) {
+ this._readyCapability.reject(new Error('Worker was destroyed'));
+ messageHandler.destroy();
+ worker.terminate();
+ return; // worker was destroyed
+ }
+ var supportTypedArray = data && data.supportTypedArray;
+ if (supportTypedArray) {
+ this._messageHandler = messageHandler;
+ this._port = worker;
+ this._webWorker = worker;
+ if (!data.supportTransfers) {
+ PDFJS.postMessageTransfers = false;
+ }
+ this._readyCapability.resolve();
+ } else {
+ this._setupFakeWorker();
+ messageHandler.destroy();
+ worker.terminate();
+ }
+ }.bind(this));
+
+ messageHandler.on('console_log', function (data) {
+ console.log.apply(console, data);
+ });
+ messageHandler.on('console_error', function (data) {
+ console.error.apply(console, data);
+ });
+
+ var testObj = new Uint8Array([PDFJS.postMessageTransfers ? 255 : 0]);
+ // Some versions of Opera throw a DATA_CLONE_ERR on serializing the
+ // typed array. Also, checking if we can use transfers.
+ try {
+ messageHandler.send('test', testObj, [testObj.buffer]);
+ } catch (ex) {
+ info('Cannot use postMessage transfers');
+ testObj[0] = 0;
+ messageHandler.send('test', testObj);
+ }
+ return;
+ } catch (e) {
+ info('The worker has been disabled.');
+ }
+ }
+ // Either workers are disabled, not supported or have thrown an exception.
+ // Thus, we fallback to a faked worker.
+ this._setupFakeWorker();
+ },
+
+ _setupFakeWorker: function PDFWorker_setupFakeWorker() {
+ warn('Setting up fake worker.');
+ globalScope.PDFJS.disableWorker = true;
+
+ setupFakeWorkerGlobal().then(function () {
+ if (this.destroyed) {
+ this._readyCapability.reject(new Error('Worker was destroyed'));
+ return;
+ }
+
+ // If we don't use a worker, just post/sendMessage to the main thread.
+ var port = {
+ _listeners: [],
+ postMessage: function (obj) {
+ var e = {data: obj};
+ this._listeners.forEach(function (listener) {
+ listener.call(this, e);
+ }, this);
+ },
+ addEventListener: function (name, listener) {
+ this._listeners.push(listener);
+ },
+ removeEventListener: function (name, listener) {
+ var i = this._listeners.indexOf(listener);
+ this._listeners.splice(i, 1);
+ },
+ terminate: function () {}
+ };
+ this._port = port;
+
+ // All fake workers use the same port, making id unique.
+ var id = 'fake' + (nextFakeWorkerId++);
+
+ // If the main thread is our worker, setup the handling for the
+ // messages -- the main thread sends to it self.
+ var workerHandler = new MessageHandler(id + '_worker', id, port);
+ PDFJS.WorkerMessageHandler.setup(workerHandler, port);
+
+ var messageHandler = new MessageHandler(id, id + '_worker', port);
+ this._messageHandler = messageHandler;
+ this._readyCapability.resolve();
+ }.bind(this));
+ },
+
+ /**
+ * Destroys the worker instance.
+ */
+ destroy: function PDFWorker_destroy() {
+ this.destroyed = true;
+ if (this._webWorker) {
+ // We need to terminate only web worker created resource.
+ this._webWorker.terminate();
+ this._webWorker = null;
+ }
+ this._port = null;
+ if (this._messageHandler) {
+ this._messageHandler.destroy();
+ this._messageHandler = null;
+ }
+ }
+ };
+
+ return PDFWorker;
+})();
+PDFJS.PDFWorker = PDFWorker;
+
+/**
+ * For internal use only.
+ * @ignore
+ */
+var WorkerTransport = (function WorkerTransportClosure() {
+ function WorkerTransport(messageHandler, loadingTask, pdfDataRangeTransport) {
+ this.messageHandler = messageHandler;
+ this.loadingTask = loadingTask;
+ this.pdfDataRangeTransport = pdfDataRangeTransport;
+ this.commonObjs = new PDFObjects();
+ this.fontLoader = new FontLoader(loadingTask.docId);
+
+ this.destroyed = false;
+ this.destroyCapability = null;
+
+ this.pageCache = [];
+ this.pagePromises = [];
+ this.downloadInfoCapability = createPromiseCapability();
+
+ this.setupMessageHandler();
+ }
+ WorkerTransport.prototype = {
+ destroy: function WorkerTransport_destroy() {
+ if (this.destroyCapability) {
+ return this.destroyCapability.promise;
+ }
+
+ this.destroyed = true;
+ this.destroyCapability = createPromiseCapability();
+
+ var waitOn = [];
+ // We need to wait for all renderings to be completed, e.g.
+ // timeout/rAF can take a long time.
+ this.pageCache.forEach(function (page) {
+ if (page) {
+ waitOn.push(page._destroy());
+ }
+ });
+ this.pageCache = [];
+ this.pagePromises = [];
+ var self = this;
+ // We also need to wait for the worker to finish its long running tasks.
+ var terminated = this.messageHandler.sendWithPromise('Terminate', null);
+ waitOn.push(terminated);
+ Promise.all(waitOn).then(function () {
+ self.fontLoader.clear();
+ if (self.pdfDataRangeTransport) {
+ self.pdfDataRangeTransport.abort();
+ self.pdfDataRangeTransport = null;
+ }
+ if (self.messageHandler) {
+ self.messageHandler.destroy();
+ self.messageHandler = null;
+ }
+ self.destroyCapability.resolve();
+ }, this.destroyCapability.reject);
+ return this.destroyCapability.promise;
+ },
+
+ setupMessageHandler:
+ function WorkerTransport_setupMessageHandler() {
+ var messageHandler = this.messageHandler;
+
+ function updatePassword(password) {
+ messageHandler.send('UpdatePassword', password);
+ }
+
+ var pdfDataRangeTransport = this.pdfDataRangeTransport;
+ if (pdfDataRangeTransport) {
+ pdfDataRangeTransport.addRangeListener(function(begin, chunk) {
+ messageHandler.send('OnDataRange', {
+ begin: begin,
+ chunk: chunk
+ });
+ });
+
+ pdfDataRangeTransport.addProgressListener(function(loaded) {
+ messageHandler.send('OnDataProgress', {
+ loaded: loaded
+ });
+ });
+
+ pdfDataRangeTransport.addProgressiveReadListener(function(chunk) {
+ messageHandler.send('OnDataRange', {
+ chunk: chunk
+ });
+ });
+
+ messageHandler.on('RequestDataRange',
+ function transportDataRange(data) {
+ pdfDataRangeTransport.requestDataRange(data.begin, data.end);
+ }, this);
+ }
+
+ messageHandler.on('GetDoc', function transportDoc(data) {
+ var pdfInfo = data.pdfInfo;
+ this.numPages = data.pdfInfo.numPages;
+ var loadingTask = this.loadingTask;
+ var pdfDocument = new PDFDocumentProxy(pdfInfo, this, loadingTask);
+ this.pdfDocument = pdfDocument;
+ loadingTask._capability.resolve(pdfDocument);
+ }, this);
+
+ messageHandler.on('NeedPassword',
+ function transportNeedPassword(exception) {
+ var loadingTask = this.loadingTask;
+ if (loadingTask.onPassword) {
+ return loadingTask.onPassword(updatePassword,
+ PasswordResponses.NEED_PASSWORD);
+ }
+ loadingTask._capability.reject(
+ new PasswordException(exception.message, exception.code));
+ }, this);
+
+ messageHandler.on('IncorrectPassword',
+ function transportIncorrectPassword(exception) {
+ var loadingTask = this.loadingTask;
+ if (loadingTask.onPassword) {
+ return loadingTask.onPassword(updatePassword,
+ PasswordResponses.INCORRECT_PASSWORD);
+ }
+ loadingTask._capability.reject(
+ new PasswordException(exception.message, exception.code));
+ }, this);
+
+ messageHandler.on('InvalidPDF', function transportInvalidPDF(exception) {
+ this.loadingTask._capability.reject(
+ new InvalidPDFException(exception.message));
+ }, this);
+
+ messageHandler.on('MissingPDF', function transportMissingPDF(exception) {
+ this.loadingTask._capability.reject(
+ new MissingPDFException(exception.message));
+ }, this);
+
+ messageHandler.on('UnexpectedResponse',
+ function transportUnexpectedResponse(exception) {
+ this.loadingTask._capability.reject(
+ new UnexpectedResponseException(exception.message, exception.status));
+ }, this);
+
+ messageHandler.on('UnknownError',
+ function transportUnknownError(exception) {
+ this.loadingTask._capability.reject(
+ new UnknownErrorException(exception.message, exception.details));
+ }, this);
+
+ messageHandler.on('DataLoaded', function transportPage(data) {
+ this.downloadInfoCapability.resolve(data);
+ }, this);
+
+ messageHandler.on('PDFManagerReady', function transportPage(data) {
+ if (this.pdfDataRangeTransport) {
+ this.pdfDataRangeTransport.transportReady();
+ }
+ }, this);
+
+ messageHandler.on('StartRenderPage', function transportRender(data) {
+ if (this.destroyed) {
+ return; // Ignore any pending requests if the worker was terminated.
+ }
+ var page = this.pageCache[data.pageIndex];
+
+ page.stats.timeEnd('Page Request');
+ page._startRenderPage(data.transparency, data.intent);
+ }, this);
+
+ messageHandler.on('RenderPageChunk', function transportRender(data) {
+ if (this.destroyed) {
+ return; // Ignore any pending requests if the worker was terminated.
+ }
+ var page = this.pageCache[data.pageIndex];
+
+ page._renderPageChunk(data.operatorList, data.intent);
+ }, this);
+
+ messageHandler.on('commonobj', function transportObj(data) {
+ if (this.destroyed) {
+ return; // Ignore any pending requests if the worker was terminated.
+ }
+
+ var id = data[0];
+ var type = data[1];
+ if (this.commonObjs.hasData(id)) {
+ return;
+ }
+
+ switch (type) {
+ case 'Font':
+ var exportedData = data[2];
+
+ var font;
+ if ('error' in exportedData) {
+ var error = exportedData.error;
+ warn('Error during font loading: ' + error);
+ this.commonObjs.resolve(id, error);
+ break;
+ } else {
+ font = new FontFaceObject(exportedData);
+ }
+
+ this.fontLoader.bind(
+ [font],
+ function fontReady(fontObjs) {
+ this.commonObjs.resolve(id, font);
+ }.bind(this)
+ );
+ break;
+ case 'FontPath':
+ this.commonObjs.resolve(id, data[2]);
+ break;
+ default:
+ error('Got unknown common object type ' + type);
+ }
+ }, this);
+
+ messageHandler.on('obj', function transportObj(data) {
+ if (this.destroyed) {
+ return; // Ignore any pending requests if the worker was terminated.
+ }
+
+ var id = data[0];
+ var pageIndex = data[1];
+ var type = data[2];
+ var pageProxy = this.pageCache[pageIndex];
+ var imageData;
+ if (pageProxy.objs.hasData(id)) {
+ return;
+ }
+
+ switch (type) {
+ case 'JpegStream':
+ imageData = data[3];
+ loadJpegStream(id, imageData, pageProxy.objs);
+ break;
+ case 'Image':
+ imageData = data[3];
+ pageProxy.objs.resolve(id, imageData);
+
+ // heuristics that will allow not to store large data
+ var MAX_IMAGE_SIZE_TO_STORE = 8000000;
+ if (imageData && 'data' in imageData &&
+ imageData.data.length > MAX_IMAGE_SIZE_TO_STORE) {
+ pageProxy.cleanupAfterRender = true;
+ }
+ break;
+ default:
+ error('Got unknown object type ' + type);
+ }
+ }, this);
+
+ messageHandler.on('DocProgress', function transportDocProgress(data) {
+ if (this.destroyed) {
+ return; // Ignore any pending requests if the worker was terminated.
+ }
+
+ var loadingTask = this.loadingTask;
+ if (loadingTask.onProgress) {
+ loadingTask.onProgress({
+ loaded: data.loaded,
+ total: data.total
+ });
+ }
+ }, this);
+
+ messageHandler.on('PageError', function transportError(data) {
+ if (this.destroyed) {
+ return; // Ignore any pending requests if the worker was terminated.
+ }
+
+ var page = this.pageCache[data.pageNum - 1];
+ var intentState = page.intentStates[data.intent];
+ if (intentState.displayReadyCapability) {
+ intentState.displayReadyCapability.reject(data.error);
+ } else {
+ error(data.error);
+ }
+ }, this);
+
+ messageHandler.on('UnsupportedFeature',
+ function transportUnsupportedFeature(data) {
+ if (this.destroyed) {
+ return; // Ignore any pending requests if the worker was terminated.
+ }
+ var featureId = data.featureId;
+ var loadingTask = this.loadingTask;
+ if (loadingTask.onUnsupportedFeature) {
+ loadingTask.onUnsupportedFeature(featureId);
+ }
+ PDFJS.UnsupportedManager.notify(featureId);
+ }, this);
+
+ messageHandler.on('JpegDecode', function(data) {
+ if (this.destroyed) {
+ return Promise.reject('Worker was terminated');
+ }
+
+ var imageUrl = data[0];
+ var components = data[1];
+ if (components !== 3 && components !== 1) {
+ return Promise.reject(
+ new Error('Only 3 components or 1 component can be returned'));
+ }
+
+ return new Promise(function (resolve, reject) {
+ var img = new Image();
+ img.onload = function () {
+ var width = img.width;
+ var height = img.height;
+ var size = width * height;
+ var rgbaLength = size * 4;
+ var buf = new Uint8Array(size * components);
+ var tmpCanvas = createScratchCanvas(width, height);
+ var tmpCtx = tmpCanvas.getContext('2d');
+ tmpCtx.drawImage(img, 0, 0);
+ var data = tmpCtx.getImageData(0, 0, width, height).data;
+ var i, j;
+
+ if (components === 3) {
+ for (i = 0, j = 0; i < rgbaLength; i += 4, j += 3) {
+ buf[j] = data[i];
+ buf[j + 1] = data[i + 1];
+ buf[j + 2] = data[i + 2];
+ }
+ } else if (components === 1) {
+ for (i = 0, j = 0; i < rgbaLength; i += 4, j++) {
+ buf[j] = data[i];
+ }
+ }
+ resolve({ data: buf, width: width, height: height});
+ };
+ img.onerror = function () {
+ reject(new Error('JpegDecode failed to load image'));
+ };
+ img.src = imageUrl;
+ });
+ }, this);
+ },
+
+ getData: function WorkerTransport_getData() {
+ return this.messageHandler.sendWithPromise('GetData', null);
+ },
+
+ getPage: function WorkerTransport_getPage(pageNumber, capability) {
+ if (pageNumber <= 0 || pageNumber > this.numPages ||
+ (pageNumber|0) !== pageNumber) {
+ return Promise.reject(new Error('Invalid page request'));
+ }
+
+ var pageIndex = pageNumber - 1;
+ if (pageIndex in this.pagePromises) {
+ return this.pagePromises[pageIndex];
+ }
+ var promise = this.messageHandler.sendWithPromise('GetPage', {
+ pageIndex: pageIndex
+ }).then(function (pageInfo) {
+ if (this.destroyed) {
+ throw new Error('Transport destroyed');
+ }
+ var page = new PDFPageProxy(pageIndex, pageInfo, this);
+ this.pageCache[pageIndex] = page;
+ return page;
+ }.bind(this));
+ this.pagePromises[pageIndex] = promise;
+ return promise;
+ },
+
+ getPageIndex: function WorkerTransport_getPageIndexByRef(ref) {
+ return this.messageHandler.sendWithPromise('GetPageIndex', { ref: ref });
+ },
+
+ getAnnotations: function WorkerTransport_getAnnotations(pageIndex, intent) {
+ return this.messageHandler.sendWithPromise('GetAnnotations', {
+ pageIndex: pageIndex,
+ intent: intent,
+ });
+ },
+
+ getDestinations: function WorkerTransport_getDestinations() {
+ return this.messageHandler.sendWithPromise('GetDestinations', null);
+ },
+
+ getDestination: function WorkerTransport_getDestination(id) {
+ return this.messageHandler.sendWithPromise('GetDestination', { id: id });
+ },
+
+ getAttachments: function WorkerTransport_getAttachments() {
+ return this.messageHandler.sendWithPromise('GetAttachments', null);
+ },
+
+ getJavaScript: function WorkerTransport_getJavaScript() {
+ return this.messageHandler.sendWithPromise('GetJavaScript', null);
+ },
+
+ getOutline: function WorkerTransport_getOutline() {
+ return this.messageHandler.sendWithPromise('GetOutline', null);
+ },
+
+ getMetadata: function WorkerTransport_getMetadata() {
+ return this.messageHandler.sendWithPromise('GetMetadata', null).
+ then(function transportMetadata(results) {
+ return {
+ info: results[0],
+ metadata: (results[1] ? new PDFJS.Metadata(results[1]) : null)
+ };
+ });
+ },
+
+ getStats: function WorkerTransport_getStats() {
+ return this.messageHandler.sendWithPromise('GetStats', null);
+ },
+
+ startCleanup: function WorkerTransport_startCleanup() {
+ this.messageHandler.sendWithPromise('Cleanup', null).
+ then(function endCleanup() {
+ for (var i = 0, ii = this.pageCache.length; i < ii; i++) {
+ var page = this.pageCache[i];
+ if (page) {
+ page.cleanup();
+ }
+ }
+ this.commonObjs.clear();
+ this.fontLoader.clear();
+ }.bind(this));
+ }
+ };
+ return WorkerTransport;
+
+})();
+
+/**
+ * A PDF document and page is built of many objects. E.g. there are objects
+ * for fonts, images, rendering code and such. These objects might get processed
+ * inside of a worker. The `PDFObjects` implements some basic functions to
+ * manage these objects.
+ * @ignore
+ */
+var PDFObjects = (function PDFObjectsClosure() {
+ function PDFObjects() {
+ this.objs = {};
+ }
+
+ PDFObjects.prototype = {
+ /**
+ * Internal function.
+ * Ensures there is an object defined for `objId`.
+ */
+ ensureObj: function PDFObjects_ensureObj(objId) {
+ if (this.objs[objId]) {
+ return this.objs[objId];
+ }
+
+ var obj = {
+ capability: createPromiseCapability(),
+ data: null,
+ resolved: false
+ };
+ this.objs[objId] = obj;
+
+ return obj;
+ },
+
+ /**
+ * If called *without* callback, this returns the data of `objId` but the
+ * object needs to be resolved. If it isn't, this function throws.
+ *
+ * If called *with* a callback, the callback is called with the data of the
+ * object once the object is resolved. That means, if you call this
+ * function and the object is already resolved, the callback gets called
+ * right away.
+ */
+ get: function PDFObjects_get(objId, callback) {
+ // If there is a callback, then the get can be async and the object is
+ // not required to be resolved right now
+ if (callback) {
+ this.ensureObj(objId).capability.promise.then(callback);
+ return null;
+ }
+
+ // If there isn't a callback, the user expects to get the resolved data
+ // directly.
+ var obj = this.objs[objId];
+
+ // If there isn't an object yet or the object isn't resolved, then the
+ // data isn't ready yet!
+ if (!obj || !obj.resolved) {
+ error('Requesting object that isn\'t resolved yet ' + objId);
+ }
+
+ return obj.data;
+ },
+
+ /**
+ * Resolves the object `objId` with optional `data`.
+ */
+ resolve: function PDFObjects_resolve(objId, data) {
+ var obj = this.ensureObj(objId);
+
+ obj.resolved = true;
+ obj.data = data;
+ obj.capability.resolve(data);
+ },
+
+ isResolved: function PDFObjects_isResolved(objId) {
+ var objs = this.objs;
+
+ if (!objs[objId]) {
+ return false;
+ } else {
+ return objs[objId].resolved;
+ }
+ },
+
+ hasData: function PDFObjects_hasData(objId) {
+ return this.isResolved(objId);
+ },
+
+ /**
+ * Returns the data of `objId` if object exists, null otherwise.
+ */
+ getData: function PDFObjects_getData(objId) {
+ var objs = this.objs;
+ if (!objs[objId] || !objs[objId].resolved) {
+ return null;
+ } else {
+ return objs[objId].data;
+ }
+ },
+
+ clear: function PDFObjects_clear() {
+ this.objs = {};
+ }
+ };
+ return PDFObjects;
+})();
+
+/**
+ * Allows controlling of the rendering tasks.
+ * @class
+ * @alias RenderTask
+ */
+var RenderTask = (function RenderTaskClosure() {
+ function RenderTask(internalRenderTask) {
+ this._internalRenderTask = internalRenderTask;
+
+ /**
+ * Callback for incremental rendering -- a function that will be called
+ * each time the rendering is paused. To continue rendering call the
+ * function that is the first argument to the callback.
+ * @type {function}
+ */
+ this.onContinue = null;
+ }
+
+ RenderTask.prototype = /** @lends RenderTask.prototype */ {
+ /**
+ * Promise for rendering task completion.
+ * @return {Promise}
+ */
+ get promise() {
+ return this._internalRenderTask.capability.promise;
+ },
+
+ /**
+ * Cancels the rendering task. If the task is currently rendering it will
+ * not be cancelled until graphics pauses with a timeout. The promise that
+ * this object extends will resolved when cancelled.
+ */
+ cancel: function RenderTask_cancel() {
+ this._internalRenderTask.cancel();
+ },
+
+ /**
+ * Registers callbacks to indicate the rendering task completion.
+ *
+ * @param {function} onFulfilled The callback for the rendering completion.
+ * @param {function} onRejected The callback for the rendering failure.
+ * @return {Promise} A promise that is resolved after the onFulfilled or
+ * onRejected callback.
+ */
+ then: function RenderTask_then(onFulfilled, onRejected) {
+ return this.promise.then.apply(this.promise, arguments);
+ }
+ };
+
+ return RenderTask;
+})();
+
+/**
+ * For internal use only.
+ * @ignore
+ */
+var InternalRenderTask = (function InternalRenderTaskClosure() {
+
+ function InternalRenderTask(callback, params, objs, commonObjs, operatorList,
+ pageNumber) {
+ this.callback = callback;
+ this.params = params;
+ this.objs = objs;
+ this.commonObjs = commonObjs;
+ this.operatorListIdx = null;
+ this.operatorList = operatorList;
+ this.pageNumber = pageNumber;
+ this.running = false;
+ this.graphicsReadyCallback = null;
+ this.graphicsReady = false;
+ this.useRequestAnimationFrame = false;
+ this.cancelled = false;
+ this.capability = createPromiseCapability();
+ this.task = new RenderTask(this);
+ // caching this-bound methods
+ this._continueBound = this._continue.bind(this);
+ this._scheduleNextBound = this._scheduleNext.bind(this);
+ this._nextBound = this._next.bind(this);
+ }
+
+ InternalRenderTask.prototype = {
+
+ initalizeGraphics:
+ function InternalRenderTask_initalizeGraphics(transparency) {
+
+ if (this.cancelled) {
+ return;
+ }
+ if (PDFJS.pdfBug && 'StepperManager' in globalScope &&
+ globalScope.StepperManager.enabled) {
+ this.stepper = globalScope.StepperManager.create(this.pageNumber - 1);
+ this.stepper.init(this.operatorList);
+ this.stepper.nextBreakPoint = this.stepper.getNextBreakPoint();
+ }
+
+ var params = this.params;
+ this.gfx = new CanvasGraphics(params.canvasContext, this.commonObjs,
+ this.objs, params.imageLayer);
+
+ this.gfx.beginDrawing(params.transform, params.viewport, transparency);
+ this.operatorListIdx = 0;
+ this.graphicsReady = true;
+ if (this.graphicsReadyCallback) {
+ this.graphicsReadyCallback();
+ }
+ },
+
+ cancel: function InternalRenderTask_cancel() {
+ this.running = false;
+ this.cancelled = true;
+ this.callback('cancelled');
+ },
+
+ operatorListChanged: function InternalRenderTask_operatorListChanged() {
+ if (!this.graphicsReady) {
+ if (!this.graphicsReadyCallback) {
+ this.graphicsReadyCallback = this._continueBound;
+ }
+ return;
+ }
+
+ if (this.stepper) {
+ this.stepper.updateOperatorList(this.operatorList);
+ }
+
+ if (this.running) {
+ return;
+ }
+ this._continue();
+ },
+
+ _continue: function InternalRenderTask__continue() {
+ this.running = true;
+ if (this.cancelled) {
+ return;
+ }
+ if (this.task.onContinue) {
+ this.task.onContinue.call(this.task, this._scheduleNextBound);
+ } else {
+ this._scheduleNext();
+ }
+ },
+
+ _scheduleNext: function InternalRenderTask__scheduleNext() {
+ if (this.useRequestAnimationFrame) {
+ window.requestAnimationFrame(this._nextBound);
+ } else {
+ Promise.resolve(undefined).then(this._nextBound);
+ }
+ },
+
+ _next: function InternalRenderTask__next() {
+ if (this.cancelled) {
+ return;
+ }
+ this.operatorListIdx = this.gfx.executeOperatorList(this.operatorList,
+ this.operatorListIdx,
+ this._continueBound,
+ this.stepper);
+ if (this.operatorListIdx === this.operatorList.argsArray.length) {
+ this.running = false;
+ if (this.operatorList.lastChunk) {
+ this.gfx.endDrawing();
+ this.callback();
+ }
+ }
+ }
+
+ };
+
+ return InternalRenderTask;
+})();
+
+/**
+ * (Deprecated) Global observer of unsupported feature usages. Use
+ * onUnsupportedFeature callback of the {PDFDocumentLoadingTask} instance.
+ */
+PDFJS.UnsupportedManager = (function UnsupportedManagerClosure() {
+ var listeners = [];
+ return {
+ listen: function (cb) {
+ deprecated('Global UnsupportedManager.listen is used: ' +
+ ' use PDFDocumentLoadingTask.onUnsupportedFeature instead');
+ listeners.push(cb);
+ },
+ notify: function (featureId) {
+ for (var i = 0, ii = listeners.length; i < ii; i++) {
+ listeners[i](featureId);
+ }
+ }
+ };
+})();
+
+
+var Metadata = PDFJS.Metadata = (function MetadataClosure() {
+ function fixMetadata(meta) {
+ return meta.replace(/>\\376\\377([^<]+)/g, function(all, codes) {
+ var bytes = codes.replace(/\\([0-3])([0-7])([0-7])/g,
+ function(code, d1, d2, d3) {
+ return String.fromCharCode(d1 * 64 + d2 * 8 + d3 * 1);
+ });
+ var chars = '';
+ for (var i = 0; i < bytes.length; i += 2) {
+ var code = bytes.charCodeAt(i) * 256 + bytes.charCodeAt(i + 1);
+ chars += code >= 32 && code < 127 && code !== 60 && code !== 62 &&
+ code !== 38 && false ? String.fromCharCode(code) :
+ '' + (0x10000 + code).toString(16).substring(1) + ';';
+ }
+ return '>' + chars;
+ });
+ }
+
+ function Metadata(meta) {
+ if (typeof meta === 'string') {
+ // Ghostscript produces invalid metadata
+ meta = fixMetadata(meta);
+
+ var parser = new DOMParser();
+ meta = parser.parseFromString(meta, 'application/xml');
+ } else if (!(meta instanceof Document)) {
+ error('Metadata: Invalid metadata object');
+ }
+
+ this.metaDocument = meta;
+ this.metadata = {};
+ this.parse();
+ }
+
+ Metadata.prototype = {
+ parse: function Metadata_parse() {
+ var doc = this.metaDocument;
+ var rdf = doc.documentElement;
+
+ if (rdf.nodeName.toLowerCase() !== 'rdf:rdf') { // Wrapped in
+ rdf = rdf.firstChild;
+ while (rdf && rdf.nodeName.toLowerCase() !== 'rdf:rdf') {
+ rdf = rdf.nextSibling;
+ }
+ }
+
+ var nodeName = (rdf) ? rdf.nodeName.toLowerCase() : null;
+ if (!rdf || nodeName !== 'rdf:rdf' || !rdf.hasChildNodes()) {
+ return;
+ }
+
+ var children = rdf.childNodes, desc, entry, name, i, ii, length, iLength;
+ for (i = 0, length = children.length; i < length; i++) {
+ desc = children[i];
+ if (desc.nodeName.toLowerCase() !== 'rdf:description') {
+ continue;
+ }
+
+ for (ii = 0, iLength = desc.childNodes.length; ii < iLength; ii++) {
+ if (desc.childNodes[ii].nodeName.toLowerCase() !== '#text') {
+ entry = desc.childNodes[ii];
+ name = entry.nodeName.toLowerCase();
+ this.metadata[name] = entry.textContent.trim();
+ }
+ }
+ }
+ },
+
+ get: function Metadata_get(name) {
+ return this.metadata[name] || null;
+ },
+
+ has: function Metadata_has(name) {
+ return typeof this.metadata[name] !== 'undefined';
+ }
+ };
+
+ return Metadata;
+})();
+
+
+//