Răsfoiți Sursa

Merge branch 'master' into sk-passport

# Conflicts:
#	app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
Shane Kilkelly 10 ani în urmă
părinte
comite
72ca1d6316

+ 9 - 7
services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee

@@ -112,14 +112,16 @@ module.exports = CollaboratorsInviteController =
 
 
 	acceptInvite: (req, res, next) ->
 	acceptInvite: (req, res, next) ->
 		projectId = req.params.Project_id
 		projectId = req.params.Project_id
-		inviteId = req.params.invite_id
-		{token} = req.body
+		token = req.params.token
 		currentUser = AuthenticationController.getSessionUser(req)
 		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?
 			if err?
-				logger.err {projectId, inviteId}, "error accepting invite by token"
+				logger.err {projectId, token}, "error accepting invite by token"
 				return next(err)
 				return next(err)
 			EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
 			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}"

+ 7 - 5
services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee

@@ -77,10 +77,12 @@ module.exports = CollaboratorsInviteHandler =
 				if err?
 				if err?
 					logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error saving token"
 					logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error saving token"
 					return callback(err)
 					return callback(err)
+				# Send email and notification in background
 				CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
 				CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
 					if err?
 					if err?
 						logger.err {projectId, email}, "error sending messages for invite"
 						logger.err {projectId, email}, "error sending messages for invite"
-					callback(err, invite)
+				callback(null, invite)
+				
 
 
 	revokeInvite: (projectId, inviteId, callback=(err)->) ->
 	revokeInvite: (projectId, inviteId, callback=(err)->) ->
 		logger.log {projectId, inviteId}, "removing invite"
 		logger.log {projectId, inviteId}, "removing invite"
@@ -117,15 +119,15 @@ module.exports = CollaboratorsInviteHandler =
 				return callback(null, null)
 				return callback(null, null)
 			callback(null, invite)
 			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) ->
 		CollaboratorsInviteHandler.getInviteByToken projectId, tokenString, (err, invite) ->
 			if err?
 			if err?
-				logger.err {err, projectId, inviteId}, "error finding invite"
+				logger.err {err, projectId, tokenString}, "error finding invite"
 				return callback(err)
 				return callback(err)
 			if !invite
 			if !invite
 				err = new Errors.NotFoundError("no matching invite found")
 				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)
 				return callback(err)
 			inviteId = invite._id
 			inviteId = invite._id
 			CollaboratorsHandler.addUserIdToProject projectId, invite.sendingUserId, user._id, invite.privileges, (err) ->
 			CollaboratorsHandler.addUserIdToProject projectId, invite.sendingUserId, user._id, invite.privileges, (err) ->

+ 1 - 1
services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee

@@ -66,7 +66,7 @@ module.exports =
 		)
 		)
 
 
 		webRouter.post(
 		webRouter.post(
-			'/project/:Project_id/invite/:invite_id/accept',
+			'/project/:Project_id/invite/token/:token/accept',
 			AuthenticationController.requireLogin(),
 			AuthenticationController.requireLogin(),
 			CollaboratorsInviteController.acceptInvite
 			CollaboratorsInviteController.acceptInvite
 		)
 		)

+ 20 - 2
services/web/app/views/project/editor/file-tree.jade

