From 5cb85c03321c1f7d694387f7eecabc3c9f6a217d Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 3 May 2018 14:29:03 +0100 Subject: [PATCH 01/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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 c4da8701c8b23d366a75d195c5f6815c4b6c7384 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 May 2018 13:38:19 +0100 Subject: [PATCH 51/76] On v2, use smaller (default) border radius on select inputs --- services/web/public/stylesheets/components/forms.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/web/public/stylesheets/components/forms.less b/services/web/public/stylesheets/components/forms.less index 8eb92411cd..62def5cf9d 100755 --- a/services/web/public/stylesheets/components/forms.less +++ b/services/web/public/stylesheets/components/forms.less @@ -149,6 +149,10 @@ output { textarea& { height: auto; } + // Smaller border-radius for `select` inputs + select& { + border-radius: @border-radius-base; + } } From 2a11a70cd3cde2a5ba981575c2bad9cf8c399473 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 May 2018 14:48:37 +0100 Subject: [PATCH 52/76] Use smaller border-radius on textarea inputs, on v2 --- services/web/public/stylesheets/components/forms.less | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/components/forms.less b/services/web/public/stylesheets/components/forms.less index 62def5cf9d..00653b105b 100755 --- a/services/web/public/stylesheets/components/forms.less +++ b/services/web/public/stylesheets/components/forms.less @@ -145,9 +145,10 @@ output { opacity: 1; // iOS fix for unreadable disabled content } - // Reset height for `textarea`s + // Reset height for `textarea`s, and smaller border-radius textarea& { height: auto; + border-radius: @border-radius-base; } // Smaller border-radius for `select` inputs select& { From c8a8fe6af7aaf191d58c852b0fd23416ed85ac6e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 25 May 2018 16:01:11 +0100 Subject: [PATCH 53/76] 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 54/76] 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 55/76] 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 56/76] 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 57/76] 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 58/76] 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 59/76] 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 From c6d2b4f1e79159097200920fdb5b32a668b4916a Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 25 May 2018 13:46:54 +0100 Subject: [PATCH 60/76] Use single rich text include instead of split toolbar & body includes --- services/web/app/views/project/editor/editor.pug | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index 3cac3a9490..184b7e854b 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -33,7 +33,7 @@ div.full-size( i.fa.fa-arrow-left |   #{translate("open_a_file_on_the_left")} - != moduleIncludes('editor:toolbar', locals) + != moduleIncludes('editor:main', locals) #editor( ace-editor="editor", @@ -73,8 +73,6 @@ div.full-size( line-height="settings.lineHeight || ui.defaultLineHeight" ) - != moduleIncludes('editor:body', locals) - include ./review-panel .ui-layout-east From d0b160d9a2b403dfd5ea478071aa397749867332 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 25 May 2018 13:48:35 +0100 Subject: [PATCH 61/76] Rename flag for clarity --- services/web/app/views/project/editor/editor.pug | 2 +- services/web/public/coffee/ide/editor/EditorManager.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index 184b7e854b..c24d967da7 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -37,7 +37,7 @@ div.full-size( #editor( ace-editor="editor", - ng-if="!editor.richText", + ng-if="!editor.showRichText", ng-show="!!editor.sharejs_doc && !editor.opening", style=showRichText ? "top: 32px" : "", theme="settings.theme", diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index e3cabf8e98..7246e09b83 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -14,7 +14,7 @@ define [ opening: true trackChanges: false wantTrackChanges: false - richText: false + showRichText: false } @$scope.$on "entity:selected", (event, entity) => From a747480425b79eacaea6d329a2b4171ec1821546 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 30 May 2018 15:28:59 +0100 Subject: [PATCH 62/76] add references host into settings --- .../app/coffee/Features/References/ReferencesHandler.coffee | 2 ++ services/web/config/settings.defaults.coffee | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index 8728896631..959833351f 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -10,6 +10,8 @@ Async = require('async') oneMinInMs = 60 * 1000 fiveMinsInMs = oneMinInMs * 5 +if !settings.apis?.references?.url? + logger.log "references search not enabled" module.exports = ReferencesHandler = diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 0892804778..7f4024c368 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -146,8 +146,8 @@ module.exports = settings = url: "http://#{process.env['CONTACTS_HOST'] or 'localhost'}:3036" sixpack: url: "" - # references: - # url: "http://localhost:3040" + references: + url: "http://#{process.env['REFERENCES_HOST'] or 'localhost'}:3040" notifications: url: "http://#{process.env['NOTIFICATIONS_HOST'] or 'localhost'}:3042" analytics: From 20cca0fcd4d303e1813dae8a3979e4723044968f Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 16 May 2018 16:53:33 +0100 Subject: [PATCH 63/76] Add action buttons to project list --- services/web/app/views/project/list/item.pug | 23 +++++++++++++++++-- .../app/views/project/list/project-list.pug | 6 +++-- .../web/app/views/project/list/v1-item.pug | 4 ++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index bfc53a8360..b2fc9390de 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -1,4 +1,4 @@ -.col-xs-6 +.col-xs-6.col-sm-4.col-md-6 input.select-item( select-individual, type="checkbox", @@ -37,8 +37,27 @@ tooltip-placement="right" tooltip-append-to-body="true" ) -.col-xs-4 +.col-xs-4.col-sm-3.col-md-2 if settings.overleaf span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}} else span.last-modified {{project.lastUpdated | formatDate}} +.hidden-xs.col-sm-3.col-md-2 + button.btn.btn-link.action-btn( + tooltip=translate('copy'), + tooltip-placement="top", + tooltip-append-to-body="true", + ) + i.icon.fa.fa-files-o + button.btn.btn-link.action-btn( + tooltip=translate('download'), + tooltip-placement="top", + tooltip-append-to-body="true", + ) + i.icon.fa.fa-cloud-download + button.btn.btn-link.action-btn( + tooltip=translate('archive'), + tooltip-placement="top", + tooltip-append-to-body="true", + ) + i.icon.fa.fa-inbox \ No newline at end of file diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug index df3c2bf681..6e1732ad63 100644 --- a/services/web/app/views/project/list/project-list.pug +++ b/services/web/app/views/project/list/project-list.pug @@ -131,7 +131,7 @@ ) li.container-fluid .row - .col-xs-6 + .col-xs-6.col-sm-4.col-md-6 input.select-all( select-all, type="checkbox" @@ -142,9 +142,11 @@ .col-xs-2 span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")} i.tablesort.fa(ng-class="getSortIconClass('accessLevel')") - .col-xs-4 + .col-xs-4.col-sm-3.col-md-2 span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")} i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')") + .hidden-xs.col-sm-3.col-md-2 + span.header #{translate("actions")} li.project_entry.container-fluid( ng-repeat="project in visibleProjects | orderBy:predicate:reverse", ng-controller="ProjectListItemController" diff --git a/services/web/app/views/project/list/v1-item.pug b/services/web/app/views/project/list/v1-item.pug index b4a3ccb99d..5a8e37bca0 100644 --- a/services/web/app/views/project/list/v1-item.pug +++ b/services/web/app/views/project/list/v1-item.pug @@ -1,4 +1,4 @@ -.col-xs-6 +.col-xs-6.col-sm-4.col-md-6 .select-item span.v1-badge( aria-label=translate("v1_badge") @@ -21,5 +21,5 @@ .col-xs-2 span.owner {{ownerName()}} -.col-xs-4 +.col-xs-4.col-sm-3.col-md-2 span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}} \ No newline at end of file From 2d1bcda9ff7a70e0ff628d843d6042fb6604b936 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 16 May 2018 16:55:23 +0100 Subject: [PATCH 64/76] Style action buttons, and hide on smaller screens --- services/web/public/stylesheets/app/project-list.less | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 982086a262..0843fc6784 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -369,6 +369,16 @@ ul.project-list { .v1-badge { margin-left: -4px; } + + .action-btn-row { + padding-right: 20px; + } + + .action-btn { + padding: 0 0.3em; + margin-left: 0.2em; + float: right; + } } i.tablesort { padding-left: 8px; From 83c62c8ab14c41229b394c7f6a783eb8c0a873ed Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 16 May 2018 18:05:33 +0100 Subject: [PATCH 65/76] Only show action buttons on v2 --- services/web/app/views/project/list/item.pug | 48 +++++++++++-------- .../app/views/project/list/project-list.pug | 12 +++-- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index b2fc9390de..ccbf73ac80 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -1,4 +1,7 @@ -.col-xs-6.col-sm-4.col-md-6 +- var titleClasses = settings.overleaf ? "col-xs-6 col-sm-4 col-md-6" : "col-xs-6" +- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4" + +div(class=titleClasses) input.select-item( select-individual, type="checkbox", @@ -37,27 +40,30 @@ tooltip-placement="right" tooltip-append-to-body="true" ) -.col-xs-4.col-sm-3.col-md-2 + +div(class=lastUpdatedClasses) if settings.overleaf span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}} else span.last-modified {{project.lastUpdated | formatDate}} -.hidden-xs.col-sm-3.col-md-2 - button.btn.btn-link.action-btn( - tooltip=translate('copy'), - tooltip-placement="top", - tooltip-append-to-body="true", - ) - i.icon.fa.fa-files-o - button.btn.btn-link.action-btn( - tooltip=translate('download'), - tooltip-placement="top", - tooltip-append-to-body="true", - ) - i.icon.fa.fa-cloud-download - button.btn.btn-link.action-btn( - tooltip=translate('archive'), - tooltip-placement="top", - tooltip-append-to-body="true", - ) - i.icon.fa.fa-inbox \ No newline at end of file + +if settings.overleaf + .hidden-xs.col-sm-3.col-md-2 + button.btn.btn-link.action-btn( + tooltip=translate('copy'), + tooltip-placement="top", + tooltip-append-to-body="true", + ) + i.icon.fa.fa-files-o + button.btn.btn-link.action-btn( + tooltip=translate('download'), + tooltip-placement="top", + tooltip-append-to-body="true", + ) + i.icon.fa.fa-cloud-download + button.btn.btn-link.action-btn( + tooltip=translate('archive'), + tooltip-placement="top", + tooltip-append-to-body="true", + ) + i.icon.fa.fa-inbox \ No newline at end of file diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug index 6e1732ad63..5ce92d3d5e 100644 --- a/services/web/app/views/project/list/project-list.pug +++ b/services/web/app/views/project/list/project-list.pug @@ -131,7 +131,10 @@ ) li.container-fluid .row - .col-xs-6.col-sm-4.col-md-6 + - var titleClasses = settings.overleaf ? " col-xs-6 col-sm-4 col-md-6" : "col-xs-6" + - var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4" + + div(class=titleClasses) input.select-all( select-all, type="checkbox" @@ -142,11 +145,12 @@ .col-xs-2 span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")} i.tablesort.fa(ng-class="getSortIconClass('accessLevel')") - .col-xs-4.col-sm-3.col-md-2 + div(class=lastUpdatedClasses) span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")} i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')") - .hidden-xs.col-sm-3.col-md-2 - span.header #{translate("actions")} + if settings.overleaf + .hidden-xs.col-sm-3.col-md-2 + span.header #{translate("actions")} li.project_entry.container-fluid( ng-repeat="project in visibleProjects | orderBy:predicate:reverse", ng-controller="ProjectListItemController" From a2dff4bfbbfa69e044953f6acdfe6a7dc90e4146 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 17 May 2018 11:12:52 +0100 Subject: [PATCH 66/76] Right align actions header --- services/web/app/views/project/list/item.pug | 2 +- services/web/app/views/project/list/project-list.pug | 2 +- services/web/public/stylesheets/app/project-list.less | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index ccbf73ac80..3326168105 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -48,7 +48,7 @@ div(class=lastUpdatedClasses) span.last-modified {{project.lastUpdated | formatDate}} if settings.overleaf - .hidden-xs.col-sm-3.col-md-2 + .hidden-xs.col-sm-3.col-md-2.action-btn-row button.btn.btn-link.action-btn( tooltip=translate('copy'), tooltip-placement="top", diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug index 5ce92d3d5e..cfea4aa6c6 100644 --- a/services/web/app/views/project/list/project-list.pug +++ b/services/web/app/views/project/list/project-list.pug @@ -149,7 +149,7 @@ span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")} i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')") if settings.overleaf - .hidden-xs.col-sm-3.col-md-2 + .hidden-xs.col-sm-3.col-md-2.action-btn-row-header span.header #{translate("actions")} li.project_entry.container-fluid( ng-repeat="project in visibleProjects | orderBy:predicate:reverse", diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 0843fc6784..569035ad72 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -370,14 +370,14 @@ ul.project-list { margin-left: -4px; } - .action-btn-row { + .action-btn-row-header, .action-btn-row { padding-right: 20px; + text-align: right; } .action-btn { padding: 0 0.3em; margin-left: 0.2em; - float: right; } } i.tablesort { From ffc06f2a3b4365a194bf9bfafdab96967947fb83 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 17 May 2018 14:28:34 +0100 Subject: [PATCH 67/76] Archive project action button --- services/web/app/views/project/list/item.pug | 1 + .../coffee/main/project-list/project-list.coffee | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index 3326168105..50a613980f 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -65,5 +65,6 @@ if settings.overleaf tooltip=translate('archive'), tooltip-placement="top", tooltip-append-to-body="true", + ng-click="archive($event)" ) i.icon.fa.fa-inbox \ No newline at end of file diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index 36520d2cc7..0a01eb52d3 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -350,14 +350,15 @@ define [ $scope.archiveOrLeaveSelectedProjects() $scope.archiveOrLeaveSelectedProjects = () -> - selected_projects = $scope.getSelectedProjects() - selected_project_ids = $scope.getSelectedProjectIds() + $scope.archiveOrLeaveProjects($scope.getSelectedProjects()) + $scope.archiveOrLeaveProjects = (projects) -> + projectIds = projects.map (p) -> p.id # Remove project from any tags for tag in $scope.tags - $scope._removeProjectIdsFromTagArray(tag, selected_project_ids) + $scope._removeProjectIdsFromTagArray(tag, projectIds) - for project in selected_projects + for project in projects project.tags = [] if project.accessLevel == "owner" project.archived = true @@ -490,3 +491,7 @@ define [ $scope.$watch "project.selected", (value) -> if value? $scope.updateSelectedProjects() + + $scope.archive = (e) -> + e.stopPropagation() + $scope.archiveOrLeaveProjects([$scope.project]) From 2354f4156bf0cddcf07a62acf812c1a43fdb2ef1 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 17 May 2018 14:28:34 +0100 Subject: [PATCH 68/76] Download project action button --- services/web/app/views/project/list/item.pug | 1 + .../main/project-list/project-list.coffee | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index 50a613980f..2f008101f9 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -59,6 +59,7 @@ if settings.overleaf tooltip=translate('download'), tooltip-placement="top", tooltip-append-to-body="true", + ng-click="download($event)" ) i.icon.fa.fa-cloud-download button.btn.btn-link.action-btn( diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index 0a01eb52d3..f458ebd15c 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -438,13 +438,14 @@ define [ ) $scope.downloadSelectedProjects = () -> - selected_project_ids = $scope.getSelectedProjectIds() - event_tracking.send 'project-list-page-interaction', 'project action', 'Download Zip' - if selected_project_ids.length > 1 - path = "/project/download/zip?project_ids=#{selected_project_ids.join(',')}" - else - path = "/project/#{selected_project_ids[0]}/download/zip" + $scope.downloadProjectsById($scope.getSelectedProjectIds()) + $scope.downloadProjectsById = (projectIds) -> + event_tracking.send 'project-list-page-interaction', 'project action', 'Download Zip' + if projectIds.length > 1 + path = "/project/download/zip?project_ids=#{projectIds.join(',')}" + else + path = "/project/#{projectIds[0]}/download/zip" window.location = path $scope.openV1ImportModal = (project) -> @@ -492,6 +493,10 @@ define [ if value? $scope.updateSelectedProjects() + $scope.download = (e) -> + e.stopPropagation() + $scope.downloadProjectsById([$scope.project.id]) + $scope.archive = (e) -> e.stopPropagation() $scope.archiveOrLeaveProjects([$scope.project]) From 7dffc568049e95665293711e0dde43b0fb2d8e15 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 17 May 2018 14:28:34 +0100 Subject: [PATCH 69/76] Clone project action button --- services/web/app/views/project/list/item.pug | 1 + .../web/public/coffee/main/project-list/project-list.coffee | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index 2f008101f9..a5362ebfd7 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -53,6 +53,7 @@ if settings.overleaf tooltip=translate('copy'), tooltip-placement="top", tooltip-append-to-body="true", + ng-click="clone($event)" ) i.icon.fa.fa-files-o button.btn.btn-link.action-btn( diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index f458ebd15c..72074a19b9 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -493,6 +493,10 @@ define [ if value? $scope.updateSelectedProjects() + $scope.clone = (e) -> + e.stopPropagation() + $scope.cloneProject($scope.project, "#{$scope.project.name} (Copy)") + $scope.download = (e) -> e.stopPropagation() $scope.downloadProjectsById([$scope.project.id]) From 5ec238cae88483325d4b93c960b8aadd71dac6e2 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 17 May 2018 15:04:50 +0100 Subject: [PATCH 70/76] Switch archive button with restore button for archived projects --- services/web/app/views/project/list/item.pug | 11 ++++++++++- .../coffee/main/project-list/project-list.coffee | 13 +++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index a5362ebfd7..bc37683489 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -64,9 +64,18 @@ if settings.overleaf ) i.icon.fa.fa-cloud-download button.btn.btn-link.action-btn( + ng-if="!project.archived" tooltip=translate('archive'), tooltip-placement="top", tooltip-append-to-body="true", ng-click="archive($event)" ) - i.icon.fa.fa-inbox \ No newline at end of file + i.icon.fa.fa-inbox + button.btn.btn-link.action-btn( + ng-if="project.archived" + tooltip=translate('unarchive'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="restore($event)" + ) + i.icon.fa.fa-reply \ No newline at end of file diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index 72074a19b9..c2b251bd65 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -415,13 +415,14 @@ define [ $scope.updateVisibleProjects() $scope.restoreSelectedProjects = () -> - selected_projects = $scope.getSelectedProjects() - selected_project_ids = $scope.getSelectedProjectIds() + $scope.restoreProjects($scope.getSelectedProjects()) - for project in selected_projects + $scope.restoreProjects = (projects) -> + projectIds = projects.map (p) -> p.id + for project in projects project.archived = false - for project_id in selected_project_ids + for projectId in projectIds queuedHttp { method: "POST" url: "/project/#{project_id}/restore" @@ -504,3 +505,7 @@ define [ $scope.archive = (e) -> e.stopPropagation() $scope.archiveOrLeaveProjects([$scope.project]) + + $scope.restore = (e) -> + e.stopPropagation() + $scope.restoreProjects([$scope.project]) From 7f86ddc72c55ce61945066ef7bc5e537fa15f91c Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Thu, 31 May 2018 11:12:31 +0100 Subject: [PATCH 71/76] extract v1 templates code to web --- .../Templates/TemplatesController.coffee | 80 +++++++++++++++++++ .../Templates/TemplatesMiddlewear.coffee | 9 +++ .../Features/Templates/TemplatesRouter.coffee | 10 +++ services/web/app/coffee/router.coffee | 3 +- .../project/editor/new_from_template.pug | 26 ++++++ 5 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 services/web/app/coffee/Features/Templates/TemplatesController.coffee create mode 100644 services/web/app/coffee/Features/Templates/TemplatesMiddlewear.coffee create mode 100644 services/web/app/coffee/Features/Templates/TemplatesRouter.coffee create mode 100644 services/web/app/views/project/editor/new_from_template.pug diff --git a/services/web/app/coffee/Features/Templates/TemplatesController.coffee b/services/web/app/coffee/Features/Templates/TemplatesController.coffee new file mode 100644 index 0000000000..0d687a7f06 --- /dev/null +++ b/services/web/app/coffee/Features/Templates/TemplatesController.coffee @@ -0,0 +1,80 @@ +path = require('path') +Project = require('../../../js/models/Project').Project +ProjectUploadManager = require('../../../js/Features/Uploads/ProjectUploadManager') +ProjectOptionsHandler = require("../../../js/Features/Project/ProjectOptionsHandler") +AuthenticationController = require('../../../js/Features/Authentication/AuthenticationController') +settings = require('settings-sharelatex') +fs = require('fs') +request = require('request') +uuid = require('uuid') +logger = require('logger-sharelatex') +async = require("async") + + +module.exports = TemplatesController = + + getV1Template: (req, res)-> + templateVersionId = req.params.Template_version_id + templateId = req.query.id + if !/^[0-9]+$/.test(templateVersionId) || !/^[0-9]+$/.test(templateId) + logger.err templateVersionId:templateVersionId, templateId: templateId, "invalid template id or version" + return res.sendStatus 400 + data = {} + data.templateVersionId = templateVersionId + data.templateId = templateId + data.name = req.query.templateName + data.compiler = req.query.latexEngine + res.render path.resolve(__dirname, "../../../views/project/editor/new_from_template"), data + + createProjectFromV1Template: (req, res)-> + currentUserId = AuthenticationController.getLoggedInUserId(req) + zipUrl = "#{settings.apis.v1.url}/api/v1/sharelatex/templates/#{req.body.templateVersionId}" + zipReq = request(zipUrl, { + 'auth': { + 'user': settings.apis.v1.user, + 'pass': settings.apis.v1.pass + } + }) + + TemplatesController.createFromZip( + zipReq, + { + templateName: req.body.templateName, + currentUserId: currentUserId, + compiler: req.body.compiler + docId: req.body.docId + templateId: req.body.templateId + templateVersionId: req.body.templateVersionId + }, + req, + res + ) + + createFromZip: (zipReq, options, req, res)-> + dumpPath = "#{settings.path.dumpFolder}/#{uuid.v4()}" + writeStream = fs.createWriteStream(dumpPath) + + zipReq.on "error", (error) -> + logger.error err: error, "error getting zip from template API" + zipReq.pipe(writeStream) + writeStream.on 'close', -> + ProjectUploadManager.createProjectFromZipArchive options.currentUserId, options.templateName, dumpPath, (err, project)-> + if err? + logger.err err:err, zipReq:zipReq, "problem building project from zip" + return res.sendStatus 500 + setCompiler project._id, options.compiler, -> + fs.unlink dumpPath, -> + delete req.session.templateData + conditions = {_id:project._id} + update = { + fromV1TemplateId:options.templateId, + fromV1TemplateVersionId:options.templateVersionId + } + Project.update conditions, update, {}, (err)-> + res.redirect "/project/#{project._id}" + +setCompiler = (project_id, compiler, callback)-> + if compiler? + ProjectOptionsHandler.setCompiler project_id, compiler, callback + else + callback() diff --git a/services/web/app/coffee/Features/Templates/TemplatesMiddlewear.coffee b/services/web/app/coffee/Features/Templates/TemplatesMiddlewear.coffee new file mode 100644 index 0000000000..8baa0ca605 --- /dev/null +++ b/services/web/app/coffee/Features/Templates/TemplatesMiddlewear.coffee @@ -0,0 +1,9 @@ +settings = require("settings-sharelatex") +logger = require("logger-sharelatex") + + +module.exports = + saveTemplateDataInSession: (req, res, next)-> + if req.query.templateName + req.session.templateData = req.query + next() diff --git a/services/web/app/coffee/Features/Templates/TemplatesRouter.coffee b/services/web/app/coffee/Features/Templates/TemplatesRouter.coffee new file mode 100644 index 0000000000..3061789591 --- /dev/null +++ b/services/web/app/coffee/Features/Templates/TemplatesRouter.coffee @@ -0,0 +1,10 @@ +AuthenticationController = require('../Authentication/AuthenticationController') +TemplatesController = require("./TemplatesController") +TemplatesMiddlewear = require('./TemplatesMiddlewear') + +module.exports = + apply: (app)-> + + app.get '/project/new/template/:Template_version_id', TemplatesMiddlewear.saveTemplateDataInSession, AuthenticationController.requireLogin(), TemplatesController.getV1Template + + app.post '/project/new/template', AuthenticationController.requireLogin(), TemplatesController.createProjectFromV1Template diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index e6b2692f7c..2e766ac178 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -48,6 +48,7 @@ MetaController = require('./Features/Metadata/MetaController') TokenAccessController = require('./Features/TokenAccess/TokenAccessController') Features = require('./infrastructure/Features') LinkedFilesRouter = require './Features/LinkedFiles/LinkedFilesRouter' +TemplatesRouter = require './Features/Templates/TemplatesRouter' logger = require("logger-sharelatex") _ = require("underscore") @@ -80,10 +81,10 @@ module.exports = class Router ContactRouter.apply(webRouter, privateApiRouter) AnalyticsRouter.apply(webRouter, privateApiRouter, publicApiRouter) LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter) + TemplatesRouter.apply(webRouter) Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter) - if Settings.enableSubscriptions webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalController.bonus diff --git a/services/web/app/views/project/editor/new_from_template.pug b/services/web/app/views/project/editor/new_from_template.pug new file mode 100644 index 0000000000..6dc27a4241 --- /dev/null +++ b/services/web/app/views/project/editor/new_from_template.pug @@ -0,0 +1,26 @@ +extends ../../layout + +block content + script. + $(document).ready(function(){ + $('#create_form').submit(); + }); + + .editor.full-size + .loading-screen() + .loading-screen-brand-container + .loading-screen-brand( + style="height: 20%;" + ) + + h3.loading-screen-label() #{translate("Opening template")} + span.loading-screen-ellip . + span.loading-screen-ellip . + span.loading-screen-ellip . + + form(id='create_form' method='POST' action='/project/new/template/') + input(type="hidden", name="_csrf", value=csrfToken) + input(type="hidden" name="templateId" value=templateId) + input(type="hidden" name="templateVersionId" value=templateVersionId) + input(type="hidden" name="templateName" value=name) + input(type="hidden" name="compiler" value=compiler) From d47e84536716ead511bef4cbe62f4f888625fb36 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Thu, 31 May 2018 12:15:42 +0100 Subject: [PATCH 72/76] add v1 template tests --- .../Templates/TemplatesController.coffee | 2 +- .../Features/Templates/TemplatesMiddlewear | 8 ++ .../Templates/TemplatesControllerTests.coffee | 76 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 services/web/app/coffee/Features/Templates/TemplatesMiddlewear create mode 100644 services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee diff --git a/services/web/app/coffee/Features/Templates/TemplatesController.coffee b/services/web/app/coffee/Features/Templates/TemplatesController.coffee index 0d687a7f06..fce6c9502c 100644 --- a/services/web/app/coffee/Features/Templates/TemplatesController.coffee +++ b/services/web/app/coffee/Features/Templates/TemplatesController.coffee @@ -1,7 +1,7 @@ path = require('path') Project = require('../../../js/models/Project').Project ProjectUploadManager = require('../../../js/Features/Uploads/ProjectUploadManager') -ProjectOptionsHandler = require("../../../js/Features/Project/ProjectOptionsHandler") +ProjectOptionsHandler = require('../../../js/Features/Project/ProjectOptionsHandler') AuthenticationController = require('../../../js/Features/Authentication/AuthenticationController') settings = require('settings-sharelatex') fs = require('fs') diff --git a/services/web/app/coffee/Features/Templates/TemplatesMiddlewear b/services/web/app/coffee/Features/Templates/TemplatesMiddlewear new file mode 100644 index 0000000000..300721c889 --- /dev/null +++ b/services/web/app/coffee/Features/Templates/TemplatesMiddlewear @@ -0,0 +1,8 @@ +settings = require("settings-sharelatex") +logger = require("logger-sharelatex") + +module.exports = + saveTemplateDataInSession: (req, res, next)-> + if req.query.templateName + req.session.templateData = req.query + next() diff --git a/services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee b/services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee new file mode 100644 index 0000000000..5cf52eca39 --- /dev/null +++ b/services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee @@ -0,0 +1,76 @@ +should = require('chai').should() +SandboxedModule = require('sandboxed-module') +assert = require('assert') +path = require('path') +sinon = require('sinon') +modulePath = '../../../../app/js/Features/Templates/TemplatesController' + + +describe 'TemplatesController', -> + + project_id = "213432" + + beforeEach -> + @request = sinon.stub() + @request.returns { + pipe:-> + on:-> + } + @fs = { + unlink : sinon.stub() + createWriteStream : sinon.stub().returns(on:(_, cb)->cb()) + } + @ProjectUploadManager = {createProjectFromZipArchive : sinon.stub().callsArgWith(3, null, {_id:project_id})} + @dumpFolder = "dump/path" + @ProjectOptionsHandler = {setCompiler:sinon.stub().callsArgWith(2)} + @uuid = "1234" + @ProjectDetailsHandler = + getProjectDescription:sinon.stub() + @Project = + update: sinon.stub().callsArgWith(3, null) + @controller = SandboxedModule.require modulePath, requires: + '../../../js/Features/Uploads/ProjectUploadManager':@ProjectUploadManager + '../../../js/Features/Project/ProjectOptionsHandler':@ProjectOptionsHandler + '../../../js/Features/Authentication/AuthenticationController': @AuthenticationController = {getLoggedInUserId: sinon.stub()} + './TemplatesPublisher':@TemplatesPublisher + "logger-sharelatex": + log:-> + err:-> + "settings-sharelatex": + path: + dumpFolder:@dumpFolder + siteUrl: @siteUrl = "http://localhost:3000" + apis: + v1: + url: @v1Url="http://overleaf.com" + user: "sharelatex" + pass: "password" + overleaf: + host: @v1Url + "uuid":v4:=>@uuid + "request": @request + "fs":@fs + "../../../../app/js/models/Project": {Project: @Project} + @zipUrl = "%2Ftemplates%2F52fb86a81ae1e566597a25f6%2Fv%2F4%2Fzip&templateName=Moderncv%20Banking&compiler=pdflatex" + @templateName = "project name here" + @user_id = "1234" + @req = + session: + user: _id:@user_id + templateData: + zipUrl: @zipUrl + templateName: @templateName + @redirect = {} + @AuthenticationController.getLoggedInUserId.returns(@user_id) + + describe 'v1Templates', -> + + it "should fetch zip from v1 based on template id", (done)-> + @templateVersionId = 15 + @req.body = {templateVersionId: @templateVersionId} + + redirect = => + @request.calledWith("#{@v1Url}/api/v1/sharelatex/templates/#{@templateVersionId}").should.equal true + done() + res = redirect:redirect + @controller.createProjectFromV1Template @req, res From f5367985c3153d97214d9873e914451b0dab3bd2 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 31 May 2018 13:44:37 +0100 Subject: [PATCH 73/76] Don't allow read-only users to restore --- services/web/app/coffee/router.coffee | 2 +- services/web/app/views/project/editor/history/diffPanelV1.pug | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index e6b2692f7c..51c51cade2 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -201,7 +201,7 @@ module.exports = class Router webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi webRouter.get "/project/:Project_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails - webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi + webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2 privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory diff --git a/services/web/app/views/project/editor/history/diffPanelV1.pug b/services/web/app/views/project/editor/history/diffPanelV1.pug index 1720f48b59..30588adf46 100644 --- a/services/web/app/views/project/editor/history/diffPanelV1.pug +++ b/services/web/app/views/project/editor/history/diffPanelV1.pug @@ -13,7 +13,7 @@ }" ) | in {{history.diff.pathname}} - .toolbar-right + .toolbar-right(ng-if="permissions.write") a.btn.btn-danger.btn-sm( href, ng-click="openRestoreDiffModal()" From c4f3a12ce5c75db00539465e75b25b320f9e6f85 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 31 May 2018 14:45:43 +0100 Subject: [PATCH 74/76] add missing locking to copyFileFromExistingProject --- .../Project/ProjectEntityUpdateHandler.coffee | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index 27ead91841..22034200f5 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -45,37 +45,40 @@ wrapWithLock = (methodWithoutLock) -> methodWithLock module.exports = ProjectEntityUpdateHandler = self = - # this doesn't need any locking because it's only called by ProjectDuplicator - copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)-> - project_id = project._id - projectHistoryId = project.overleaf?.history?.id - logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project" - return callback(err) if err? - ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id)=> - if !origonalFileRef? - logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null" - return callback() - # convert any invalid characters in original file to '_' - fileRef = new File name : SafePath.clean(origonalFileRef.name) - FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)-> - if err? - logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3" - return callback(err) - ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result)=> - if err? - logger.err { err, project_id, folder_id }, "error putting element as part of copy" - return callback(err) - TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) -> + copyFileFromExistingProjectWithProject: wrapWithLock + beforeLock: (next) -> + (project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)-> + project_id = project._id + logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project" + ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id) -> + if !origonalFileRef? + logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null" + return callback() + # convert any invalid characters in original file to '_' + fileRef = new File name : SafePath.clean(origonalFileRef.name) + FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)-> if err? - logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker" - newFiles = [ - file: fileRef - path: result?.path?.fileSystem - url: fileStoreUrl - ] - DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles}, (error) -> - return callback(error) if error? - callback null, fileRef, folder_id + logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3" + return callback(err) + next(project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback) + withLock: (project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)-> + project_id = project._id + projectHistoryId = project.overleaf?.history?.id + ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result, newProject) -> + if err? + logger.err { err, project_id, folder_id }, "error putting element as part of copy" + return callback(err) + TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) -> + if err? + logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker" + newFiles = [ + file: fileRef + path: result?.path?.fileSystem + url: fileStoreUrl + ] + DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles, newProject}, (error) -> + return callback(error) if error? + callback null, fileRef, folder_id updateDocLines: (project_id, doc_id, lines, version, ranges, callback = (error) ->)-> ProjectGetter.getProjectWithoutDocLines project_id, (err, project)-> From 063187b5fc5f0e8a6171f5f2780beb53c46d97e1 Mon Sep 17 00:00:00 2001 From: hugh-obrien Date: Thu, 31 May 2018 17:03:41 +0100 Subject: [PATCH 75/76] add function to check for existance of folders --- .../web/public/coffee/ide/file-tree/FileTreeManager.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee index d7a428ec80..8d717e09b8 100644 --- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee +++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee @@ -335,6 +335,11 @@ define [ return null + projectContainsFolder: () -> + for entity in @$scope.rootFolder.children + return true if entity.type == 'folder' + return false + existsInThisFolder: (folder, name) -> for entity in folder?.children or [] return true if entity.name is name From 7898a1decad4be7cf1f2e44d4e0124a464f17993 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 4 Jun 2018 10:45:23 +0100 Subject: [PATCH 76/76] Fix missed snake_case to camelCase, causing bug where projects couldn't be restored --- .../web/public/coffee/main/project-list/project-list.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index c2b251bd65..6696243905 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -425,7 +425,7 @@ define [ for projectId in projectIds queuedHttp { method: "POST" - url: "/project/#{project_id}/restore" + url: "/project/#{projectId}/restore" headers: "X-CSRF-Token": window.csrfToken }