From 5cb85c03321c1f7d694387f7eecabc3c9f6a217d Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 3 May 2018 14:29:03 +0100 Subject: [PATCH 01/57] WIP: Add ProjectFileAgent --- .../LinkedFiles/LinkedFilesController.coffee | 5 +- .../LinkedFiles/ProjectFileAgent.coffee | 70 +++++++++++++++++++ .../coffee/infrastructure/FileWriter.coffee | 14 +++- .../ide/file-tree/FileTreeManager.coffee | 12 ++++ 4 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee index aaf4172cf4..bb5a93efb9 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee @@ -5,7 +5,8 @@ logger = require 'logger-sharelatex' module.exports = LinkedFilesController = { Agents: { - url: require('./UrlAgent') + url: require('./UrlAgent'), + project_file: require('./ProjectFileAgent') } createLinkedFile: (req, res, next) -> @@ -29,4 +30,4 @@ module.exports = LinkedFilesController = { EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error) -> return next(error) if error? res.send(204) # created -} \ No newline at end of file +} diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee new file mode 100644 index 0000000000..116434ef3f --- /dev/null +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -0,0 +1,70 @@ +FileWriter = require('../../infrastructure/FileWriter') +AuthorizationManager = require('../Authorization/AuthorizationManager') +ProjectLocator = require('../Project/ProjectLocator') +DocstoreManager = require('../Docstore/DocstoreManager') +FileStoreHandler = require('../FileStore/FileStoreHandler') +FileWriter = require('../../infrastructure/FileWriter') +_ = require "underscore" +Settings = require 'settings-sharelatex' + + +AccessDeniedError = (message) -> + error = new Error(message) + error.name = 'AccessDenied' + error.__proto__ = AccessDeniedError.prototype + return error +AccessDeniedError.prototype.__proto__ = Error.prototype + +BadEntityTypeError = (message) -> + error = new Error(message) + error.name = 'BadEntityType' + error.__proto__ = BadEntityTypeError.prototype + return error +BadEntityTypeError.prototype.__proto__ = Error.prototype + + +module.exports = ProjectFileAgent = + + sanitizeData: (data) -> + # TODO: + # - Nothing? + return data + + writeIncomingFileToDisk: + (project_id, data, current_user_id, callback = (error, fsPath) ->) -> + callback = _.once(callback) + {source_project_id, source_entity_path} = data + AuthorizationManager.canUserReadProject current_user_id, source_project_id, + null, (err, canRead) -> + return callback(err) if err? + return callback(new AccessDeniedError()) if !canRead + ProjectLocator.findElementByPath { + project_id: source_project_id, + path: source_entity_path + }, (err, entity, type) -> + return callback(err) if err? # also applies when file not found + ProjectFileAgent._writeEntityToDisk source_project_id, entity._id, type, callback + + _writeEntityToDisk: (project_id, entity_id, type, callback=(err, location)->) -> + callback = _.once(callback) + if type == 'doc' + DocstoreManager.getDoc project_id, entity_id, (err, lines) -> + return callback(err) if err? + FileWriter.writeLinesToDisk entity_id, lines, callback + else if type == 'file' + FileStoreHandler.getFileStream project_id, entity_id, (err, fileStream) -> + return callback(err) if err? + FileWriter.writeStreamToDisk entity_id, fileStream, callback + else + callback(new BadEntityTypeError()) + + handleError: (error, req, res, next) -> + if error instanceof AccessDeniedError + res.status(403).send("You do not have access to this project") + else if error instanceof FileNotFoundError + res.status(404).send("The file does not exist") + else if error instanceof BadEntityTypeError + res.status(404).send("The file is the wrong type") # TODO: better error message + else + next(error) + next() diff --git a/services/web/app/coffee/infrastructure/FileWriter.coffee b/services/web/app/coffee/infrastructure/FileWriter.coffee index dedeed9bad..21353e7a36 100644 --- a/services/web/app/coffee/infrastructure/FileWriter.coffee +++ b/services/web/app/coffee/infrastructure/FileWriter.coffee @@ -6,6 +6,18 @@ Settings = require 'settings-sharelatex' request = require 'request' module.exports = FileWriter = + + writeLinesToDisk: (identifier, lines, callback = (error, fsPath)->) -> + callback = _.once(callback) + fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}" + fs.mkdir Settings.path.dumpFolder, (error) -> + if error? and error.code != 'EEXIST' + # Ignore error about already existing + return callback(error) + fs.writeFile fsPath, lines.join('\n'), (error) -> + return callback(error) if error? + callback(null, fsPath) + writeStreamToDisk: (identifier, stream, callback = (error, fsPath) ->) -> callback = _.once(callback) fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}" @@ -39,4 +51,4 @@ module.exports = FileWriter = else err = new Error("bad response from url: #{response.statusCode}") logger.err {err, identifier, url}, err.message - callback(err) \ No newline at end of file + callback(err) diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee index d7a428ec80..f342843a54 100644 --- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee +++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee @@ -25,6 +25,18 @@ define [ $(document).on "click", => @clearMultiSelectedEntities() @$scope.$digest() + window.doLinkedFileImportFromProject = (project, path, name) => + parent_folder = @getCurrentFolder() + @ide.$http.post "/project/#{@ide.project_id}/linked_file", { + name: name, + parent_folder_id: parent_folder?.id + provider: 'project_file', + data: { + source_project_id: project, + source_entity_path: path + }, + _csrf: window.csrfToken + } _bindToSocketEvents: () -> @ide.socket.on "reciveNewDoc", (parent_folder_id, doc) => From 87fb226c3edeccbd05973301cb5b92e3f6149fd8 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 3 May 2018 15:30:44 +0100 Subject: [PATCH 02/57] Fix invocation of `getFileStream` --- .../web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee index 116434ef3f..60f5c394f4 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -52,7 +52,7 @@ module.exports = ProjectFileAgent = return callback(err) if err? FileWriter.writeLinesToDisk entity_id, lines, callback else if type == 'file' - FileStoreHandler.getFileStream project_id, entity_id, (err, fileStream) -> + FileStoreHandler.getFileStream project_id, entity_id, null, (err, fileStream) -> return callback(err) if err? FileWriter.writeStreamToDisk entity_id, fileStream, callback else From 4925bfe5364a02e172b13946af29b9003699e578 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 4 May 2018 09:44:13 +0100 Subject: [PATCH 03/57] Add an endpoint to get users projects as json --- .../Features/Project/ProjectController.coffee | 13 +++++++++++++ services/web/app/coffee/router.coffee | 1 + .../coffee/ide/file-tree/FileTreeManager.coffee | 10 +++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 9312ba0b1b..19374a1975 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -138,6 +138,19 @@ module.exports = ProjectController = return next(err) if err? res.sendStatus 200 + projectsJson: (req, res, next) -> + user_id = AuthenticationController.getLoggedInUserId(req) + + ProjectGetter.findAllUsersProjects user_id, + 'name lastUpdated publicAccesLevel archived owner_ref tokens', (err, projects) -> + return next(err) if err? + projects = ProjectController._buildProjectList(projects) + .filter((p) -> !p.archived) + .filter((p) -> !p.isV1Project) + .map((p) -> {_id: p.id, name: p.name, accessLevel: p.accessLevel}) + + res.json({projects: projects}) + projectListPage: (req, res, next)-> timer = new metrics.Timer("project-list") user_id = AuthenticationController.getLoggedInUserId(req) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 7ec4dafbf4..83fae46131 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -118,6 +118,7 @@ module.exports = class Router webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo privateApiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo + webRouter.get '/user/projects', AuthenticationController.requireLogin(), ProjectController.projectsJson webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee index f342843a54..a5784c57fd 100644 --- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee +++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee @@ -25,7 +25,9 @@ define [ $(document).on "click", => @clearMultiSelectedEntities() @$scope.$digest() - window.doLinkedFileImportFromProject = (project, path, name) => + + # TODO: remove + window._doLinkedFileImportFromProject = (project, path, name) => parent_folder = @getCurrentFolder() @ide.$http.post "/project/#{@ide.project_id}/linked_file", { name: name, @@ -38,6 +40,12 @@ define [ _csrf: window.csrfToken } + # TODO: remove + window._getProjects = () => + @ide.$http.get "/user/projects", { + _csrf: window.csrfToken + } + _bindToSocketEvents: () -> @ide.socket.on "reciveNewDoc", (parent_folder_id, doc) => parent_folder = @findEntityById(parent_folder_id) or @$scope.rootFolder From 3c3ce2010a53e0d98263ff3e084af31bff7822cb Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 4 May 2018 10:45:13 +0100 Subject: [PATCH 04/57] Add endpoint to list entities within a project --- .../Features/Project/ProjectController.coffee | 22 +++++++++++++++++-- services/web/app/coffee/router.coffee | 5 ++++- .../ide/file-tree/FileTreeManager.coffee | 5 +++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 19374a1975..3d1d11da71 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -25,6 +25,7 @@ Sources = require "../Authorization/Sources" TokenAccessHandler = require '../TokenAccess/TokenAccessHandler' CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler' Modules = require '../../infrastructure/Modules' +ProjectEntityHandler = require './ProjectEntityHandler' crypto = require 'crypto' module.exports = ProjectController = @@ -138,9 +139,8 @@ module.exports = ProjectController = return next(err) if err? res.sendStatus 200 - projectsJson: (req, res, next) -> + userProjectsJson: (req, res, next) -> user_id = AuthenticationController.getLoggedInUserId(req) - ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref tokens', (err, projects) -> return next(err) if err? @@ -151,6 +151,24 @@ module.exports = ProjectController = res.json({projects: projects}) + projectEntitiesJson: (req, res, next) -> + user_id = AuthenticationController.getLoggedInUserId(req) + project_id = req.params.Project_id + AuthorizationManager.canUserReadProject user_id, project_id, + null, (err, canRead) -> + return next(err) if err? + return res.status(403) if !canRead + ProjectGetter.getProject project_id, (err, project) -> + return next(err) if err? + ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) -> + return next(err) if err? + entities = docs.concat(files) + .map (e) -> { + path: e.path, + type: if e.doc? then 'doc' else 'file' + } + res.json({entities: entities}) + projectListPage: (req, res, next)-> timer = new metrics.Timer("project-list") user_id = AuthenticationController.getLoggedInUserId(req) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 83fae46131..6d94aa65b1 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -118,7 +118,10 @@ module.exports = class Router webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo privateApiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo - webRouter.get '/user/projects', AuthenticationController.requireLogin(), ProjectController.projectsJson + # TODO: check this is the right router for these routes + webRouter.get '/user/projects', AuthenticationController.requireLogin(), ProjectController.userProjectsJson + webRouter.get '/project/:Project_id/entities', AuthenticationController.requireLogin(), ProjectController.projectEntitiesJson + webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee index a5784c57fd..01fa211b1f 100644 --- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee +++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee @@ -46,6 +46,11 @@ define [ _csrf: window.csrfToken } + window._getProjectEntities = (project_id) => + @ide.$http.get "/project/#{project_id}/entities", { + _csrf: window.csrfToken + } + _bindToSocketEvents: () -> @ide.socket.on "reciveNewDoc", (parent_folder_id, doc) => parent_folder = @findEntityById(parent_folder_id) or @$scope.rootFolder From 08263180fa1c6bf16c2d7a192724841db7d9f608 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 4 May 2018 11:03:54 +0100 Subject: [PATCH 05/57] Add project_id to the entities payload --- .../web/app/coffee/Features/Project/ProjectController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 3d1d11da71..b69c1e412b 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -167,7 +167,7 @@ module.exports = ProjectController = path: e.path, type: if e.doc? then 'doc' else 'file' } - res.json({entities: entities}) + res.json({project_id: project_id, entities: entities}) projectListPage: (req, res, next)-> timer = new metrics.Timer("project-list") From 30beb098ab2a0fc776339d9f8cac8fd6d031712c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 4 May 2018 11:06:59 +0100 Subject: [PATCH 06/57] Sort the project entities by path --- .../web/app/coffee/Features/Project/ProjectController.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index b69c1e412b..5abf03ad00 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -163,6 +163,7 @@ module.exports = ProjectController = ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) -> return next(err) if err? entities = docs.concat(files) + .sort (a, b) -> a.path > b.path # Sort by path ascending .map (e) -> { path: e.path, type: if e.doc? then 'doc' else 'file' From 9c33f3f8bc05409b3942f27087530688fb38af76 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 15 May 2018 16:22:47 +0100 Subject: [PATCH 07/57] WIP: Project Linked File modal --- .../app/views/project/editor/file-tree.pug | 54 ++++++++++ .../ide/file-tree/FileTreeManager.coffee | 8 +- .../controllers/FileTreeController.coffee | 98 +++++++++++++++++++ 3 files changed, 156 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index 55fc660abc..ec26c8d503 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -342,6 +342,60 @@ script(type='text/ng-template', id='newDocModalTemplate') span(ng-show="state.inflight") #{translate("creating")}... +// Project Linked Files Modal +script(type='text/ng-template', id='projectLinkedFileModalTemplate') + .modal-header + h3 New file from Project + + .modal-body + div + div.alert.alert-danger(ng-if="state.error") Error + div + form + .form-controls + label(for="project-select") Select a Project + select.form-control( + name="project-select" + ng-model="data.selectedProject" + ng-disabled="!shouldEnableProjectSelect()" + ) + option(value="") -- + option( + ng-repeat="project in data.projects" + value="{{ project._id }}" + ) {{ project.name }} + + br + .form-controls + label(for="project-entity-select") Select a File + select.form-control( + name="project-entity-select" + ng-model="data.selectedProjectEntity" + ng-disabled="!shouldEnableProjectEntitySelect()" + ) + option(value="") -- + option( + ng-repeat="projectEntity in data.projectEntities" + value="{{ projectEntity.path }}" + ) {{ projectEntity.path }} + br + + .modal-footer + span(ng-show="state.inFlight") + i.fa.fa-spinner.fa-spin + |   + button.btn.btn-default( + ng-disabled="state.inflight" + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-primary( + ng-disabled="!shouldEnableCreateButton()" + ng-click="create()" + ) + span(ng-hide="state.inflight") #{translate("create")} + span(ng-show="state.inflight") #{translate("creating")}... + + script(type='text/ng-template', id='linkedFileModalTemplate') .modal-header h3 New file from URL diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee index 01fa211b1f..8536db63c7 100644 --- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee +++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee @@ -42,14 +42,14 @@ define [ # TODO: remove window._getProjects = () => - @ide.$http.get "/user/projects", { + @ide.$http.get("/user/projects", { _csrf: window.csrfToken - } + }).then (resp) -> console.log(resp.status, resp.data) window._getProjectEntities = (project_id) => - @ide.$http.get "/project/#{project_id}/entities", { + @ide.$http.get("/project/#{project_id}/entities", { _csrf: window.csrfToken - } + }).then (resp) -> console.log(resp.status, resp.data) _bindToSocketEvents: () -> @ide.socket.on "reciveNewDoc", (parent_folder_id, doc) => 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 3d4077b2dd..cf03602d92 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -43,6 +43,19 @@ define [ } ) + $scope.openProjectLinkedFileModal = window.openProjectLinkedFileModal = () -> + unless 'url' in window.data.enabledLinkedFileTypes + console.warn("Project linked files are not enabled") + return + $modal.open( + templateUrl: "projectLinkedFileModalTemplate" + controller: "ProjectLinkedFileModalController" + scope: $scope + resolve: { + parent_folder: () -> ide.fileTreeManager.getCurrentFolder() + } + ) + $scope.orderByFoldersFirst = (entity) -> return '0' if entity?.type == "folder" return '1' @@ -201,6 +214,91 @@ define [ $modalInstance.dismiss('cancel') ] + App.controller "ProjectLinkedFileModalController", [ + "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", + ($scope, ide, $modalInstance, $timeout, parent_folder) -> + $scope.data = + projects: null # or [] + selectedProject: null + projectEntities: null # or [] + selectedProjectEntity: null + $scope.state = + inFlight: false + error: false + + $scope.$watch 'data.selectedProject', (newVal, oldVal) -> + return if !newVal + $scope.data.selectedProjectEntity = null + $scope.getProjectEntities($scope.data.selectedProject) + + $scope._reset = () -> + $scope.state.inFlight = false + $scope.state.error = false + + $scope._resetAfterResponse = (opts) -> + isError = !!opts.err + $scope.state.inFlight = false + $scope.state.error = isError + + $scope.shouldEnableProjectSelect = () -> + state = $scope.state + data = $scope.data + return !state.inFlight && data.projects + + $scope.shouldEnableProjectEntitySelect = () -> + state = $scope.state + data = $scope.data + return !state.inFlight && data.projects && data.selectedProject + + $scope.shouldEnableCreateButton = () -> + state = $scope.state + data = $scope.data + return !state.inFlight && + data.projects && + data.selectedProject && + data.projectEntities && + data.selectedProjectEntity + + $scope.getUserProjects = () -> + $scope.state.inFlight = true + ide.$http.get("/user/projects", { + _csrf: window.csrfToken + }) + .then (resp) -> + $scope.data.projectEntities = null + $scope.data.projects = resp.data.projects + $scope._resetAfterResponse(err: false) + .catch (err) -> + $scope._resetAfterResponse(err: true) + + $scope.getProjectEntities = (project_id) => + $scope.state.inFlight = true + ide.$http.get("/project/#{project_id}/entities", { + _csrf: window.csrfToken + }) + .then (resp) -> + if $scope.data.selectedProject == resp.data.project_id + $scope.data.projectEntities = resp.data.entities + $scope._resetAfterResponse(err: false) + .catch (err) -> + $scope._resetAfterResponse(err: true) + + # TODO: remove + window._S = $scope + + $scope.init = () -> + $scope.getUserProjects() + $timeout($scope.init, 100) + + $scope.create = () -> + console.log ">> create" + + $scope.cancel = () -> + $modalInstance.dismiss('cancel') + + ] + + # TODO: rename all this to UrlLinkedFilModalController App.controller "LinkedFileModalController", [ "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", ($scope, ide, $modalInstance, $timeout, parent_folder) -> From 103832af7d0aab175211a3bd03733114c2fa8d78 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 16 May 2018 10:29:51 +0100 Subject: [PATCH 08/57] Functioning project-linked-file importer --- .../app/views/project/editor/file-tree.pug | 15 ++++++++-- .../controllers/FileTreeController.coffee | 29 ++++++++++++++----- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index ec26c8d503..41de992d61 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -359,7 +359,7 @@ script(type='text/ng-template', id='projectLinkedFileModalTemplate') ng-model="data.selectedProject" ng-disabled="!shouldEnableProjectSelect()" ) - option(value="") -- + option(value="") No Project Selected option( ng-repeat="project in data.projects" value="{{ project._id }}" @@ -373,13 +373,24 @@ script(type='text/ng-template', id='projectLinkedFileModalTemplate') ng-model="data.selectedProjectEntity" ng-disabled="!shouldEnableProjectEntitySelect()" ) - option(value="") -- + option(value="") No File Selected option( ng-repeat="projectEntity in data.projectEntities" value="{{ projectEntity.path }}" ) {{ projectEntity.path }} br + .form-controls + label(for="name") File Name In This Project + input.form-control( + type="text" + placeholder="example.tex" + required + ng-model="data.name" + name="name" + ) + br + .modal-footer span(ng-show="state.inFlight") i.fa.fa-spinner.fa-spin 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 cf03602d92..0d8c43c7f3 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -222,6 +222,7 @@ define [ selectedProject: null projectEntities: null # or [] selectedProjectEntity: null + name: null $scope.state = inFlight: false error: false @@ -231,12 +232,8 @@ define [ $scope.data.selectedProjectEntity = null $scope.getProjectEntities($scope.data.selectedProject) - $scope._reset = () -> - $scope.state.inFlight = false - $scope.state.error = false - $scope._resetAfterResponse = (opts) -> - isError = !!opts.err + isError = opts.err == true $scope.state.inFlight = false $scope.state.error = isError @@ -257,7 +254,8 @@ define [ data.projects && data.selectedProject && data.projectEntities && - data.selectedProjectEntity + data.selectedProjectEntity && + data.name $scope.getUserProjects = () -> $scope.state.inFlight = true @@ -266,7 +264,8 @@ define [ }) .then (resp) -> $scope.data.projectEntities = null - $scope.data.projects = resp.data.projects + $scope.data.projects = resp.data.projects.filter (p) -> + p._id != ide.project_id $scope._resetAfterResponse(err: false) .catch (err) -> $scope._resetAfterResponse(err: true) @@ -291,7 +290,21 @@ define [ $timeout($scope.init, 100) $scope.create = () -> - console.log ">> create" + project = $scope.data.selectedProject + path = $scope.data.selectedProjectEntity + name = $scope.data.name + $scope.state.inFlight = true + ide.fileTreeManager + .createLinkedFile(name, parent_folder, 'project_file', { + source_project_id: project, + source_entity_path: path + }) + .then () -> + $scope._resetAfterResponse(err: false) + $modalInstance.close() + .catch (response)-> + { data } = response + $scope._resetAfterResponse(err: true) $scope.cancel = () -> $modalInstance.dismiss('cancel') From f2702c7b0a9567eda660a5940b4c12d49550ae4d Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 16 May 2018 10:41:21 +0100 Subject: [PATCH 09/57] Show the linked-files UI for project-linked-files --- .../web/app/views/project/editor/binary-file.pug | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/binary-file.pug b/services/web/app/views/project/editor/binary-file.pug index e5690c1874..d7a3ce14f6 100644 --- a/services/web/app/views/project/editor/binary-file.pug +++ b/services/web/app/views/project/editor/binary-file.pug @@ -47,7 +47,18 @@ div.binary-file.full-size( | | at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }} - span(ng-if="openFile.linkedFileData.provider == 'url'") + div(ng-if="openFile.linkedFileData.provider == 'project_file'") + p + i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon + | Imported from + | + // TODO: Show the project name instead + a(ng-href='/project/{{openFile.linkedFileData.source_project_id}}') + | {{ displayUrl(openFile.linkedFileData.source_project_id) }} + | + | at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }} + + span(ng-if="openFile.linkedFileData.provider == 'url' || openFile.linkedFileData.provider == 'project_file'") button.btn.btn-success( href, ng-click="refreshFile(openFile)", ng-disabled="refreshing" From 74d8e67a052ebac1ae4be65af66368907f62cc69 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 16 May 2018 10:58:56 +0100 Subject: [PATCH 10/57] Remove leading slash from path names, for display --- services/web/app/views/project/editor/file-tree.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index 41de992d61..d862cd3b54 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -377,7 +377,7 @@ script(type='text/ng-template', id='projectLinkedFileModalTemplate') option( ng-repeat="projectEntity in data.projectEntities" value="{{ projectEntity.path }}" - ) {{ projectEntity.path }} + ) {{ projectEntity.path.slice(1) }} br .form-controls From e3bc6cac9e9bd6dd7abb058ac5951c161e8a3140 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 16 May 2018 11:12:00 +0100 Subject: [PATCH 11/57] Auto-set filename based on selected file --- .../ide/file-tree/controllers/FileTreeController.coffee | 7 +++++++ 1 file changed, 7 insertions(+) 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 0d8c43c7f3..4f816efe2a 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -232,6 +232,13 @@ define [ $scope.data.selectedProjectEntity = null $scope.getProjectEntities($scope.data.selectedProject) + # auto-set filename based on selected file + $scope.$watch 'data.selectedProjectEntity', (newVal, oldVal) -> + return if !newVal + fileName = newVal.split('/').reverse()[0] + if fileName + $scope.data.name = fileName + $scope._resetAfterResponse = (opts) -> isError = opts.err == true $scope.state.inFlight = false From 7292602167669af4c693dbe14486f5c6b71366cd Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 16 May 2018 11:49:10 +0100 Subject: [PATCH 12/57] More fine-grained loading spinners --- .../app/views/project/editor/file-tree.pug | 14 +++++++++---- .../controllers/FileTreeController.coffee | 21 ++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index d862cd3b54..d1b037925a 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -349,17 +349,20 @@ script(type='text/ng-template', id='projectLinkedFileModalTemplate') .modal-body div - div.alert.alert-danger(ng-if="state.error") Error + div.alert.alert-danger(ng-if="state.error") Error, something went wrong! div form .form-controls label(for="project-select") Select a Project + span(ng-show="state.inFlight.projects") + |   + i.fa.fa-spinner.fa-spin select.form-control( name="project-select" ng-model="data.selectedProject" ng-disabled="!shouldEnableProjectSelect()" ) - option(value="") No Project Selected + option(value="") - No Project Selected option( ng-repeat="project in data.projects" value="{{ project._id }}" @@ -368,12 +371,15 @@ script(type='text/ng-template', id='projectLinkedFileModalTemplate') br .form-controls label(for="project-entity-select") Select a File + span(ng-show="state.inFlight.entities") + |   + i.fa.fa-spinner.fa-spin select.form-control( name="project-entity-select" ng-model="data.selectedProjectEntity" ng-disabled="!shouldEnableProjectEntitySelect()" ) - option(value="") No File Selected + option(value="") - No File Selected option( ng-repeat="projectEntity in data.projectEntities" value="{{ projectEntity.path }}" @@ -392,7 +398,7 @@ script(type='text/ng-template', id='projectLinkedFileModalTemplate') br .modal-footer - span(ng-show="state.inFlight") + span(ng-show="state.inFlight.create") i.fa.fa-spinner.fa-spin |   button.btn.btn-default( 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 4f816efe2a..42f676b0ae 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -224,7 +224,10 @@ define [ selectedProjectEntity: null name: null $scope.state = - inFlight: false + inFlight: + projects: false + entities: false + create: false error: false $scope.$watch 'data.selectedProject', (newVal, oldVal) -> @@ -241,23 +244,25 @@ define [ $scope._resetAfterResponse = (opts) -> isError = opts.err == true - $scope.state.inFlight = false + inFlight = $scope.state.inFlight + inFlight.projects = inFlight.entities = inFlight.create = false $scope.state.error = isError $scope.shouldEnableProjectSelect = () -> state = $scope.state data = $scope.data - return !state.inFlight && data.projects + return !state.inFlight.projects && data.projects $scope.shouldEnableProjectEntitySelect = () -> state = $scope.state data = $scope.data - return !state.inFlight && data.projects && data.selectedProject + return !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProject $scope.shouldEnableCreateButton = () -> state = $scope.state data = $scope.data - return !state.inFlight && + return !state.inFlight.projects && + !state.inFlight.entities && data.projects && data.selectedProject && data.projectEntities && @@ -265,7 +270,7 @@ define [ data.name $scope.getUserProjects = () -> - $scope.state.inFlight = true + $scope.state.inFlight.projects = true ide.$http.get("/user/projects", { _csrf: window.csrfToken }) @@ -278,7 +283,7 @@ define [ $scope._resetAfterResponse(err: true) $scope.getProjectEntities = (project_id) => - $scope.state.inFlight = true + $scope.state.inFlight.entities = true ide.$http.get("/project/#{project_id}/entities", { _csrf: window.csrfToken }) @@ -300,7 +305,7 @@ define [ project = $scope.data.selectedProject path = $scope.data.selectedProjectEntity name = $scope.data.name - $scope.state.inFlight = true + $scope.state.inFlight.create = true ide.fileTreeManager .createLinkedFile(name, parent_folder, 'project_file', { source_project_id: project, From f533674dbd6bc719479d3f1393034f552ed96d7d Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 16 May 2018 13:26:59 +0100 Subject: [PATCH 13/57] Clean up --- .../app/views/project/editor/binary-file.pug | 2 +- .../app/views/project/editor/file-tree.pug | 2 +- .../controllers/FileTreeController.coffee | 43 ++++++++++--------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/services/web/app/views/project/editor/binary-file.pug b/services/web/app/views/project/editor/binary-file.pug index d7a3ce14f6..eeb633a318 100644 --- a/services/web/app/views/project/editor/binary-file.pug +++ b/services/web/app/views/project/editor/binary-file.pug @@ -52,9 +52,9 @@ div.binary-file.full-size( i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon | Imported from | - // TODO: Show the project name instead a(ng-href='/project/{{openFile.linkedFileData.source_project_id}}') | {{ displayUrl(openFile.linkedFileData.source_project_id) }} + |  / {{ openFile.linkedFileData.source_entity_path.slice(1) }}, | | at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }} diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index d1b037925a..67a3a18f04 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -359,7 +359,7 @@ script(type='text/ng-template', id='projectLinkedFileModalTemplate') i.fa.fa-spinner.fa-spin select.form-control( name="project-select" - ng-model="data.selectedProject" + ng-model="data.selectedProjectId" ng-disabled="!shouldEnableProjectSelect()" ) option(value="") - No Project Selected 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 42f676b0ae..33a4bf495e 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -219,7 +219,7 @@ define [ ($scope, ide, $modalInstance, $timeout, parent_folder) -> $scope.data = projects: null # or [] - selectedProject: null + selectedProjectId: null projectEntities: null # or [] selectedProjectEntity: null name: null @@ -230,10 +230,10 @@ define [ create: false error: false - $scope.$watch 'data.selectedProject', (newVal, oldVal) -> + $scope.$watch 'data.selectedProjectId', (newVal, oldVal) -> return if !newVal $scope.data.selectedProjectEntity = null - $scope.getProjectEntities($scope.data.selectedProject) + $scope.getProjectEntities($scope.data.selectedProjectId) # auto-set filename based on selected file $scope.$watch 'data.selectedProjectEntity', (newVal, oldVal) -> @@ -242,7 +242,10 @@ define [ if fileName $scope.data.name = fileName - $scope._resetAfterResponse = (opts) -> + $scope._setInFlight = (type) -> + $scope.state.inFlight[type] = true + + $scope._reset = (opts) -> isError = opts.err == true inFlight = $scope.state.inFlight inFlight.projects = inFlight.entities = inFlight.create = false @@ -256,7 +259,7 @@ define [ $scope.shouldEnableProjectEntitySelect = () -> state = $scope.state data = $scope.data - return !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProject + return !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProjectId $scope.shouldEnableCreateButton = () -> state = $scope.state @@ -264,13 +267,13 @@ define [ return !state.inFlight.projects && !state.inFlight.entities && data.projects && - data.selectedProject && + data.selectedProjectId && data.projectEntities && data.selectedProjectEntity && data.name $scope.getUserProjects = () -> - $scope.state.inFlight.projects = true + $scope._setInFlight('projects') ide.$http.get("/user/projects", { _csrf: window.csrfToken }) @@ -278,45 +281,45 @@ define [ $scope.data.projectEntities = null $scope.data.projects = resp.data.projects.filter (p) -> p._id != ide.project_id - $scope._resetAfterResponse(err: false) + $scope._reset(err: false) .catch (err) -> - $scope._resetAfterResponse(err: true) + $scope._reset(err: true) $scope.getProjectEntities = (project_id) => - $scope.state.inFlight.entities = true + $scope._setInFlight('entities') ide.$http.get("/project/#{project_id}/entities", { _csrf: window.csrfToken }) .then (resp) -> - if $scope.data.selectedProject == resp.data.project_id + if $scope.data.selectedProjectId == resp.data.project_id $scope.data.projectEntities = resp.data.entities - $scope._resetAfterResponse(err: false) + $scope._reset(err: false) .catch (err) -> - $scope._resetAfterResponse(err: true) - - # TODO: remove - window._S = $scope + $scope._reset(err: true) $scope.init = () -> $scope.getUserProjects() $timeout($scope.init, 100) $scope.create = () -> - project = $scope.data.selectedProject + project = $scope.data.selectedProjectId path = $scope.data.selectedProjectEntity name = $scope.data.name - $scope.state.inFlight.create = true + if !name || !path || !project + $scope._reset(err: true) + return + $scope._setInFlight('create') ide.fileTreeManager .createLinkedFile(name, parent_folder, 'project_file', { source_project_id: project, source_entity_path: path }) .then () -> - $scope._resetAfterResponse(err: false) + $scope._reset(err: false) $modalInstance.close() .catch (response)-> { data } = response - $scope._resetAfterResponse(err: true) + $scope._reset(err: true) $scope.cancel = () -> $modalInstance.dismiss('cancel') From 9624e2a290390f35a746680588a3c118b7924cc1 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 16 May 2018 13:44:21 +0100 Subject: [PATCH 14/57] Record the source project display name, to render with the file --- services/web/app/views/project/editor/binary-file.pug | 6 +++--- .../file-tree/controllers/FileTreeController.coffee | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/services/web/app/views/project/editor/binary-file.pug b/services/web/app/views/project/editor/binary-file.pug index eeb633a318..921ed5571d 100644 --- a/services/web/app/views/project/editor/binary-file.pug +++ b/services/web/app/views/project/editor/binary-file.pug @@ -52,9 +52,9 @@ div.binary-file.full-size( i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon | Imported from | - a(ng-href='/project/{{openFile.linkedFileData.source_project_id}}') - | {{ displayUrl(openFile.linkedFileData.source_project_id) }} - |  / {{ openFile.linkedFileData.source_entity_path.slice(1) }}, + a(ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank") + | {{ openFile.linkedFileData.source_project_display_name }} + |  {{ openFile.linkedFileData.source_entity_path.slice(1) }}, | | at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }} 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 33a4bf495e..bf5a6af511 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -302,17 +302,19 @@ define [ $timeout($scope.init, 100) $scope.create = () -> - project = $scope.data.selectedProjectId + projectId = $scope.data.selectedProjectId + projectDisplayName = _.find($scope.data.projects, (p) -> p._id == projectId).name path = $scope.data.selectedProjectEntity name = $scope.data.name - if !name || !path || !project + if !name || !path || !projectId || !projectDisplayName $scope._reset(err: true) return $scope._setInFlight('create') ide.fileTreeManager .createLinkedFile(name, parent_folder, 'project_file', { - source_project_id: project, - source_entity_path: path + source_project_id: projectId, + source_entity_path: path, + source_project_display_name: projectDisplayName }) .then () -> $scope._reset(err: false) From c626446aad2ad2ee311354d19543cd28f5729bcf Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 16 May 2018 13:52:54 +0100 Subject: [PATCH 15/57] Tidy up the project/file display in project-linked-file --- services/web/app/views/project/editor/binary-file.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/binary-file.pug b/services/web/app/views/project/editor/binary-file.pug index 921ed5571d..5171483d24 100644 --- a/services/web/app/views/project/editor/binary-file.pug +++ b/services/web/app/views/project/editor/binary-file.pug @@ -54,7 +54,7 @@ div.binary-file.full-size( | a(ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank") | {{ openFile.linkedFileData.source_project_display_name }} - |  {{ openFile.linkedFileData.source_entity_path.slice(1) }}, + | /{{ openFile.linkedFileData.source_entity_path.slice(1) }}, | | at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }} From 2345b77ea7cbddc81e91a7213857c62693fc868c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 17 May 2018 10:51:58 +0100 Subject: [PATCH 16/57] Validate project-linked-file data before doing import --- .../LinkedFiles/ProjectFileAgent.coffee | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee index 60f5c394f4..f71889fd85 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -15,6 +15,7 @@ AccessDeniedError = (message) -> return error AccessDeniedError.prototype.__proto__ = Error.prototype + BadEntityTypeError = (message) -> error = new Error(message) error.name = 'BadEntityType' @@ -23,16 +24,31 @@ BadEntityTypeError = (message) -> BadEntityTypeError.prototype.__proto__ = Error.prototype +BadDataError = (message) -> + error = new Error(message) + error.name = 'BadData' + error.__proto__ = BadDataError.prototype + return error +BadDataError.prototype.__proto__ = Error.prototype + + module.exports = ProjectFileAgent = sanitizeData: (data) -> - # TODO: - # - Nothing? return data + _validate: (data) -> + return ( + !!data.source_project_id && + !!data.source_entity_path && + !!data.source_project_display_name + ) + writeIncomingFileToDisk: (project_id, data, current_user_id, callback = (error, fsPath) ->) -> callback = _.once(callback) + if !ProjectFileAgent._validate(data) + return callback(new BadDataError()) {source_project_id, source_entity_path} = data AuthorizationManager.canUserReadProject current_user_id, source_project_id, null, (err, canRead) -> @@ -61,10 +77,10 @@ module.exports = ProjectFileAgent = handleError: (error, req, res, next) -> if error instanceof AccessDeniedError res.status(403).send("You do not have access to this project") - else if error instanceof FileNotFoundError - res.status(404).send("The file does not exist") + else if error instanceof BadDataError + res.status(400).send("The submitted data is not valid") else if error instanceof BadEntityTypeError - res.status(404).send("The file is the wrong type") # TODO: better error message + res.status(404).send("The file is the wrong type") else next(error) next() From 94a599d5303994f5399d5588f8d2d35b7a40e15f Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 18 May 2018 10:35:02 +0100 Subject: [PATCH 17/57] Fix the reloading of file view after refreshing linked file --- .../LinkedFiles/LinkedFilesController.coffee | 4 ++-- .../ide/binary-files/BinaryFilesManager.coffee | 14 ++++++++++++++ .../controllers/BinaryFileController.coffee | 11 +++++++++-- .../coffee/ide/file-tree/FileTreeManager.coffee | 9 +++++++++ 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee index bb5a93efb9..c9edeefe32 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee @@ -27,7 +27,7 @@ module.exports = LinkedFilesController = { if error? logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk' return Agent.handleError(error, req, res, next) - EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error) -> + EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error, file) -> return next(error) if error? - res.send(204) # created + res.json(new_file_id: file._id) # created } diff --git a/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee b/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee index ebecf1132e..fc86208717 100644 --- a/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee +++ b/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee @@ -19,3 +19,17 @@ define [ , 0 , this ) + + openFileById: (id) -> + file = @ide.fileTreeManager.selectEntityById(id) + @$scope.ui.view = "file" + @$scope.openFile = null + @$scope.$apply() + window.setTimeout( + () => + @$scope.openFile = file + @$scope.$apply() + @$scope.$digest() + , 0 + , this + ) diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index c14e097842..cdc8cd5d2c 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -49,8 +49,15 @@ define [ $scope.refreshFile = (file) -> $scope.refreshing = true ide.fileTreeManager.refreshLinkedFile(file) - .then () -> - loadTextFileFilePreview() + .then (response) -> + { data } = response + new_file_id = data.new_file_id + $timeout( + () -> + ide.binaryFilesManager.openFileById(new_file_id) + , 1000 + ) + # loadTextFileFilePreview() .finally () -> $scope.refreshing = false diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee index 8536db63c7..2cd6ea4031 100644 --- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee +++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee @@ -112,6 +112,15 @@ define [ entity.selected = false entity.selected = true + selectEntityById: (entity_id) -> + @selected_entity_id = entity_id # For reselecting after a reconnect + selected_entity = null + @ide.fileTreeManager.forEachEntity (entity) -> + if entity.id == entity_id + selected_entity = entity + entity.selected = true + return selected_entity + toggleMultiSelectEntity: (entity) -> entity.multiSelected = !entity.multiSelected @$scope.multiSelectedCount = @multiSelectedCount() From 16106df2f04b1f30110ca0febb8abc5c1287e813 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 18 May 2018 11:05:20 +0100 Subject: [PATCH 18/57] Remove obsolete code --- .../ide/binary-files/controllers/BinaryFileController.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index cdc8cd5d2c..9406c1f900 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -57,7 +57,6 @@ define [ ide.binaryFilesManager.openFileById(new_file_id) , 1000 ) - # loadTextFileFilePreview() .finally () -> $scope.refreshing = false From 4acd55b1c630b902b215aadcce92247a671a056c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 18 May 2018 11:07:59 +0100 Subject: [PATCH 19/57] More tidy unpacking of data --- .../ide/binary-files/controllers/BinaryFileController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index 9406c1f900..9a89a87b6f 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -51,7 +51,7 @@ define [ ide.fileTreeManager.refreshLinkedFile(file) .then (response) -> { data } = response - new_file_id = data.new_file_id + { new_file_id } = data $timeout( () -> ide.binaryFilesManager.openFileById(new_file_id) From 2b99080ed3c3d4a90da937a9a28721e8b099a966 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 18 May 2018 11:25:01 +0100 Subject: [PATCH 20/57] Fix rendering of long previews, stop cutting off last line in short ones --- .../ide/binary-files/controllers/BinaryFileController.coffee | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index 9a89a87b6f..31b0f93f03 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -92,11 +92,9 @@ define [ # show dots when payload is closs to cutoff if data.length >= (TWO_MEGABYTES - 200) $scope.textPreview.shouldShowDots = true - try # remove last partial line data = data.replace(/\n.*$/, '') - finally - $scope.textPreview.data = data + $scope.textPreview.data = data $timeout(setHeight, 0) .catch (error) -> console.error(error) From ee1b32eee162003986fc460992a610f7b74c54e7 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 21 May 2018 10:12:41 +0100 Subject: [PATCH 21/57] Check for case where the source file is not found --- .../LinkedFiles/ProjectFileAgent.coffee | 18 ++++++++++++++++-- .../Features/LinkedFiles/UrlAgent.coffee | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee index f71889fd85..3e434d5361 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -32,6 +32,14 @@ BadDataError = (message) -> BadDataError.prototype.__proto__ = Error.prototype +FileNotFoundError = (message) -> + error = new Error(message) + error.name = 'BadData' + error.__proto__ = FileNotFoundError.prototype + return error +FileNotFoundError.prototype.__proto__ = Error.prototype + + module.exports = ProjectFileAgent = sanitizeData: (data) -> @@ -58,7 +66,11 @@ module.exports = ProjectFileAgent = project_id: source_project_id, path: source_entity_path }, (err, entity, type) -> - return callback(err) if err? # also applies when file not found + # return callback(err) if err? # also applies when file not found + if err? + if err.toString().match(/^not found.*/) + err = new FileNotFoundError() + return callback(err) ProjectFileAgent._writeEntityToDisk source_project_id, entity._id, type, callback _writeEntityToDisk: (project_id, entity_id, type, callback=(err, location)->) -> @@ -80,7 +92,9 @@ module.exports = ProjectFileAgent = else if error instanceof BadDataError res.status(400).send("The submitted data is not valid") else if error instanceof BadEntityTypeError - res.status(404).send("The file is the wrong type") + res.status(400).send("The file is the wrong type") + else if error instanceof FileNotFoundError + res.status(404).send("File not found") else next(error) next() diff --git a/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee index ad96aa628f..567a1b4c39 100644 --- a/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee @@ -65,4 +65,4 @@ module.exports = UrlAgent = { if !Settings.apis?.linkedUrlProxy?.url? throw new Error('no linked url proxy configured') return "#{Settings.apis.linkedUrlProxy.url}?url=#{encodeURIComponent(url)}" -} \ No newline at end of file +} From 7d8c7bebe2b74b30ebabf2332f4bb5f6a0fa5777 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 21 May 2018 10:17:00 +0100 Subject: [PATCH 22/57] Remove commented-out code --- .../web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee index 3e434d5361..514792eb64 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -66,7 +66,6 @@ module.exports = ProjectFileAgent = project_id: source_project_id, path: source_entity_path }, (err, entity, type) -> - # return callback(err) if err? # also applies when file not found if err? if err.toString().match(/^not found.*/) err = new FileNotFoundError() From 1f2ee4e3fcf4a526abee1d2bf7042b8f5429c706 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 21 May 2018 11:02:12 +0100 Subject: [PATCH 23/57] Show error if refresh fails --- services/web/app/views/project/editor/binary-file.pug | 4 ++++ .../ide/binary-files/controllers/BinaryFileController.coffee | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/services/web/app/views/project/editor/binary-file.pug b/services/web/app/views/project/editor/binary-file.pug index 5171483d24..35d75c9fb3 100644 --- a/services/web/app/views/project/editor/binary-file.pug +++ b/services/web/app/views/project/editor/binary-file.pug @@ -74,3 +74,7 @@ div.binary-file.full-size( i.fa.fa-fw.fa-download | | #{translate("download")} + div(ng-if="refreshError") + br + .alert.alert-danger.col-md-6.col-md-offset-3 + | Error: {{ refreshError}} diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index 31b0f93f03..3ccffc9123 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -31,6 +31,7 @@ define [ data: null $scope.refreshing = false + $scope.refreshError = null MAX_URL_LENGTH = 60 FRONT_OF_URL_LENGTH = 35 @@ -48,6 +49,7 @@ define [ $scope.refreshFile = (file) -> $scope.refreshing = true + $scope.refreshError = null ide.fileTreeManager.refreshLinkedFile(file) .then (response) -> { data } = response @@ -57,6 +59,9 @@ define [ ide.binaryFilesManager.openFileById(new_file_id) , 1000 ) + $scope.refreshError = null + .catch (response) -> + $scope.refreshError = response.data .finally () -> $scope.refreshing = false From 73184c063e23a105177af1059209c76e7a12bd0b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 May 2018 11:36:35 +0100 Subject: [PATCH 24/57] Be more specific about the source-file-not-found error case --- .../Features/LinkedFiles/ProjectFileAgent.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee index 514792eb64..908f53a7ea 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -32,12 +32,12 @@ BadDataError = (message) -> BadDataError.prototype.__proto__ = Error.prototype -FileNotFoundError = (message) -> +SourceFileNotFoundError = (message) -> error = new Error(message) error.name = 'BadData' - error.__proto__ = FileNotFoundError.prototype + error.__proto__ = SourceFileNotFoundError.prototype return error -FileNotFoundError.prototype.__proto__ = Error.prototype +SourceFileNotFoundError.prototype.__proto__ = Error.prototype module.exports = ProjectFileAgent = @@ -68,7 +68,7 @@ module.exports = ProjectFileAgent = }, (err, entity, type) -> if err? if err.toString().match(/^not found.*/) - err = new FileNotFoundError() + err = new SourceFileNotFoundError() return callback(err) ProjectFileAgent._writeEntityToDisk source_project_id, entity._id, type, callback @@ -92,8 +92,8 @@ module.exports = ProjectFileAgent = res.status(400).send("The submitted data is not valid") else if error instanceof BadEntityTypeError res.status(400).send("The file is the wrong type") - else if error instanceof FileNotFoundError - res.status(404).send("File not found") + else if error instanceof SourceFileNotFoundError + res.status(404).send("Source file not found") else next(error) next() From e34131ed45bc09894afb00e3aba4cbbceffdc622 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 May 2018 15:01:51 +0100 Subject: [PATCH 25/57] Add acceptance test for project linked files --- services/web/docker-compose.yml | 1 + .../acceptance/coffee/LinkedFilesTests.coffee | 83 ++++++++++++++++++- .../acceptance/coffee/helpers/User.coffee | 12 +++ 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/services/web/docker-compose.yml b/services/web/docker-compose.yml index 5a668bc4a3..410462a745 100644 --- a/services/web/docker-compose.yml +++ b/services/web/docker-compose.yml @@ -17,6 +17,7 @@ services: PROJECT_HISTORY_ENABLED: 'true' ENABLED_LINKED_FILE_TYPES: 'url' LINKED_URL_PROXY: 'http://localhost:6543' + ENABLED_LINKED_FILE_TYPES: 'url,project_file' depends_on: - redis - mongo diff --git a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee index 9ca7ecae42..ad79a4802c 100644 --- a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee +++ b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee @@ -27,6 +27,81 @@ describe "LinkedFiles", -> @owner.login -> mkdirp Settings.path.dumpFolder, done + describe "creating a project linked file", -> + before (done) -> + @source_doc_name = 'test.txt' + async.series [ + (cb) => + @owner.createProject 'plf-test-one', {template: 'blank'}, (error, project_id) => + @project_one_id = project_id + cb(error) + (cb) => + @owner.getProject @project_one_id, (error, project) => + @project_one = project + @project_one_root_folder_id = project.rootFolder[0]._id.toString() + cb(error) + (cb) => + @owner.createProject 'plf-test-two', {template: 'blank'}, (error, project_id) => + @project_two_id = project_id + cb(error) + (cb) => + @owner.getProject @project_two_id, (error, project) => + @project_two = project + @project_two_root_folder_id = project.rootFolder[0]._id.toString() + cb(error) + (cb) => + @owner.createDocInProject @project_two_id, + @project_two_root_folder_id, + @source_doc_name, + (error, doc_id) => + @source_doc_id = doc_id + cb(error) + ], done + + it 'should import a file from the source project', (done) -> + @owner.request.post { + url: "/project/#{@project_one_id}/linked_file", + json: + name: 'test-link.txt', + parent_folder_id: @project_one_root_folder_id, + provider: 'project_file', + data: + source_project_id: @project_two_id, + source_entity_path: "/#{@source_doc_name}", + source_project_display_name: "Project Two" + }, (error, response, body) => + new_file_id = body.new_file_id + @existing_file_id = new_file_id + expect(new_file_id).to.exist + @owner.getProject @project_one_id, (error, project) => + return done(error) if error? + firstFile = project.rootFolder[0].fileRefs[0] + expect(firstFile._id.toString()).to.equal(new_file_id.toString()) + expect(firstFile.name).to.equal('test-link.txt') + done() + + it 'should refresh the file', (done) -> + @owner.request.post { + url: "/project/#{@project_one_id}/linked_file", + json: + name: 'test-link.txt', + parent_folder_id: @project_one_root_folder_id, + provider: 'project_file', + data: + source_project_id: @project_two_id, + source_entity_path: "/#{@source_doc_name}", + source_project_display_name: "Project Two" + }, (error, response, body) => + new_file_id = body.new_file_id + expect(new_file_id).to.exist + expect(new_file_id).to.not.equal @existing_file_id + @owner.getProject @project_one_id, (error, project) => + return done(error) if error? + firstFile = project.rootFolder[0].fileRefs[0] + expect(firstFile._id.toString()).to.equal(new_file_id.toString()) + expect(firstFile.name).to.equal('test-link.txt') + done() + describe "creating a URL based linked file", -> before (done) -> @owner.createProject "url-linked-files-project", {template: "blank"}, (error, project_id) => @@ -50,7 +125,7 @@ describe "LinkedFiles", -> name: 'url-test-file-1' }, (error, response, body) => throw error if error? - expect(response.statusCode).to.equal 204 + expect(response.statusCode).to.equal 200 @owner.getProject @project_id, (error, project) => throw error if error? file = project.rootFolder[0].fileRefs[0] @@ -76,7 +151,7 @@ describe "LinkedFiles", -> name: 'url-test-file-2' }, (error, response, body) => throw error if error? - expect(response.statusCode).to.equal 204 + expect(response.statusCode).to.equal 200 @owner.request.post { url: "/project/#{@project_id}/linked_file", json: @@ -88,7 +163,7 @@ describe "LinkedFiles", -> name: 'url-test-file-2' }, (error, response, body) => throw error if error? - expect(response.statusCode).to.equal 204 + expect(response.statusCode).to.equal 200 @owner.getProject @project_id, (error, project) => throw error if error? file = project.rootFolder[0].fileRefs[1] @@ -168,7 +243,7 @@ describe "LinkedFiles", -> name: 'url-test-file-6' }, (error, response, body) => throw error if error? - expect(response.statusCode).to.equal 204 + expect(response.statusCode).to.equal 200 @owner.getProject @project_id, (error, project) => throw error if error? file = _.find project.rootFolder[0].fileRefs, (file) -> diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index 7d8e9086d4..a0c595a694 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -137,6 +137,18 @@ class User return callback(err) callback(null) + createDocInProject: (project_id, parent_folder_id, name, callback=(error, doc_id)->) -> + @getCsrfToken (error) => + return callback(error) if error? + @request.post { + url: "/project/#{project_id}/doc", + json: { + name: name, + parent_folder_id: parent_folder_id + } + }, (error, response, body) => + callback(null, body._id) + addUserToProject: (project_id, user, privileges, callback = (error, user) ->) -> if privileges == 'readAndWrite' updateOp = {$addToSet: {collaberator_refs: user._id.toString()}} From bc5769cd736d7e00f061c8014a39f714de3c942c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 May 2018 15:56:01 +0100 Subject: [PATCH 26/57] Stub out the ProjectEntityHandler in ProjectController tests --- .../web/test/unit/coffee/Project/ProjectControllerTests.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee index ccae21ba70..3b583daa6c 100644 --- a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee @@ -67,6 +67,7 @@ describe "ProjectController", -> protectTokens: sinon.stub() @CollaboratorsHandler = userIsTokenMember: sinon.stub().callsArgWith(2, null, false) + @ProjectEntityHandler = {} @Modules = hooks: fire: sinon.stub() @@ -98,6 +99,7 @@ describe "ProjectController", -> "../TokenAccess/TokenAccessHandler": @TokenAccessHandler "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler "../../infrastructure/Modules": @Modules + "./ProjectEntityHandler": @ProjectEntityHandler @projectName = "£12321jkj9ujkljds" @req = From b1c1cdecef10a71a2cda905353891c7431b42b09 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 May 2018 16:17:59 +0100 Subject: [PATCH 27/57] Add unit test for ProjectController.userProjectsJson --- .../Project/ProjectControllerTests.coffee | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee index 3b583daa6c..223d2beb6b 100644 --- a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee @@ -521,7 +521,32 @@ describe "ProjectController", -> @ProjectUpdateHandler.markAsOpened.calledWith(@project_id).should.equal true done() @ProjectController.loadEditor @req, @res - + + describe 'userProjectsJson', -> + beforeEach (done) -> + projects = [ + {archived: true, id: 'a', name: 'A', accessLevel: 'a', somethingElse: 1} + {archived: false, id: 'b', name: 'B', accessLevel: 'b', somethingElse: 1} + {archived: false, id: 'c', name: 'C', accessLevel: 'c', somethingElse: 1} + {archived: false, id: 'd', name: 'D', accessLevel: 'd', somethingElse: 1} + ] + @ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, []) + @ProjectController._buildProjectList = sinon.stub().returns(projects) + @AuthenticationController.getLoggedInUserId = sinon.stub().returns 'abc' + done() + + it 'should produce a list of projects', (done) -> + @res.json = (data) => + expect(data).to.deep.equal { + projects: [ + {_id: 'b', name: 'B', accessLevel: 'b'}, + {_id: 'c', name: 'C', accessLevel: 'c'}, + {_id: 'd', name: 'D', accessLevel: 'd'} + ] + } + done() + @ProjectController.userProjectsJson @req, @res, @next + describe '_isInPercentageRollout', -> before -> @ids = [ From 4daf062be9fd2b86c571869b5b6b83cbd7c8c4e1 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 22 May 2018 16:40:39 +0100 Subject: [PATCH 28/57] Add unit test for ProjectController.projectEntitiesJson --- .../Features/Project/ProjectController.coffee | 25 +++++----- .../Project/ProjectControllerTests.coffee | 48 +++++++++++++++++++ 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 3a1219afd5..10e32d1746 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -154,21 +154,20 @@ module.exports = ProjectController = projectEntitiesJson: (req, res, next) -> user_id = AuthenticationController.getLoggedInUserId(req) project_id = req.params.Project_id - AuthorizationManager.canUserReadProject user_id, project_id, - null, (err, canRead) -> + AuthorizationManager.canUserReadProject user_id, project_id, null, (err, canRead) -> + return next(err) if err? + return res.sendStatus(403) if !canRead + ProjectGetter.getProject project_id, (err, project) -> return next(err) if err? - return res.status(403) if !canRead - ProjectGetter.getProject project_id, (err, project) -> + ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) -> return next(err) if err? - ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) -> - return next(err) if err? - entities = docs.concat(files) - .sort (a, b) -> a.path > b.path # Sort by path ascending - .map (e) -> { - path: e.path, - type: if e.doc? then 'doc' else 'file' - } - res.json({project_id: project_id, entities: entities}) + entities = docs.concat(files) + .sort (a, b) -> a.path > b.path # Sort by path ascending + .map (e) -> { + path: e.path, + type: if e.doc? then 'doc' else 'file' + } + res.json({project_id: project_id, entities: entities}) projectListPage: (req, res, next)-> timer = new metrics.Timer("project-list") diff --git a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee index 223d2beb6b..6483319bab 100644 --- a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee @@ -547,6 +547,54 @@ describe "ProjectController", -> done() @ProjectController.userProjectsJson @req, @res, @next + describe 'projectEntitiesJson', -> + beforeEach () -> + @AuthenticationController.getLoggedInUserId = sinon.stub().returns 'abc' + @req.params = {Project_id: 'abcd'} + @project = { _id: 'abcd' } + @docs = [ + {path: '/things/b.txt', doc: true}, + {path: '/main.tex', doc: true} + ] + @files = [ + {path: '/things/a.txt'} + ] + @ProjectGetter.getProject = sinon.stub().callsArgWith(1, null, @project) + @ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub().callsArgWith(1, null, @docs, @files) + + describe 'when the user can access the project', -> + beforeEach () -> + @AuthorizationManager.canUserReadProject = sinon.stub().callsArgWith(3, null, true) + + it 'should produce a list of entities', (done) -> + @res.json = (data) => + expect(data).to.deep.equal { + project_id: 'abcd', + entities: [ + {path: '/main.tex', type: 'doc'}, + {path: '/things/a.txt', type: 'file'}, + {path: '/things/b.txt', type: 'doc'} + ] + } + expect(@ProjectGetter.getProject.callCount).to.equal 1 + expect(@ProjectEntityHandler.getAllEntitiesFromProject.callCount).to.equal 1 + done() + @ProjectController.projectEntitiesJson @req, @res, @next + + describe 'when the user cannot access the project', -> + beforeEach () -> + @AuthorizationManager.canUserReadProject = sinon.stub().callsArgWith(3, null, false) + + it 'should send a 403 response', (done) -> + @res.json = sinon.stub() + @res.sendStatus = (code) => + expect(code).to.equal 403 + expect(@ProjectGetter.getProject.callCount).to.equal 0 + expect(@ProjectEntityHandler.getAllEntitiesFromProject.callCount).to.equal 0 + expect(@res.json.callCount).to.equal 0 + done() + @ProjectController.projectEntitiesJson @req, @res, @next + describe '_isInPercentageRollout', -> before -> @ids = [ From 6a5af88e12458c57138e2908f6d297e66eb19d20 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 23 May 2018 11:32:00 +0100 Subject: [PATCH 29/57] Remove stray comment --- services/web/app/coffee/router.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index e7c730af2e..66e359e98f 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -119,7 +119,6 @@ module.exports = class Router webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo privateApiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo - # TODO: check this is the right router for these routes webRouter.get '/user/projects', AuthenticationController.requireLogin(), ProjectController.userProjectsJson webRouter.get '/project/:Project_id/entities', AuthenticationController.requireLogin(), ProjectController.projectEntitiesJson From 295425e791d7d7eb26f5a00715b3fc0ac7581f1f Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 23 May 2018 11:34:55 +0100 Subject: [PATCH 30/57] Check that user can read a project on entities-json route --- services/web/app/coffee/router.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 66e359e98f..7b0d11863b 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -120,7 +120,9 @@ module.exports = class Router privateApiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo webRouter.get '/user/projects', AuthenticationController.requireLogin(), ProjectController.userProjectsJson - webRouter.get '/project/:Project_id/entities', AuthenticationController.requireLogin(), ProjectController.projectEntitiesJson + webRouter.get '/project/:Project_id/entities', AuthenticationController.requireLogin(), + AuthorizationMiddlewear.ensureUserCanReadProject + ProjectController.projectEntitiesJson webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject From f5dd94ca1d63218a2d69deedc9f9014d65c1d325 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 23 May 2018 11:46:37 +0100 Subject: [PATCH 31/57] Remove test code from FileTreeManager --- .../ide/file-tree/FileTreeManager.coffee | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee index 2cd6ea4031..dfaef74edb 100644 --- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee +++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee @@ -26,31 +26,6 @@ define [ @clearMultiSelectedEntities() @$scope.$digest() - # TODO: remove - window._doLinkedFileImportFromProject = (project, path, name) => - parent_folder = @getCurrentFolder() - @ide.$http.post "/project/#{@ide.project_id}/linked_file", { - name: name, - parent_folder_id: parent_folder?.id - provider: 'project_file', - data: { - source_project_id: project, - source_entity_path: path - }, - _csrf: window.csrfToken - } - - # TODO: remove - window._getProjects = () => - @ide.$http.get("/user/projects", { - _csrf: window.csrfToken - }).then (resp) -> console.log(resp.status, resp.data) - - window._getProjectEntities = (project_id) => - @ide.$http.get("/project/#{project_id}/entities", { - _csrf: window.csrfToken - }).then (resp) -> console.log(resp.status, resp.data) - _bindToSocketEvents: () -> @ide.socket.on "reciveNewDoc", (parent_folder_id, doc) => parent_folder = @findEntityById(parent_folder_id) or @$scope.rootFolder From 561b62f7db26eb1f804173c8ceadba9f6cdd6f68 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 23 May 2018 12:37:42 +0100 Subject: [PATCH 32/57] Add a hidden 'Test Controls' section to the left-menu. This is to be used for hidden features that are not yet ready to ship, and would otherwise be hidden behind a console command. Append `?tc=true` to the project url to reveal this panel. --- .../Features/Project/ProjectController.coffee | 1 + .../web/app/views/project/editor/left-menu.pug | 18 ++++++++++++++++++ services/web/public/coffee/ide.coffee | 1 + .../controllers/TestControlsController.coffee | 14 ++++++++++++++ .../coffee/ide/test-controls/index.coffee | 3 +++ 5 files changed, 37 insertions(+) create mode 100644 services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee create mode 100644 services/web/public/coffee/ide/test-controls/index.coffee diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 10e32d1746..cafcc8153a 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -344,6 +344,7 @@ module.exports = ProjectController = maxDocLength: Settings.max_doc_length useV2History: !!project.overleaf?.history?.display showRichText: req.query?.rt == 'true' + showTestControls: req.query?.tc == 'true' showPublishModal: req.query?.pm == 'true' timer.done() diff --git a/services/web/app/views/project/editor/left-menu.pug b/services/web/app/views/project/editor/left-menu.pug index ed6ef85b44..d85a45723b 100644 --- a/services/web/app/views/project/editor/left-menu.pug +++ b/services/web/app/views/project/editor/left-menu.pug @@ -62,6 +62,23 @@ aside#left-menu.full-size( != moduleIncludes("editorLeftMenu:editing_services", locals) + if showTestControls + h4 Test Controls + ul.list-unstyled.nav(ng-controller="TestControlsController") + li + a(href="#" ng-click="richText()") + i.fa.fa-exclamation.fa-fw + | Rich Text + li + a(href="#" ng-click="openProjectLinkedFileModal()") + i.fa.fa-exclamation.fa-fw + | Project-Linked-File Modal + li + a(href="#" ng-click="openLinkedFileModal()") + i.fa.fa-exclamation.fa-fw + | URL-Linked-File Modal + + h4(ng-show="!anonymous") #{translate("settings")} form.settings(ng-controller="SettingsController", ng-show="!anonymous") .containter-fluid @@ -179,6 +196,7 @@ aside#left-menu.full-size( option(value="pdfjs") #{translate("built_in")} option(value="native") #{translate("native")} + h4 #{translate("hotkeys")} ul.list-unstyled.nav li(ng-controller="HotkeysController") diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 7c8602eb76..4f2d914135 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -18,6 +18,7 @@ define [ "ide/chat/index" "ide/clone/index" "ide/hotkeys/index" + "ide/test-controls/index" "ide/wordcount/index" "ide/directives/layout" "ide/directives/validFile" diff --git a/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee b/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee new file mode 100644 index 0000000000..65378a2cde --- /dev/null +++ b/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee @@ -0,0 +1,14 @@ +define [ + "base" + "ace/ace" +], (App) -> + App.controller "TestControlsController", ($scope) -> + + $scope.openProjectLinkedFileModal = () -> + window.openProjectLinkedFileModal() + + $scope.openLinkedFileModal = () -> + window.openLinkedFileModal() + + $scope.richText = () -> + window.location.href = window.location.toString() + '&rt=true' diff --git a/services/web/public/coffee/ide/test-controls/index.coffee b/services/web/public/coffee/ide/test-controls/index.coffee new file mode 100644 index 0000000000..d60d9e1a01 --- /dev/null +++ b/services/web/public/coffee/ide/test-controls/index.coffee @@ -0,0 +1,3 @@ +define [ + "ide/test-controls/controllers/TestControlsController" +], () -> From ba9143fc3c0edca104a216cfaeae30139f98eb1a Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 23 May 2018 12:55:49 +0100 Subject: [PATCH 33/57] Show test-controls for admin users by default --- .../web/app/coffee/Features/Project/ProjectController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index cafcc8153a..3a47690b20 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -344,7 +344,7 @@ module.exports = ProjectController = maxDocLength: Settings.max_doc_length useV2History: !!project.overleaf?.history?.display showRichText: req.query?.rt == 'true' - showTestControls: req.query?.tc == 'true' + showTestControls: req.query?.tc == 'true' || user.isAdmin showPublishModal: req.query?.pm == 'true' timer.done() From 78f87c0ecfb2abfc75fceb5d4afa58cdd36450e5 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 23 May 2018 15:02:45 +0100 Subject: [PATCH 34/57] Add acceptance test for the project-list and project-entities endpoints --- .../acceptance/coffee/LinkedFilesTests.coffee | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee index ad79a4802c..34079de8d1 100644 --- a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee +++ b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee @@ -56,8 +56,45 @@ describe "LinkedFiles", -> (error, doc_id) => @source_doc_id = doc_id cb(error) + (cb) => + @owner.createDocInProject @project_two_id, + @project_two_root_folder_id, + 'some-harmless-doc.txt', + (error, doc_id) => + cb(error) ], done + it 'should produce a list of the users projects', (done) -> + @owner.request.get { + url: "/user/projects", + json: true + }, (err, response, body) => + expect(err).to.not.exist + expect(body).to.deep.equal { + projects: [ + { _id: @project_one_id, name: 'plf-test-one', accessLevel: 'owner' }, + { _id: @project_two_id, name: 'plf-test-two', accessLevel: 'owner' } + ] + } + done() + + it 'should produce a list of entities in the project', (done) -> + @owner.request.get { + url: "/project/#{@project_two_id}/entities", + json: true + }, (err, response, body) => + expect(err).to.not.exist + expect(body).to.deep.equal { + project_id: @project_two_id, + entities: [ + { path: '/main.tex', type: 'doc' }, + { path: '/some-harmless-doc.txt', type: 'doc' }, + { path: '/test.txt', type: 'doc' } + ] + } + done() + + it 'should import a file from the source project', (done) -> @owner.request.post { url: "/project/#{@project_one_id}/linked_file", From 3181f624a7d99665739ac4820b3e6201e338557b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 24 May 2018 10:22:17 +0100 Subject: [PATCH 35/57] Remove obsolete auth check --- .../Features/Project/ProjectController.coffee | 21 ++++----- .../Project/ProjectControllerTests.coffee | 46 ++++++------------- 2 files changed, 23 insertions(+), 44 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 10e32d1746..201b9830af 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -154,20 +154,17 @@ module.exports = ProjectController = projectEntitiesJson: (req, res, next) -> user_id = AuthenticationController.getLoggedInUserId(req) project_id = req.params.Project_id - AuthorizationManager.canUserReadProject user_id, project_id, null, (err, canRead) -> + ProjectGetter.getProject project_id, (err, project) -> return next(err) if err? - return res.sendStatus(403) if !canRead - ProjectGetter.getProject project_id, (err, project) -> + ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) -> return next(err) if err? - ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) -> - return next(err) if err? - entities = docs.concat(files) - .sort (a, b) -> a.path > b.path # Sort by path ascending - .map (e) -> { - path: e.path, - type: if e.doc? then 'doc' else 'file' - } - res.json({project_id: project_id, entities: entities}) + entities = docs.concat(files) + .sort (a, b) -> a.path > b.path # Sort by path ascending + .map (e) -> { + path: e.path, + type: if e.doc? then 'doc' else 'file' + } + res.json({project_id: project_id, entities: entities}) projectListPage: (req, res, next)-> timer = new metrics.Timer("project-list") diff --git a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee index 6483319bab..32e1c4953a 100644 --- a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee @@ -562,38 +562,20 @@ describe "ProjectController", -> @ProjectGetter.getProject = sinon.stub().callsArgWith(1, null, @project) @ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub().callsArgWith(1, null, @docs, @files) - describe 'when the user can access the project', -> - beforeEach () -> - @AuthorizationManager.canUserReadProject = sinon.stub().callsArgWith(3, null, true) - - it 'should produce a list of entities', (done) -> - @res.json = (data) => - expect(data).to.deep.equal { - project_id: 'abcd', - entities: [ - {path: '/main.tex', type: 'doc'}, - {path: '/things/a.txt', type: 'file'}, - {path: '/things/b.txt', type: 'doc'} - ] - } - expect(@ProjectGetter.getProject.callCount).to.equal 1 - expect(@ProjectEntityHandler.getAllEntitiesFromProject.callCount).to.equal 1 - done() - @ProjectController.projectEntitiesJson @req, @res, @next - - describe 'when the user cannot access the project', -> - beforeEach () -> - @AuthorizationManager.canUserReadProject = sinon.stub().callsArgWith(3, null, false) - - it 'should send a 403 response', (done) -> - @res.json = sinon.stub() - @res.sendStatus = (code) => - expect(code).to.equal 403 - expect(@ProjectGetter.getProject.callCount).to.equal 0 - expect(@ProjectEntityHandler.getAllEntitiesFromProject.callCount).to.equal 0 - expect(@res.json.callCount).to.equal 0 - done() - @ProjectController.projectEntitiesJson @req, @res, @next + it 'should produce a list of entities', (done) -> + @res.json = (data) => + expect(data).to.deep.equal { + project_id: 'abcd', + entities: [ + {path: '/main.tex', type: 'doc'}, + {path: '/things/a.txt', type: 'file'}, + {path: '/things/b.txt', type: 'doc'} + ] + } + expect(@ProjectGetter.getProject.callCount).to.equal 1 + expect(@ProjectEntityHandler.getAllEntitiesFromProject.callCount).to.equal 1 + done() + @ProjectController.projectEntitiesJson @req, @res, @next describe '_isInPercentageRollout', -> before -> From 1cbc90149212eec437e297456d24c64a1e910421 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 24 May 2018 11:29:37 +0100 Subject: [PATCH 36/57] Add a `checkAuth` function to linked-file agents --- .../LinkedFiles/LinkedFilesController.coffee | 17 +++++----- .../LinkedFiles/ProjectFileAgent.coffee | 31 +++++++++++-------- .../Features/LinkedFiles/UrlAgent.coffee | 3 ++ 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee index c9edeefe32..67acdcfc86 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee @@ -23,11 +23,14 @@ module.exports = LinkedFilesController = { linkedFileData = Agent.sanitizeData(data) linkedFileData.provider = provider - Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) -> - if error? - logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk' - return Agent.handleError(error, req, res, next) - EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error, file) -> - return next(error) if error? - res.json(new_file_id: file._id) # created + Agent.checkAuth project_id, data, user_id, (err, allowed) -> + return next(err) if err? + return ses.sendStatus(403) if !allowed + Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) -> + if error? + logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk' + return Agent.handleError(error, req, res, next) + EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error, file) -> + return next(error) if error? + res.json(new_file_id: file._id) # created } diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee index 908f53a7ea..55f20ed9a7 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -52,25 +52,30 @@ module.exports = ProjectFileAgent = !!data.source_project_display_name ) + checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) -> + callback = _.once(callback) + if !ProjectFileAgent._validate(data) + return callback(new BadDataError()) + {source_project_id, source_entity_path} = data + AuthorizationManager.canUserReadProject current_user_id, source_project_id, null, (err, canRead) -> + return callback(err) if err? + callback(null, canRead) + writeIncomingFileToDisk: (project_id, data, current_user_id, callback = (error, fsPath) ->) -> callback = _.once(callback) if !ProjectFileAgent._validate(data) return callback(new BadDataError()) {source_project_id, source_entity_path} = data - AuthorizationManager.canUserReadProject current_user_id, source_project_id, - null, (err, canRead) -> - return callback(err) if err? - return callback(new AccessDeniedError()) if !canRead - ProjectLocator.findElementByPath { - project_id: source_project_id, - path: source_entity_path - }, (err, entity, type) -> - if err? - if err.toString().match(/^not found.*/) - err = new SourceFileNotFoundError() - return callback(err) - ProjectFileAgent._writeEntityToDisk source_project_id, entity._id, type, callback + ProjectLocator.findElementByPath { + project_id: source_project_id, + path: source_entity_path + }, (err, entity, type) -> + if err? + if err.toString().match(/^not found.*/) + err = new SourceFileNotFoundError() + return callback(err) + ProjectFileAgent._writeEntityToDisk source_project_id, entity._id, type, callback _writeEntityToDisk: (project_id, entity_id, type, callback=(err, location)->) -> callback = _.once(callback) diff --git a/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee index 567a1b4c39..59422c5e67 100644 --- a/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee @@ -27,6 +27,9 @@ module.exports = UrlAgent = { url: @._prependHttpIfNeeded(data.url) } + checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) -> + callback(null, true) + writeIncomingFileToDisk: (project_id, data, current_user_id, callback = (error, fsPath) ->) -> callback = _.once(callback) url = data.url From b5e8ed81b9706e4f63438cdb1e895c0f68ee8f7c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 24 May 2018 11:30:03 +0100 Subject: [PATCH 37/57] Better sanitization and validation for project-linked-file --- .../Features/LinkedFiles/ProjectFileAgent.coffee | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee index 55f20ed9a7..75aadf76fc 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -43,13 +43,18 @@ SourceFileNotFoundError.prototype.__proto__ = Error.prototype module.exports = ProjectFileAgent = sanitizeData: (data) -> - return data + return _.pick( + data, + 'source_project_id', + 'source_entity_path', + 'source_project_display_name' + ) _validate: (data) -> return ( - !!data.source_project_id && - !!data.source_entity_path && - !!data.source_project_display_name + data.source_project_id? && + data.source_entity_path? && + data.source_project_display_name? ) checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) -> From 8766b5d487dfc52c8f6c830c836d6ba2f81ae1bf Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 24 May 2018 11:30:29 +0100 Subject: [PATCH 38/57] DRY up writing to dump-folder in FileWriter --- .../app/coffee/infrastructure/FileWriter.coffee | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/infrastructure/FileWriter.coffee b/services/web/app/coffee/infrastructure/FileWriter.coffee index 21353e7a36..27b1f16921 100644 --- a/services/web/app/coffee/infrastructure/FileWriter.coffee +++ b/services/web/app/coffee/infrastructure/FileWriter.coffee @@ -7,13 +7,18 @@ request = require 'request' module.exports = FileWriter = - writeLinesToDisk: (identifier, lines, callback = (error, fsPath)->) -> - callback = _.once(callback) - fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}" + _ensureDumpFolderExists: (callback=(error)->) -> fs.mkdir Settings.path.dumpFolder, (error) -> if error? and error.code != 'EEXIST' # Ignore error about already existing return callback(error) + callback(null) + + writeLinesToDisk: (identifier, lines, callback = (error, fsPath)->) -> + callback = _.once(callback) + fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}" + FileWriter._ensureDumpFolderExists (error) -> + return callback(error) if error? fs.writeFile fsPath, lines.join('\n'), (error) -> return callback(error) if error? callback(null, fsPath) @@ -23,11 +28,9 @@ module.exports = FileWriter = fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}" stream.pause() - fs.mkdir Settings.path.dumpFolder, (error) -> + FileWriter._ensureDumpFolderExists (error) -> + return callback(error) if error? stream.resume() - if error? and error.code != 'EEXIST' - # Ignore error about already existing - return callback(error) writeStream = fs.createWriteStream(fsPath) stream.pipe(writeStream) From 578d667efa7ff410481bbb9d3a9b1d6c855f1835 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 24 May 2018 11:30:53 +0100 Subject: [PATCH 39/57] Disable the 'select a project/file' options in project-linked-file modal --- services/web/app/views/project/editor/file-tree.pug | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index 67a3a18f04..d16258d1c9 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -362,7 +362,7 @@ script(type='text/ng-template', id='projectLinkedFileModalTemplate') ng-model="data.selectedProjectId" ng-disabled="!shouldEnableProjectSelect()" ) - option(value="") - No Project Selected + option(value="" disabled selected) - Please Select a Project option( ng-repeat="project in data.projects" value="{{ project._id }}" @@ -379,7 +379,7 @@ script(type='text/ng-template', id='projectLinkedFileModalTemplate') ng-model="data.selectedProjectEntity" ng-disabled="!shouldEnableProjectEntitySelect()" ) - option(value="") - No File Selected + option(value="" disabled selected) - Please Select a File option( ng-repeat="projectEntity in data.projectEntities" value="{{ projectEntity.path }}" From 656d40ac397ae75ec11f790afe2d093a7a5e0a5c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 24 May 2018 11:31:50 +0100 Subject: [PATCH 40/57] Better view refresh after refreshing linked file --- .../ide/binary-files/BinaryFilesManager.coffee | 14 -------------- .../controllers/BinaryFileController.coffee | 16 ++++++++++++++-- .../coffee/ide/file-tree/FileTreeManager.coffee | 9 --------- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee b/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee index fc86208717..ebecf1132e 100644 --- a/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee +++ b/services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee @@ -19,17 +19,3 @@ define [ , 0 , this ) - - openFileById: (id) -> - file = @ide.fileTreeManager.selectEntityById(id) - @$scope.ui.view = "file" - @$scope.openFile = null - @$scope.$apply() - window.setTimeout( - () => - @$scope.openFile = file - @$scope.$apply() - @$scope.$digest() - , 0 - , this - ) diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index 3ccffc9123..6b9c075a29 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -47,6 +47,18 @@ define [ else return url + _tryOpenFile = (new_file_id) -> + iterations = 0 + do tryOpen = () -> + if iterations > 10 + return + iterations += 1 + newFile = ide.fileTreeManager.findEntityById(new_file_id) + if newFile? + ide.binaryFilesManager.openFile(newFile) + else + setTimeout(tryOpen, 500) + $scope.refreshFile = (file) -> $scope.refreshing = true $scope.refreshError = null @@ -56,8 +68,8 @@ define [ { new_file_id } = data $timeout( () -> - ide.binaryFilesManager.openFileById(new_file_id) - , 1000 + _tryOpenFile(new_file_id) + , 0 ) $scope.refreshError = null .catch (response) -> diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee index dfaef74edb..d7a428ec80 100644 --- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee +++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee @@ -87,15 +87,6 @@ define [ entity.selected = false entity.selected = true - selectEntityById: (entity_id) -> - @selected_entity_id = entity_id # For reselecting after a reconnect - selected_entity = null - @ide.fileTreeManager.forEachEntity (entity) -> - if entity.id == entity_id - selected_entity = entity - entity.selected = true - return selected_entity - toggleMultiSelectEntity: (entity) -> entity.multiSelected = !entity.multiSelected @$scope.multiSelectedCount = @multiSelectedCount() From 16419847ae248a052959a40a26f0c0562d42af3e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 24 May 2018 11:32:20 +0100 Subject: [PATCH 41/57] Fix linked-file-types check when opening modal --- .../coffee/ide/file-tree/controllers/FileTreeController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bf5a6af511..5b92ded9da 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -44,7 +44,7 @@ define [ ) $scope.openProjectLinkedFileModal = window.openProjectLinkedFileModal = () -> - unless 'url' in window.data.enabledLinkedFileTypes + unless 'project_file' in window.data.enabledLinkedFileTypes console.warn("Project linked files are not enabled") return $modal.open( From de1f33a7207c7add4f4fb242e53a8a33f585811f Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 24 May 2018 11:32:44 +0100 Subject: [PATCH 42/57] Remove 'private' methods from the controller scope --- .../controllers/FileTreeController.coffee | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) 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 5b92ded9da..0f1da48731 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -242,10 +242,10 @@ define [ if fileName $scope.data.name = fileName - $scope._setInFlight = (type) -> + _setInFlight = (type) -> $scope.state.inFlight[type] = true - $scope._reset = (opts) -> + _reset = (opts) -> isError = opts.err == true inFlight = $scope.state.inFlight inFlight.projects = inFlight.entities = inFlight.create = false @@ -273,7 +273,7 @@ define [ data.name $scope.getUserProjects = () -> - $scope._setInFlight('projects') + _setInFlight('projects') ide.$http.get("/user/projects", { _csrf: window.csrfToken }) @@ -281,25 +281,25 @@ define [ $scope.data.projectEntities = null $scope.data.projects = resp.data.projects.filter (p) -> p._id != ide.project_id - $scope._reset(err: false) + _reset(err: false) .catch (err) -> - $scope._reset(err: true) + _reset(err: true) $scope.getProjectEntities = (project_id) => - $scope._setInFlight('entities') + _setInFlight('entities') ide.$http.get("/project/#{project_id}/entities", { _csrf: window.csrfToken }) .then (resp) -> if $scope.data.selectedProjectId == resp.data.project_id $scope.data.projectEntities = resp.data.entities - $scope._reset(err: false) + _reset(err: false) .catch (err) -> - $scope._reset(err: true) + _reset(err: true) $scope.init = () -> $scope.getUserProjects() - $timeout($scope.init, 100) + $timeout($scope.init, 0) $scope.create = () -> projectId = $scope.data.selectedProjectId @@ -307,9 +307,9 @@ define [ path = $scope.data.selectedProjectEntity name = $scope.data.name if !name || !path || !projectId || !projectDisplayName - $scope._reset(err: true) + _reset(err: true) return - $scope._setInFlight('create') + _setInFlight('create') ide.fileTreeManager .createLinkedFile(name, parent_folder, 'project_file', { source_project_id: projectId, @@ -317,11 +317,11 @@ define [ source_project_display_name: projectDisplayName }) .then () -> - $scope._reset(err: false) + _reset(err: false) $modalInstance.close() .catch (response)-> { data } = response - $scope._reset(err: true) + _reset(err: true) $scope.cancel = () -> $modalInstance.dismiss('cancel') From 73a45b15ce24fc775f90ec49af12501c978bd5e7 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 24 May 2018 11:35:55 +0100 Subject: [PATCH 43/57] Make string replace op safer --- .../ide/binary-files/controllers/BinaryFileController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index 6b9c075a29..fddf315f66 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -110,7 +110,7 @@ define [ if data.length >= (TWO_MEGABYTES - 200) $scope.textPreview.shouldShowDots = true # remove last partial line - data = data.replace(/\n.*$/, '') + data = data?.replace?(/\n.*$/, '') $scope.textPreview.data = data $timeout(setHeight, 0) .catch (error) -> From 3849bcfb400f266376b7250a6c44a739bcb34b20 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 May 2018 10:36:20 +0100 Subject: [PATCH 44/57] Add a `waitFor` helper to the ide object --- services/web/public/coffee/ide.coffee | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 4f2d914135..3ddf9521b6 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -229,4 +229,18 @@ define [ ide.$scope.project.publicAccesLevel = data.newAccessLevel $scope.$digest() + ide.waitFor = (fn, callback, timeout) -> + sleepTime = 500 + iterationLimit = Math.floor(timeout / sleepTime) + iterations = 0 + do tryIteration = () -> + if iterations > iterationLimit + return + iterations += 1 + result = fn() + if result? + callback(result) + else + setTimeout(tryIteration, sleepTime) + angular.bootstrap(document.body, ["SharelatexApp"]) From 19d870094789027a9916f12f62403fbf73d1a6de Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 May 2018 10:36:39 +0100 Subject: [PATCH 45/57] Use `waitFor` when refreshing the binary file view --- .../controllers/BinaryFileController.coffee | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index fddf315f66..ddff96e1d4 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -47,18 +47,6 @@ define [ else return url - _tryOpenFile = (new_file_id) -> - iterations = 0 - do tryOpen = () -> - if iterations > 10 - return - iterations += 1 - newFile = ide.fileTreeManager.findEntityById(new_file_id) - if newFile? - ide.binaryFilesManager.openFile(newFile) - else - setTimeout(tryOpen, 500) - $scope.refreshFile = (file) -> $scope.refreshing = true $scope.refreshError = null @@ -68,7 +56,13 @@ define [ { new_file_id } = data $timeout( () -> - _tryOpenFile(new_file_id) + ide.waitFor( + () -> + ide.fileTreeManager.findEntityById(new_file_id) + (newFile) -> + ide.binaryFilesManager.openFile(newFile) + 5000 + ) , 0 ) $scope.refreshError = null From e33b7b1a4925e0f2672d17d7b6d79c6826e00286 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 May 2018 10:37:04 +0100 Subject: [PATCH 46/57] Use `waitFor` when restoring a file in v2 history --- .../HistoryV2DiffController.coffee | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee index c2ced4cf59..a547f42f94 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee @@ -24,17 +24,14 @@ define [ $scope.restoreState.inflight = false openEntity = (data) -> - iterations = 0 {id, type} = data - do tryOpen = () -> - if iterations > 5 - return - iterations += 1 - entity = ide.fileTreeManager.findEntityById(id) - if entity? and type == 'doc' - ide.editorManager.openDoc(entity) - else if entity? and type == 'file' - ide.binaryFilesManager.openFile(entity) - else - setTimeout(tryOpen, 500) - \ No newline at end of file + ide.waitFor( + () -> + ide.fileTreeManager.findEntityById(id) + (entity) -> + if type == 'doc' + ide.editorManager.openDoc(entity) + else type == 'file' + ide.binaryFilesManager.openFile(entity) + 3000 + ) From f5f253ad019ca844debd67f179e266827b8010ba Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 May 2018 10:56:08 +0100 Subject: [PATCH 47/57] Add an optional pollInterval parameter to waitFor --- services/web/public/coffee/ide.coffee | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 3ddf9521b6..312d528670 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -229,18 +229,17 @@ define [ ide.$scope.project.publicAccesLevel = data.newAccessLevel $scope.$digest() - ide.waitFor = (fn, callback, timeout) -> - sleepTime = 500 - iterationLimit = Math.floor(timeout / sleepTime) + ide.waitFor = (testFunction, callback, timeout, pollInterval=500) -> + iterationLimit = Math.floor(timeout / pollInterval) iterations = 0 do tryIteration = () -> if iterations > iterationLimit return iterations += 1 - result = fn() + result = testFunction() if result? callback(result) else - setTimeout(tryIteration, sleepTime) + setTimeout(tryIteration, pollInterval) angular.bootstrap(document.body, ["SharelatexApp"]) From 8be4279165e347953d31cc65cf6dcf8bf8f5f932 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 May 2018 10:56:32 +0100 Subject: [PATCH 48/57] Fix a broken if-else-if statement --- .../ide/history/controllers/HistoryV2DiffController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee index a547f42f94..826d3381f1 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee @@ -31,7 +31,7 @@ define [ (entity) -> if type == 'doc' ide.editorManager.openDoc(entity) - else type == 'file' + else if type == 'file' ide.binaryFilesManager.openFile(entity) 3000 ) From cfc17d56e8f4702a047a5009cde15313178a148d Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 May 2018 11:42:13 +0100 Subject: [PATCH 49/57] Use a promise (with Angular's `$q`) in `waitFor` --- services/web/public/coffee/ide.coffee | 25 +++++++++++-------- .../controllers/BinaryFileController.coffee | 6 +++-- .../HistoryV2DiffController.coffee | 8 +++--- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 312d528670..424f71988a 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -55,7 +55,7 @@ define [ SafariScrollPatcher ) -> - App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, metadata) -> + App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, metadata, $q) -> # Don't freak out if we're already in an apply callback $scope.$originalApply = $scope.$apply $scope.$apply = (fn = () ->) -> @@ -229,17 +229,20 @@ define [ ide.$scope.project.publicAccesLevel = data.newAccessLevel $scope.$digest() - ide.waitFor = (testFunction, callback, timeout, pollInterval=500) -> + ide.waitFor = (testFunction, timeout, pollInterval=500) -> iterationLimit = Math.floor(timeout / pollInterval) iterations = 0 - do tryIteration = () -> - if iterations > iterationLimit - return - iterations += 1 - result = testFunction() - if result? - callback(result) - else - setTimeout(tryIteration, pollInterval) + $q( + (resolve, reject) -> + do tryIteration = () -> + if iterations > iterationLimit + return reject(new Error("waiting too long, #{JSON.stringify({timeout, pollInterval})}")) + iterations += 1 + result = testFunction() + if result? + resolve(result) + else + setTimeout(tryIteration, pollInterval) + ) angular.bootstrap(document.body, ["SharelatexApp"]) diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index ddff96e1d4..ed6fb8ec11 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -59,10 +59,12 @@ define [ ide.waitFor( () -> ide.fileTreeManager.findEntityById(new_file_id) - (newFile) -> - ide.binaryFilesManager.openFile(newFile) 5000 ) + .then (newFile) -> + ide.binaryFilesManager.openFile(newFile) + .catch (err) -> + console.warn(err) , 0 ) $scope.refreshError = null diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee index 826d3381f1..5db663cb5e 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee @@ -28,10 +28,12 @@ define [ ide.waitFor( () -> ide.fileTreeManager.findEntityById(id) - (entity) -> + 3000 + ) + .then (entity) -> if type == 'doc' ide.editorManager.openDoc(entity) else if type == 'file' ide.binaryFilesManager.openFile(entity) - 3000 - ) + .catch (err) -> + console.warn(err) From 105d858155668cab54cfe5c025da26cdd8c30c5f Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 May 2018 13:02:58 +0100 Subject: [PATCH 50/57] Move `waitFor` into an angular service --- services/web/public/coffee/ide.coffee | 17 +--------------- .../controllers/BinaryFileController.coffee | 4 ++-- .../HistoryV2DiffController.coffee | 4 ++-- .../public/coffee/services/wait-for.coffee | 20 +++++++++++++++++++ 4 files changed, 25 insertions(+), 20 deletions(-) create mode 100644 services/web/public/coffee/services/wait-for.coffee diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 424f71988a..6c5b1d920e 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -35,6 +35,7 @@ define [ "directives/videoPlayState" "services/queued-http" "services/validateCaptcha" + "services/wait-for" "filters/formatDate" "main/event" "main/account-upgrade" @@ -229,20 +230,4 @@ define [ ide.$scope.project.publicAccesLevel = data.newAccessLevel $scope.$digest() - ide.waitFor = (testFunction, timeout, pollInterval=500) -> - iterationLimit = Math.floor(timeout / pollInterval) - iterations = 0 - $q( - (resolve, reject) -> - do tryIteration = () -> - if iterations > iterationLimit - return reject(new Error("waiting too long, #{JSON.stringify({timeout, pollInterval})}")) - iterations += 1 - result = testFunction() - if result? - resolve(result) - else - setTimeout(tryIteration, pollInterval) - ) - angular.bootstrap(document.body, ["SharelatexApp"]) diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index ed6fb8ec11..bba455c447 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -2,7 +2,7 @@ define [ "base" "moment" ], (App, moment) -> - App.controller "BinaryFileController", ["$scope", "$rootScope", "$http", "$timeout", "$element", "ide", ($scope, $rootScope, $http, $timeout, $element, ide) -> + App.controller "BinaryFileController", ["$scope", "$rootScope", "$http", "$timeout", "$element", "ide", "waitFor", ($scope, $rootScope, $http, $timeout, $element, ide, waitFor) -> TWO_MEGABYTES = 2 * 1024 * 1024 @@ -56,7 +56,7 @@ define [ { new_file_id } = data $timeout( () -> - ide.waitFor( + waitFor( () -> ide.fileTreeManager.findEntityById(new_file_id) 5000 diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee index 5db663cb5e..279c230afb 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee @@ -1,7 +1,7 @@ define [ "base" ], (App) -> - App.controller "HistoryV2DiffController", ($scope, ide, event_tracking) -> + App.controller "HistoryV2DiffController", ($scope, ide, event_tracking, waitFor) -> $scope.restoreState = inflight: false error: false @@ -25,7 +25,7 @@ define [ openEntity = (data) -> {id, type} = data - ide.waitFor( + waitFor( () -> ide.fileTreeManager.findEntityById(id) 3000 diff --git a/services/web/public/coffee/services/wait-for.coffee b/services/web/public/coffee/services/wait-for.coffee new file mode 100644 index 0000000000..409142354c --- /dev/null +++ b/services/web/public/coffee/services/wait-for.coffee @@ -0,0 +1,20 @@ +define [ + "base" +], (App) -> + App.factory "waitFor", ($q) -> + waitFor = (testFunction, timeout, pollInterval=500) -> + iterationLimit = Math.floor(timeout / pollInterval) + iterations = 0 + $q( + (resolve, reject) -> + do tryIteration = () -> + if iterations > iterationLimit + return reject(new Error("waiting too long, #{JSON.stringify({timeout, pollInterval})}")) + iterations += 1 + result = testFunction() + if result? + resolve(result) + else + setTimeout(tryIteration, pollInterval) + ) + return waitFor From c8a8fe6af7aaf191d58c852b0fd23416ed85ac6e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 May 2018 16:01:11 +0100 Subject: [PATCH 51/57] Use Agent.handleError in case checkAuth produces an error --- .../coffee/Features/LinkedFiles/LinkedFilesController.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee index 67acdcfc86..807325371f 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee @@ -24,8 +24,8 @@ module.exports = LinkedFilesController = { linkedFileData = Agent.sanitizeData(data) linkedFileData.provider = provider Agent.checkAuth project_id, data, user_id, (err, allowed) -> - return next(err) if err? - return ses.sendStatus(403) if !allowed + return Agent.handleError(err, req, res, next) if err? + return res.sendStatus(403) if !allowed Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) -> if error? logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk' From bc7d6a64ede03970666373993811dfaf494a8c9c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 May 2018 16:01:37 +0100 Subject: [PATCH 52/57] Add a trailing comma --- services/web/app/coffee/router.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 7b0d11863b..2ec3011ea2 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -121,7 +121,7 @@ module.exports = class Router webRouter.get '/user/projects', AuthenticationController.requireLogin(), ProjectController.userProjectsJson webRouter.get '/project/:Project_id/entities', AuthenticationController.requireLogin(), - AuthorizationMiddlewear.ensureUserCanReadProject + AuthorizationMiddlewear.ensureUserCanReadProject, ProjectController.projectEntitiesJson webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage From ce147b012f34afd41fd5584aeeb34ddc9a1913e6 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 May 2018 16:03:45 +0100 Subject: [PATCH 53/57] Cleaner unpacking of data from scope --- .../ide/file-tree/controllers/FileTreeController.coffee | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 0f1da48731..0b9ae20def 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -252,13 +252,11 @@ define [ $scope.state.error = isError $scope.shouldEnableProjectSelect = () -> - state = $scope.state - data = $scope.data + { state, data } = $scope return !state.inFlight.projects && data.projects $scope.shouldEnableProjectEntitySelect = () -> - state = $scope.state - data = $scope.data + { state, data } = $scope return !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProjectId $scope.shouldEnableCreateButton = () -> From 92fb83e665a1a6c5fd32d27c08ac9ad40bb9c6cf Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 29 May 2018 10:05:50 +0100 Subject: [PATCH 54/57] Use the correct linkedFileData var --- .../coffee/Features/LinkedFiles/LinkedFilesController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee index 807325371f..e0a5998327 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee @@ -23,7 +23,7 @@ module.exports = LinkedFilesController = { linkedFileData = Agent.sanitizeData(data) linkedFileData.provider = provider - Agent.checkAuth project_id, data, user_id, (err, allowed) -> + Agent.checkAuth project_id, linkedFileData, user_id, (err, allowed) -> return Agent.handleError(err, req, res, next) if err? return res.sendStatus(403) if !allowed Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) -> From dccac6302eec8f3e72253f7aaa52c9ee023bf0bd Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 29 May 2018 10:07:31 +0100 Subject: [PATCH 55/57] Use a `decorateLinkedFileData` function on server to add project name --- .../LinkedFiles/LinkedFilesController.coffee | 19 ++++++++------ .../LinkedFiles/ProjectFileAgent.coffee | 26 ++++++++++++++++--- .../Features/LinkedFiles/UrlAgent.coffee | 3 +++ .../controllers/FileTreeController.coffee | 6 ++--- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee index e0a5998327..1a5e13e86e 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee @@ -26,11 +26,14 @@ module.exports = LinkedFilesController = { Agent.checkAuth project_id, linkedFileData, user_id, (err, allowed) -> return Agent.handleError(err, req, res, next) if err? return res.sendStatus(403) if !allowed - Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) -> - if error? - logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk' - return Agent.handleError(error, req, res, next) - EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error, file) -> - return next(error) if error? - res.json(new_file_id: file._id) # created -} + Agent.decorateLinkedFileData linkedFileData, (err, newLinkedFileData) -> + return Agent.handleError(err) if err? + linkedFileData = newLinkedFileData + Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) -> + if error? + logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk' + return Agent.handleError(error, req, res, next) + EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error, file) -> + return next(error) if error? + res.json(new_file_id: file._id) # created + } diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee index 75aadf76fc..5ea4554426 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -1,6 +1,7 @@ FileWriter = require('../../infrastructure/FileWriter') AuthorizationManager = require('../Authorization/AuthorizationManager') ProjectLocator = require('../Project/ProjectLocator') +ProjectGetter = require('../Project/ProjectGetter') DocstoreManager = require('../Docstore/DocstoreManager') FileStoreHandler = require('../FileStore/FileStoreHandler') FileWriter = require('../../infrastructure/FileWriter') @@ -32,6 +33,14 @@ BadDataError = (message) -> BadDataError.prototype.__proto__ = Error.prototype +ProjectNotFoundError = (message) -> + error = new Error(message) + error.name = 'ProjectNotFound' + error.__proto__ = ProjectNotFoundError.prototype + return error +ProjectNotFoundError.prototype.__proto__ = Error.prototype + + SourceFileNotFoundError = (message) -> error = new Error(message) error.name = 'BadData' @@ -46,17 +55,24 @@ module.exports = ProjectFileAgent = return _.pick( data, 'source_project_id', - 'source_entity_path', - 'source_project_display_name' + 'source_entity_path' ) _validate: (data) -> return ( data.source_project_id? && - data.source_entity_path? && - data.source_project_display_name? + data.source_entity_path? ) + decorateLinkedFileData: (data, callback = (err, newData) ->) -> + callback = _.once(callback) + { source_project_id } = data + return callback(new BadDataError()) if !source_project_id? + ProjectGetter.getProject source_project_id, (err, project) -> + return callback(err) if err? + return callback(new ProjectNotFoundError()) if !project? + callback(err, _.extend(data, {source_project_display_name: project.name})) + checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) -> callback = _.once(callback) if !ProjectFileAgent._validate(data) @@ -104,6 +120,8 @@ module.exports = ProjectFileAgent = res.status(400).send("The file is the wrong type") else if error instanceof SourceFileNotFoundError res.status(404).send("Source file not found") + else if error instanceof ProjectNotFoundError + res.status(404).send("Project not found") else next(error) next() diff --git a/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee index 59422c5e67..7a15fe52d3 100644 --- a/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee @@ -27,6 +27,9 @@ module.exports = UrlAgent = { url: @._prependHttpIfNeeded(data.url) } + decorateLinkedFileData: (data, callback = (err, newData) ->) -> + return callback(null, data) + checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) -> callback(null, true) 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 0b9ae20def..010e00476f 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -301,18 +301,16 @@ define [ $scope.create = () -> projectId = $scope.data.selectedProjectId - projectDisplayName = _.find($scope.data.projects, (p) -> p._id == projectId).name path = $scope.data.selectedProjectEntity name = $scope.data.name - if !name || !path || !projectId || !projectDisplayName + if !name || !path || !projectId _reset(err: true) return _setInFlight('create') ide.fileTreeManager .createLinkedFile(name, parent_folder, 'project_file', { source_project_id: projectId, - source_entity_path: path, - source_project_display_name: projectDisplayName + source_entity_path: path }) .then () -> _reset(err: false) From 14898acd7f8ee5f8dee4a7bc7efb3c6eb7663063 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 29 May 2018 10:40:38 +0100 Subject: [PATCH 56/57] Update linked-file acceptance tests --- .../web/test/acceptance/coffee/LinkedFilesTests.coffee | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee index 34079de8d1..f3e8694e2a 100644 --- a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee +++ b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee @@ -105,7 +105,6 @@ describe "LinkedFiles", -> data: source_project_id: @project_two_id, source_entity_path: "/#{@source_doc_name}", - source_project_display_name: "Project Two" }, (error, response, body) => new_file_id = body.new_file_id @existing_file_id = new_file_id @@ -114,6 +113,12 @@ describe "LinkedFiles", -> return done(error) if error? firstFile = project.rootFolder[0].fileRefs[0] expect(firstFile._id.toString()).to.equal(new_file_id.toString()) + expect(firstFile.linkedFileData).to.deep.equal { + provider: 'project_file', + source_project_id: @project_two_id, + source_entity_path: "/#{@source_doc_name}", + source_project_display_name: "plf-test-two" + } expect(firstFile.name).to.equal('test-link.txt') done() @@ -127,7 +132,6 @@ describe "LinkedFiles", -> data: source_project_id: @project_two_id, source_entity_path: "/#{@source_doc_name}", - source_project_display_name: "Project Two" }, (error, response, body) => new_file_id = body.new_file_id expect(new_file_id).to.exist From 9e65e5e8130051aa9e6d9563fe2722185ecd13d5 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 29 May 2018 10:46:22 +0100 Subject: [PATCH 57/57] Fix loading of Rich Text page in Test Controls --- .../test-controls/controllers/TestControlsController.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee b/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee index 65378a2cde..ae7db45905 100644 --- a/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee +++ b/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee @@ -11,4 +11,6 @@ define [ window.openLinkedFileModal() $scope.richText = () -> - window.location.href = window.location.toString() + '&rt=true' + current = window.location.toString() + target = "#{current}#{if window.location.search then '&' else '?'}rt=true" + window.location.href = target