diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index fade8e0782..0ac316e922 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -119,6 +119,7 @@ script(type='text/ng-template', id='entityListItemTemplate') ng-blur="finishRenaming()", select-name-when="entity.renaming", on-enter="finishRenaming()" + valid-file ) span.dropdown.entity-menu-toggle( @@ -207,6 +208,7 @@ script(type='text/ng-template', id='entityListItemTemplate') ng-blur="finishRenaming()", select-name-when="entity.renaming", on-enter="finishRenaming()" + valid-file ) span.dropdown.entity-menu-toggle( @@ -316,10 +318,10 @@ script(type='text/ng-template', id='newDocModalTemplate') ng-model="inputs.name", on-enter="create()", select-name-on="open", - ng-pattern="validFileRegex", + valid-file, name="name" ) - .text-danger.row-spaced-small(ng-show="newDocForm.name.$error.pattern") + .text-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile") | #{translate('files_cannot_include_invalid_characters')} .modal-footer button.btn.btn-default( @@ -347,10 +349,10 @@ script(type='text/ng-template', id='newFolderModalTemplate') ng-model="inputs.name", on-enter="create()", select-name-on="open", - ng-pattern="validFileRegex", + valid-file, name="name" ) - .text-danger.row-spaced-small(ng-show="newFolderForm.name.$error.pattern") + .text-danger.row-spaced-small(ng-show="newFolderForm.name.$error.validFile") | #{translate('files_cannot_include_invalid_characters')} .modal-footer button.btn.btn-default( diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 6f49b4bce8..078202655f 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -20,6 +20,7 @@ define [ "ide/hotkeys/index" "ide/wordcount/index" "ide/directives/layout" + "ide/directives/validFile" "ide/services/ide" "__IDE_CLIENTSIDE_INCLUDES__" "analytics/AbTestingManager" diff --git a/services/web/public/coffee/ide/directives/SafePath.coffee b/services/web/public/coffee/ide/directives/SafePath.coffee new file mode 100644 index 0000000000..f5b49cdff7 --- /dev/null +++ b/services/web/public/coffee/ide/directives/SafePath.coffee @@ -0,0 +1,47 @@ +# This file is shared between the frontend and server code of web, so that +# filename validation is the same in both implementations. +# Both copies must be kept in sync: +# app/coffee/Features/Project/SafePath.coffee +# public/coffee/ide/directives/SafePath.coffee + +load = () -> + BADCHAR_RX = /// + [ + \/ # no slashes + \* # no asterisk + \u0000-\u001F # no control characters (0-31) + \u007F # no delete + \u0080-\u009F # no unicode control characters (C1) + \uD800-\uDFFF # no unicode surrogate characters + ] + ///g + + BADFILE_RX = /// + (^\.$) # reject . as a filename + | (^\.\.$) # reject .. as a filename + | (^\s+) # reject leading space + | (\s+$) # reject trailing space + ///g + + MAX_PATH = 1024 # Maximum path length, in characters. This is fairly arbitrary. + + SafePath = + clean: (filename) -> + filename = filename.replace BADCHAR_RX, '_' + # for BADFILE_RX replace any matches with an equal number of underscores + filename = filename.replace BADFILE_RX, (match) -> + return new Array(match.length + 1).join("_") + return filename + + isCleanFilename: (filename) -> + return SafePath.isAllowedLength(filename) && + not filename.match(BADCHAR_RX) && + not filename.match(BADFILE_RX) + + isAllowedLength: (pathname) -> + return pathname.length > 0 && pathname.length <= MAX_PATH + +if define? + define [], load +else + module.exports = load() \ No newline at end of file diff --git a/services/web/public/coffee/ide/directives/validFile.coffee b/services/web/public/coffee/ide/directives/validFile.coffee new file mode 100644 index 0000000000..b4ff0eab74 --- /dev/null +++ b/services/web/public/coffee/ide/directives/validFile.coffee @@ -0,0 +1,12 @@ +define [ + "base" + "ide/directives/SafePath" +], (App, SafePath) -> + + App.directive "validFile", () -> + return { + require: 'ngModel' + link: (scope, element, attrs, ngModelCtrl) -> + ngModelCtrl.$validators.validFile = (filename) -> + return SafePath.isCleanFilename filename + } 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 aee631f334..ba9eb8a81e 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -44,8 +44,6 @@ define [ App.controller "NewDocModalController", [ "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", ($scope, ide, $modalInstance, $timeout, parent_folder) -> - $scope.validFileRegex = ide.validFileRegex - $scope.inputs = name: "name.tex" $scope.state = @@ -78,8 +76,6 @@ define [ App.controller "NewFolderModalController", [ "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", ($scope, ide, $modalInstance, $timeout, parent_folder) -> - $scope.validFileRegex = ide.validFileRegex - $scope.inputs = name: "name" $scope.state = diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee index 55bf16a172..735d065cd8 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee @@ -32,8 +32,9 @@ define [ return if !$scope.entity.renaming name = $scope.inputs.name - - if !name.match(new RegExp(ide.validFileRegex)) + + # validator will set name to undefined for invalid filenames + if !name? # Showing the modal blurs the rename box which calls us again # so track this with the invalidModalShowing flag return if invalidModalShowing