diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 847bd99bc3..b88ded33b2 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -112,14 +112,16 @@ module.exports = CollaboratorsInviteController = acceptInvite: (req, res, next) -> projectId = req.params.Project_id - inviteId = req.params.invite_id - {token} = req.body + token = req.params.token currentUser = AuthenticationController.getSessionUser(req) - logger.log {projectId, inviteId, userId: currentUser._id}, "accepting invite" - CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) -> + logger.log {projectId, userId: currentUser._id, token}, "got request to accept invite" + CollaboratorsInviteHandler.acceptInvite projectId, token, currentUser, (err) -> if err? - logger.err {projectId, inviteId}, "error accepting invite by token" + logger.err {projectId, token}, "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}" + AnalyticsManger.recordEvent(currentUser._id, "project-invite-accept", {projectId:projectId, userId:currentUser._id}) + if req.xhr + res.sendStatus 204 # Done async via project page notification + else + 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 bf03fe242d..960bcff5c3 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -77,10 +77,12 @@ module.exports = CollaboratorsInviteHandler = if err? logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error saving token" return callback(err) + # Send email and notification in background CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) -> if err? logger.err {projectId, email}, "error sending messages for invite" - callback(err, invite) + callback(null, invite) + revokeInvite: (projectId, inviteId, callback=(err)->) -> logger.log {projectId, inviteId}, "removing invite" @@ -117,15 +119,15 @@ module.exports = CollaboratorsInviteHandler = return callback(null, null) callback(null, invite) - acceptInvite: (projectId, inviteId, tokenString, user, callback=(err)->) -> - logger.log {projectId, inviteId, userId: user._id}, "accepting invite" + acceptInvite: (projectId, tokenString, user, callback=(err)->) -> + logger.log {projectId, userId: user._id, tokenString}, "accepting invite" CollaboratorsInviteHandler.getInviteByToken projectId, tokenString, (err, invite) -> if err? - logger.err {err, projectId, inviteId}, "error finding invite" + logger.err {err, projectId, tokenString}, "error finding invite" return callback(err) if !invite err = new Errors.NotFoundError("no matching invite found") - logger.log {err, projectId, inviteId, tokenString}, "no matching invite found" + logger.log {err, projectId, tokenString}, "no matching invite found" return callback(err) inviteId = invite._id CollaboratorsHandler.addUserIdToProject projectId, invite.sendingUserId, user._id, invite.privileges, (err) -> diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index 7fc4722ef2..4c7cc8c76a 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -66,7 +66,7 @@ module.exports = ) webRouter.post( - '/project/:Project_id/invite/:invite_id/accept', + '/project/:Project_id/invite/token/:token/accept', AuthenticationController.requireLogin(), CollaboratorsInviteController.acceptInvite ) diff --git a/services/web/app/views/project/editor/file-tree.jade b/services/web/app/views/project/editor/file-tree.jade index 77cbd10da9..b32bdbed23 100644 --- a/services/web/app/views/project/editor/file-tree.jade +++ b/services/web/app/views/project/editor/file-tree.jade @@ -315,8 +315,12 @@ script(type='text/ng-template', id='newDocModalTemplate') required, ng-model="inputs.name", on-enter="create()", - select-name-on="open" + select-name-on="open", + ng-pattern="validFileRegex", + name="name" ) + .text-danger.row-spaced-small(ng-show="newDocForm.name.$error.pattern") + | #{translate('files_cannot_include_invalid_characters')} .modal-footer button.btn.btn-default( ng-disabled="state.inflight" @@ -341,8 +345,12 @@ script(type='text/ng-template', id='newFolderModalTemplate') required, ng-model="inputs.name", on-enter="create()", - select-name-on="open" + select-name-on="open", + ng-pattern="validFileRegex", + name="name" ) + .text-danger.row-spaced-small(ng-show="newFolderForm.name.$error.pattern") + | #{translate('files_cannot_include_invalid_characters')} .modal-footer button.btn.btn-default( ng-disabled="state.inflight" @@ -414,3 +422,13 @@ script(type='text/ng-template', id='deleteEntityModalTemplate') ) span(ng-hide="state.inflight") #{translate("delete")} span(ng-show="state.inflight") #{translate("deleting")}... + +script(type='text/ng-template', id='invalidFileNameModalTemplate') + .modal-header + h3 #{translate('invalid_file_name')} + .modal-body + p #{translate('files_cannot_include_invalid_characters')} + .modal-footer + button.btn.btn-default( + ng-click="$close()" + ) #{translate('ok')} \ No newline at end of file diff --git a/services/web/app/views/project/invite/show.jade b/services/web/app/views/project/invite/show.jade index 833e75085e..eed30d3d19 100644 --- a/services/web/app/views/project/invite/show.jade +++ b/services/web/app/views/project/invite/show.jade @@ -20,7 +20,7 @@ block content form.form( name="acceptForm", method="POST", - action="/project/#{invite.projectId}/invite/#{invite._id}/accept" + action="/project/#{invite.projectId}/invite/token/#{invite.token}/accept" ) input(name='_csrf', type='hidden', value=csrfToken) input(name='token', type='hidden', value="#{invite.token}") diff --git a/services/web/app/views/project/list/notifications.jade b/services/web/app/views/project/list/notifications.jade index 8dba0852ff..f4be585f7b 100644 --- a/services/web/app/views/project/list/notifications.jade +++ b/services/web/app/views/project/list/notifications.jade @@ -4,14 +4,31 @@ span(ng-controller="NotificationsController").userNotifications ng-cloak ) li.notification_entry( - ng-repeat="unreadNotification in notifications", + ng-repeat="notification in notifications", ) - .row(ng-hide="unreadNotification.hide") + .row(ng-hide="notification.hide") .col-xs-12 - .alert.alert-info + .alert.alert-info(ng-if="notification.templateKey == 'notification_project_invite'", ng-controller="ProjectInviteNotificationController") div.notification_inner - span(ng-bind-html="unreadNotification.html").notification_body + .notification_body(ng-show="!notification.accepted") + | !{translate("notification_project_invite_message")} + a.pull-right.btn.btn-sm.btn-info(href, ng-click="accept()", ng-disabled="notification.inflight") + span(ng-show="!notification.inflight") #{translate("join_project")} + span(ng-show="notification.inflight") + i.fa.fa-fw.fa-spinner.fa-spin + |   + | #{translate("joining")}... + .notification_body(ng-show="notification.accepted") + | !{translate("notification_project_invite_accepted_message")} + a.pull-right.btn.btn-sm.btn-info(href="/project/{{ notification.messageOpts.projectId }}") #{translate("open_project")} span().notification_close - button(ng-click="dismiss(unreadNotification)").close.pull-right + button(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + .alert.alert-info(ng-if="notification.templateKey != 'notification_project_invite'") + div.notification_inner + span(ng-bind-html="notification.html").notification_body + span().notification_close + button(ng-click="dismiss(notification)").close.pull-right span(aria-hidden="true") × span.sr-only #{translate("close")} diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index be82aa72a8..2553bf9c42 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -87,6 +87,8 @@ define [ # End of tracking code. window._ide = ide + + ide.validFileRegex = '^[^\*\/]*$' # Don't allow * and / ide.project_id = $scope.project_id = window.project_id ide.$scope = $scope diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee index a0dcaa1367..5c8b82e5ed 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -44,6 +44,8 @@ define [ App.controller "NewDocModalController", [ "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", ($scope, ide, $modalInstance, $timeout, parent_folder) -> + $scope.validFileRegex = ide.validFileRegex + $scope.inputs = name: "name.tex" $scope.state = @@ -74,6 +76,8 @@ define [ App.controller "NewFolderModalController", [ "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", ($scope, ide, $modalInstance, $timeout, parent_folder) -> + $scope.validFileRegex = ide.validFileRegex + $scope.inputs = name: "name" $scope.state = diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee index f0813cf03b..e6573bbe57 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee @@ -26,9 +26,23 @@ define [ $scope.startRenaming = () -> $scope.entity.renaming = true + invalidModalShowing = false $scope.finishRenaming = () -> - delete $scope.entity.renaming name = $scope.inputs.name + + if !name.match(new RegExp(ide.validFileRegex)) + # Showing the modal blurs the rename box which calls us again + # so track this with the invalidModalShowing flag + return if invalidModalShowing + invalidModalShowing = true + modal = $modal.open( + templateUrl: "invalidFileNameModalTemplate" + ) + modal.result.then () -> + invalidModalShowing = false + return + + delete $scope.entity.renaming if !name? or name.length == 0 $scope.inputs.name = $scope.entity.name return diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee index d0f2f8a3ae..93fec247ba 100644 --- a/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee +++ b/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee @@ -12,7 +12,9 @@ define [ INDICATOR_DELAY2: 250 # time until the indicator starts animating constructor: (@url, @options) -> - # PDFJS.disableFontFace = true # avoids repaints, uses worker more + if navigator.userAgent?.indexOf("Edge/") >= 0 + # Microsoft Edge does not work well with font-face (Sept 2016) + PDFJS.disableFontFace = true if @options.disableAutoFetch PDFJS.disableAutoFetch = true # prevent loading whole file # PDFJS.disableStream diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 48c6440718..6f95d4e38f 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -79,19 +79,20 @@ define [ return member = members.shift() - if !member.type? and member.display in currentMemberEmails + if member.type == "user" + email = member.email + else # Not an auto-complete object, so email == display + email = member.display + email = email.toLowerCase() + + if email in currentMemberEmails # Skip this existing member 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 email in currentInviteEmails and inviteId = _.find(($scope.project.invites || []), (invite) -> invite.email == email)?._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) - else # Not an auto-complete object, so email == display - request = projectInvites.sendInvite(member.display, $scope.inputs.privileges) + else + request = projectInvites.sendInvite(email, $scope.inputs.privileges) request .success (data) -> diff --git a/services/web/public/coffee/main/project-list/notifications-controller.coffee b/services/web/public/coffee/main/project-list/notifications-controller.coffee index 36a725f778..c9f2d0c68b 100644 --- a/services/web/public/coffee/main/project-list/notifications-controller.coffee +++ b/services/web/public/coffee/main/project-list/notifications-controller.coffee @@ -15,3 +15,24 @@ define [ }) .success (data) -> notification.hide = true + + App.controller "ProjectInviteNotificationController", ($scope, $http) -> + # Shortcuts for translation keys + $scope.projectName = $scope.notification.messageOpts.projectName + $scope.userName = $scope.notification.messageOpts.userName + + $scope.accept = () -> + $scope.notification.inflight = true + $http({ + url: "/project/#{$scope.notification.messageOpts.projectId}/invite/token/#{$scope.notification.messageOpts.token}/accept" + method: "POST" + headers: + "X-Csrf-Token": window.csrfToken + "X-Requested-With": "XMLHttpRequest" + }) + .success () -> + $scope.notification.inflight = false + $scope.notification.accepted = true + .error () -> + $scope.notification.inflight = false + $scope.notification.error = true \ 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 96f25906e9..f01e2c7015 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -536,14 +536,12 @@ describe "CollaboratorsInviteController", -> beforeEach -> @req.params = Project_id: @project_id - invite_id: @invite_id = "thuseoautoh" + token: @token = "mock-token" @req.session = user: _id: @current_user_id = "current-user-id" - @req.body = - token: "thsueothaueotauahsuet" @res.render = sinon.stub() @res.redirect = sinon.stub() - @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, null) + @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, null) @callback = sinon.stub() @next = sinon.stub() @@ -557,7 +555,9 @@ describe "CollaboratorsInviteController", -> @res.redirect.calledWith("/project/#{@project_id}").should.equal true it 'should have called acceptInvite', -> - @CollaboratorsInviteHandler.acceptInvite.callCount.should.equal 1 + @CollaboratorsInviteHandler.acceptInvite + .calledWith(@project_id, @token) + .should.equal true it 'should have called emitToRoom', -> @EditorRealTimeController.emitToRoom.callCount.should.equal 1 @@ -567,7 +567,7 @@ describe "CollaboratorsInviteController", -> beforeEach -> @err = new Error('woops') - @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, @err) + @CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, @err) @CollaboratorsInviteController.acceptInvite @req, @res, @next it 'should not redirect to project page', -> diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index d4ae9a4229..ac94fcf10d 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -404,7 +404,7 @@ describe "CollaboratorsInviteHandler", -> @CollaboratorsInviteHandler._tryCancelInviteNotification = sinon.stub().callsArgWith(1, null) @ProjectInvite.remove.callsArgWith(1, null) @call = (callback) => - @CollaboratorsInviteHandler.acceptInvite @projectId, @inviteId, @token, @user, callback + @CollaboratorsInviteHandler.acceptInvite @projectId, @token, @user, callback afterEach -> @_getInviteByToken.restore()