@@ -315,8 +315,12 @@ script(type='text/ng-template', id='newDocModalTemplate')
 				required,
 				required,
 				ng-model="inputs.name",
 				ng-model="inputs.name",
 				on-enter="create()",
 				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
 	.modal-footer
 		button.btn.btn-default(
 		button.btn.btn-default(
 			ng-disabled="state.inflight"
 			ng-disabled="state.inflight"
@@ -341,8 +345,12 @@ script(type='text/ng-template', id='newFolderModalTemplate')
 				required,
 				required,
 				ng-model="inputs.name",
 				ng-model="inputs.name",
 				on-enter="create()",
 				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
 	.modal-footer
 		button.btn.btn-default(
 		button.btn.btn-default(
 			ng-disabled="state.inflight"
 			ng-disabled="state.inflight"
@@ -414,3 +422,13 @@ script(type='text/ng-template', id='deleteEntityModalTemplate')
 		)
 		)
 			span(ng-hide="state.inflight") #{translate("delete")}
 			span(ng-hide="state.inflight") #{translate("delete")}
 			span(ng-show="state.inflight") #{translate("deleting")}...
 			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')}

+ 1 - 1
services/web/app/views/project/invite/show.jade

@@ -20,7 +20,7 @@ block content
 								form.form(
 								form.form(
 									name="acceptForm",
 									name="acceptForm",
 									method="POST",
 									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='_csrf', type='hidden', value=csrfToken)
 									input(name='token', type='hidden', value="#{invite.token}")
 									input(name='token', type='hidden', value="#{invite.token}")

+ 22 - 5
services/web/app/views/project/list/notifications.jade

@@ -4,14 +4,31 @@ span(ng-controller="NotificationsController").userNotifications
 		ng-cloak
 		ng-cloak
 	)
 	)
 		li.notification_entry(
 		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
 				.col-xs-12
-					.alert.alert-info
+					.alert.alert-info(ng-if="notification.templateKey == 'notification_project_invite'", ng-controller="ProjectInviteNotificationController")
 						div.notification_inner
 						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
 							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(aria-hidden="true") ×
 									span.sr-only #{translate("close")}
 									span.sr-only #{translate("close")}

+ 2 - 0
services/web/public/coffee/ide.coffee

@@ -87,6 +87,8 @@ define [
 		# End of tracking code.
 		# End of tracking code.
 
 
 		window._ide = ide
 		window._ide = ide
+		
+		ide.validFileRegex = '^[^\*\/]*$' # Don't allow * and /
 
 
 		ide.project_id = $scope.project_id = window.project_id
 		ide.project_id = $scope.project_id = window.project_id
 		ide.$scope = $scope
 		ide.$scope = $scope

+ 4 - 0
services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee

@@ -44,6 +44,8 @@ define [
 	App.controller "NewDocModalController", [
 	App.controller "NewDocModalController", [
 		"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
 		"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
 		($scope,   ide,   $modalInstance,   $timeout,   parent_folder) ->
 		($scope,   ide,   $modalInstance,   $timeout,   parent_folder) ->
+			$scope.validFileRegex = ide.validFileRegex
+			
 			$scope.inputs = 
 			$scope.inputs = 
 				name: "name.tex"
 				name: "name.tex"
 			$scope.state =
 			$scope.state =
@@ -74,6 +76,8 @@ define [
 	App.controller "NewFolderModalController", [
 	App.controller "NewFolderModalController", [
 		"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
 		"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
 		($scope,   ide,   $modalInstance,   $timeout,   parent_folder) ->
 		($scope,   ide,   $modalInstance,   $timeout,   parent_folder) ->
+			$scope.validFileRegex = ide.validFileRegex
+
 			$scope.inputs = 
 			$scope.inputs = 
 				name: "name"
 				name: "name"
 			$scope.state =
 			$scope.state =

+ 15 - 1
services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee

@@ -26,9 +26,23 @@ define [
 		$scope.startRenaming = () ->
 		$scope.startRenaming = () ->
 			$scope.entity.renaming = true
 			$scope.entity.renaming = true
 
 
+		invalidModalShowing = false
 		$scope.finishRenaming = () ->
 		$scope.finishRenaming = () ->
-			delete $scope.entity.renaming
 			name = $scope.inputs.name
 			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
 			if !name? or name.length == 0
 				$scope.inputs.name = $scope.entity.name
 				$scope.inputs.name = $scope.entity.name
 				return
 				return

+ 3 - 1
services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee

@@ -12,7 +12,9 @@ define [
 			INDICATOR_DELAY2: 250  # time until the indicator starts animating
 			INDICATOR_DELAY2: 250  # time until the indicator starts animating
 
 
 			constructor: (@url, @options) ->
 			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
 				if @options.disableAutoFetch
 					PDFJS.disableAutoFetch = true # prevent loading whole file
 					PDFJS.disableAutoFetch = true # prevent loading whole file
 				# PDFJS.disableStream
 				# PDFJS.disableStream

+ 10 - 9
services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee

@@ -79,19 +79,20 @@ define [
 						return
 						return
 
 
 					member = members.shift()
 					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
 						# Skip this existing member
 						return addNextMember()
 						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)
 						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
 					request
 						.success (data) ->
 						.success (data) ->

+ 21 - 0
services/web/public/coffee/main/project-list/notifications-controller.coffee

@@ -15,3 +15,24 @@ define [
 			})
 			})
 				.success (data) ->
 				.success (data) ->
 					notification.hide = true
 					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

+ 6 - 6
services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee

@@ -536,14 +536,12 @@ describe "CollaboratorsInviteController", ->
 		beforeEach ->
 		beforeEach ->
 			@req.params =
 			@req.params =
 				Project_id: @project_id
 				Project_id: @project_id
-				invite_id: @invite_id = "thuseoautoh"
+				token: @token = "mock-token"
 			@req.session =
 			@req.session =
 				user: _id: @current_user_id = "current-user-id"
 				user: _id: @current_user_id = "current-user-id"
-			@req.body =
-				token: "thsueothaueotauahsuet"
 			@res.render = sinon.stub()
 			@res.render = sinon.stub()
 			@res.redirect = sinon.stub()
 			@res.redirect = sinon.stub()
-			@CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, null)
+			@CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, null)
 			@callback = sinon.stub()
 			@callback = sinon.stub()
 			@next = sinon.stub()
 			@next = sinon.stub()
 
 
@@ -557,7 +555,9 @@ describe "CollaboratorsInviteController", ->
 				@res.redirect.calledWith("/project/#{@project_id}").should.equal true
 				@res.redirect.calledWith("/project/#{@project_id}").should.equal true
 
 
 			it 'should have called acceptInvite', ->
 			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', ->
 			it 'should have called emitToRoom', ->
 				@EditorRealTimeController.emitToRoom.callCount.should.equal 1
 				@EditorRealTimeController.emitToRoom.callCount.should.equal 1
@@ -567,7 +567,7 @@ describe "CollaboratorsInviteController", ->
 
 
 			beforeEach ->
 			beforeEach ->
 				@err = new Error('woops')
 				@err = new Error('woops')
-				@CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(4, @err)
+				@CollaboratorsInviteHandler.acceptInvite = sinon.stub().callsArgWith(3, @err)
 				@CollaboratorsInviteController.acceptInvite @req, @res, @next
 				@CollaboratorsInviteController.acceptInvite @req, @res, @next
 
 
 			it 'should not redirect to project page', ->
 			it 'should not redirect to project page', ->

+ 1 - 1
services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee

@@ -404,7 +404,7 @@ describe "CollaboratorsInviteHandler", ->
 			@CollaboratorsInviteHandler._tryCancelInviteNotification = sinon.stub().callsArgWith(1, null)
 			@CollaboratorsInviteHandler._tryCancelInviteNotification = sinon.stub().callsArgWith(1, null)
 			@ProjectInvite.remove.callsArgWith(1, null)
 			@ProjectInvite.remove.callsArgWith(1, null)
 			@call = (callback) =>
 			@call = (callback) =>
-				@CollaboratorsInviteHandler.acceptInvite @projectId, @inviteId, @token, @user, callback
+				@CollaboratorsInviteHandler.acceptInvite @projectId, @token, @user, callback
 
 
 		afterEach ->
 		afterEach ->
 			@_getInviteByToken.restore()
 			@_getInviteByToken.restore()