From a8c29644d41323f7beba5b904b42bfaebb8a7e03 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 26 Jul 2017 08:35:08 +0200 Subject: [PATCH] U4-10023 - multiple start nodes --- src/Umbraco.Core/Models/UserExtensions.cs | 66 ++++++++++++------- src/Umbraco.Core/Services/EntityService.cs | 8 ++- .../Models/UserExtensionsTests.cs | 49 ++++++++------ .../linkpicker/linkpicker.controller.js | 2 +- .../mediaPicker/mediapicker.controller.js | 2 + .../overlays/mediaPicker/mediapicker.html | 2 +- .../grid/editors/media.controller.js | 2 +- .../grid/editors/rte.controller.js | 2 +- .../mediapicker/mediapicker.controller.js | 4 +- .../propertyeditors/rte/rte.controller.js | 2 +- src/Umbraco.Web/Editors/EntityController.cs | 22 +++++-- .../Trees/ContentTreeController.cs | 8 +-- .../Trees/ContentTreeControllerBase.cs | 17 +++-- src/Umbraco.Web/Trees/MediaTreeController.cs | 7 +- 14 files changed, 119 insertions(+), 74 deletions(-) diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 223fb37be4..b087d6cee1 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -141,31 +141,23 @@ namespace Umbraco.Core.Models { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", "path"); - var formattedPath = "," + path + ","; - var formattedRecycleBinId = "," + recycleBinId.ToInvariantString() + ","; + // check for no access + if (startNodeIds.Length == 0) + return false; - //check for root path access - //TODO: This logic may change - if (startNodeIds.Length == 0 || startNodeIds.Contains(Constants.System.Root)) + // check for root access + if (startNodeIds.Contains(Constants.System.Root)) return true; - //only users with root access have access to the recycle bin so if the above check didn't pass than access is denied - if (formattedPath.Contains(formattedRecycleBinId)) - { + var formattedPath = "," + path + ","; + + // only users with root access have access to the recycle bin, + // if the above check didn't pass then access is denied + if (formattedPath.Contains("," + recycleBinId + ",")) return false; - } - //check for normal paths - foreach (var startNodeId in startNodeIds) - { - var formattedStartNodeId = "," + startNodeId.ToInvariantString() + ","; - - var hasAccess = formattedPath.Contains(formattedStartNodeId); - if (hasAccess) - return true; - } - - return false; + // check for a start node in the path + return startNodeIds.Any(x => formattedPath.Contains("," + x + ",")); } /// @@ -181,6 +173,7 @@ namespace Umbraco.Core.Models return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.AdminGroupAlias); } + // calc. start nodes, combining groups' and user's, and excluding what's in the bin public static int[] CalculateContentStartNodeIds(this IUser user, IEntityService entityService) { const string cacheKey = "AllContentStartNodes"; @@ -195,6 +188,7 @@ namespace Umbraco.Core.Models return vals; } + // calc. start nodes, combining groups' and user's, and excluding what's in the bin public static int[] CalculateMediaStartNodeIds(this IUser user, IEntityService entityService) { const string cacheKey = "AllMediaStartNodes"; @@ -239,7 +233,23 @@ namespace Umbraco.Core.Models return test.StartsWith(path) && test.Length > path.Length && test[path.Length] == ','; } - //TODO: Unit test this + private static string GetBinPath(UmbracoObjectTypes objectType) + { + var binPath = Constants.System.Root + ","; + switch (objectType) + { + case UmbracoObjectTypes.Document: + binPath += Constants.System.RecycleBinContent; + break; + case UmbracoObjectTypes.Media: + binPath += Constants.System.RecycleBinMedia; + break; + default: + throw new ArgumentOutOfRangeException("objectType"); + } + return binPath; + } + internal static int[] CombineStartNodes(UmbracoObjectTypes objectType, int[] groupSn, int[] userSn, IEntityService entityService) { // assume groupSn and userSn each don't contain duplicates @@ -247,13 +257,17 @@ namespace Umbraco.Core.Models var asn = groupSn.Concat(userSn).Distinct().ToArray(); var paths = entityService.GetAllPaths(objectType, asn).ToDictionary(x => x.Id, x => x.Path); - paths[-1] = "-1"; // entityService does not get that one + paths[Constants.System.Root] = Constants.System.Root.ToString(); // entityService does not get that one + + var binPath = GetBinPath(objectType); var lsn = new List(); foreach (var sn in groupSn) { string snp; - if (paths.TryGetValue(sn, out snp) == false) continue; // ignore + if (paths.TryGetValue(sn, out snp) == false) continue; // ignore rogue node (no path) + + if (StartsWithPath(snp, binPath)) continue; // ignore bin if (lsn.Any(x => StartsWithPath(snp, paths[x]))) continue; // skip if something above this sn lsn.RemoveAll(x => StartsWithPath(paths[x], snp)); // remove anything below this sn @@ -263,10 +277,12 @@ namespace Umbraco.Core.Models var usn = new List(); foreach (var sn in userSn) { - if (groupSn.Contains(sn)) continue; + if (groupSn.Contains(sn)) continue; // ignore, already there string snp; - if (paths.TryGetValue(sn, out snp) == false) continue; // ignore + if (paths.TryGetValue(sn, out snp) == false) continue; // ignore rogue node (no path) + + if (StartsWithPath(snp, binPath)) continue; // ignore bin if (usn.Any(x => StartsWithPath(paths[x], snp))) continue; // skip if something below this sn usn.RemoveAll(x => StartsWithPath(snp, paths[x])); // remove anything above this sn diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index a47d552ba5..6e4b5ef17c 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -370,13 +370,19 @@ namespace Umbraco.Core.Services public IEnumerable GetPagedDescendants(IEnumerable ids, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") { + totalRecords = 0; + + var idsA = ids.ToArray(); + if (idsA.Length == 0) + return Enumerable.Empty(); + var objectTypeId = umbracoObjectType.GetGuid(); + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateEntityRepository(uow); var query = Query.Builder; - var idsA = ids.ToArray(); if (idsA.All(x => x != Constants.System.Root)) { var clauses = new List>>(); diff --git a/src/Umbraco.Tests/Models/UserExtensionsTests.cs b/src/Umbraco.Tests/Models/UserExtensionsTests.cs index 2b1f6c9e37..1e6c7664de 100644 --- a/src/Umbraco.Tests/Models/UserExtensionsTests.cs +++ b/src/Umbraco.Tests/Models/UserExtensionsTests.cs @@ -14,12 +14,15 @@ namespace Umbraco.Tests.Models [TestFixture] public class UserExtensionsTests { - [TestCase(2, "-1,1,2", "-1,1,2,3,4,5", true)] - [TestCase(6, "-1,1,2,3,4,5,6", "-1,1,2,3,4,5", false)] - [TestCase(-1, "-1", "-1,1,2,3,4,5", true)] - [TestCase(5, "-1,1,2,3,4,5", "-1,1,2,3,4,5", true)] - [TestCase(-1, "-1", "-1,-20,1,2,3,4,5", true)] - [TestCase(1, "-1,-20,1", "-1,-20,1,2,3,4,5", false)] + [TestCase(-1, "-1", "-1,1,2,3,4,5", true)] // below root start node + [TestCase(2, "-1,1,2", "-1,1,2,3,4,5", true)] // below start node + [TestCase(5, "-1,1,2,3,4,5", "-1,1,2,3,4,5", true)] // at start node + + [TestCase(6, "-1,1,2,3,4,5,6", "-1,1,2,3,4,5", false)] // above start node + + [TestCase(-1, "-1", "-1,-20,1,2,3,4,5", true)] // below root start node, bin + [TestCase(1, "-1,-20,1", "-1,-20,1,2,3,4,5", false)] // below bin start node + public void Determines_Path_Based_Access_To_Content(int startNodeId, string startNodePath, string contentPath, bool outcome) { var userMock = new Mock(); @@ -27,15 +30,12 @@ namespace Umbraco.Tests.Models var user = userMock.Object; var content = Mock.Of(c => c.Path == contentPath && c.Id == 5); - var entityServiceMock = new Mock(); - entityServiceMock.Setup(x => x.GetAll(It.IsAny(), It.IsAny())) - .Returns(new[] - { - Mock.Of(entity => entity.Id == startNodeId && entity.Path == startNodePath) - }); - var entityService = entityServiceMock.Object; + var esmock = new Mock(); + esmock + .Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) + .Returns((type, ids) => new [] { new EntityPath { Id = startNodeId, Path = startNodePath } }); - Assert.AreEqual(outcome, user.HasPathAccess(content, entityService)); + Assert.AreEqual(outcome, user.HasPathAccess(content, esmock.Object)); } [TestCase("", "1", "1")] // single user start, top level @@ -52,6 +52,9 @@ namespace Umbraco.Tests.Models [TestCase("3", "2,5", "2,5")] // user and group start, restrict [TestCase("3", "2,1", "2,1")] // user and group start, expand + [TestCase("3,8", "2,6", "3,2")] // exclude bin + [TestCase("", "6", "")] // exclude bin + public void CombineStartNodes(string groupSn, string userSn, string expected) { // 1 @@ -59,15 +62,23 @@ namespace Umbraco.Tests.Models // 5 // 2 // 4 + // bin + // 6 + // 7 + // 8 var paths = new Dictionary { - { 1, "-1, 1" }, - { 2, "-1, 2" }, - { 3, "-1, 1, 3" }, - { 4, "-1, 2, 4" }, - { 5, "-1, 1, 3, 5" }, + { 1, "-1,1" }, + { 2, "-1,2" }, + { 3, "-1,1,3" }, + { 4, "-1,2,4" }, + { 5, "-1,1,3,5" }, + { 6, "-1,-20,6" }, + { 7, "-1,-20,7" }, + { 8, "-1,-20,7,8" }, }; + var esmock = new Mock(); esmock .Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js index 9c5a2b888c..d58234b493 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -91,7 +91,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", userService.getCurrentUser().then(function (userData) { $scope.mediaPickerOverlay = { view: "mediapicker", - startNodeId: userData.startMediaIds.length == 0 ? -1 : userData.startMediaIds[0], + startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], show: true, submit: function(model) { var media = model.selectedImages[0]; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index fb38581eac..59118b9b8f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -17,6 +17,8 @@ angular.module("umbraco") $scope.cropSize = dialogOptions.cropSize; $scope.lastOpenedNode = localStorageService.get("umbLastOpenedMediaNodeId"); + $scope.canFolderInput = true; // FIXME only if ... ? + var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; var allowedUploadFiles = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); if ($scope.onlyImages) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html index ab745f0f75..4fd8bff7d9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html @@ -48,7 +48,7 @@ / -
  • +
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index 57f7d06f80..062762cf17 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -4,7 +4,7 @@ angular.module("umbraco") if (!$scope.model.config.startNodeId) { userService.getCurrentUser().then(function (userData) { - $scope.model.config.startNodeId = userData.startMediaIds.length === 0 ? -1 : userData.startMediaIds[0]; + $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index 6ec9e74fa4..99c3fd80ff 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -28,7 +28,7 @@ currentTarget: currentTarget, onlyImages: true, showDetails: true, - startNodeId: userData.startMediaIds.length === 0 ? -1 : userData.startMediaIds[0], + startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], view: "mediapicker", show: true, submit: function(model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index d09c89c445..6cde701c1b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -10,7 +10,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl if (!$scope.model.config.startNodeId) { userService.getCurrentUser().then(function (userData) { - $scope.model.config.startNodeId = userData.startMediaIds.length === 0 ? -1 : userData.startMediaIds[0]; + $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; }); } @@ -45,7 +45,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl $scope.ids.push(media.udi); } else { - $scope.ids.push(media.id); + $scope.ids.push(media.id); } } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index 485b674594..b050e22f2d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -286,7 +286,7 @@ angular.module("umbraco") onlyImages: true, showDetails: true, disableFolderSelect: true, - startNodeId: userData.startMediaIds.length === 0 ? -1 : userData.startMediaIds[0], + startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], view: "mediapicker", show: true, submit: function(model) { diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index b22ce81784..e3e9abfaf7 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -547,6 +547,8 @@ namespace Umbraco.Web.Editors if (id == Constants.System.Root) { + // root is special: we reduce it to start nodes + int[] aids = null; switch (type) { @@ -558,9 +560,9 @@ namespace Umbraco.Web.Editors break; } - entities = aids != null && aids.Length > 0 - ? Services.EntityService.GetPagedDescendants(aids, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter) - : Services.EntityService.GetPagedDescendantsFromRoot(objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter, includeTrashed: false); + entities = aids == null || aids.Contains(Constants.System.Root) + ? Services.EntityService.GetPagedDescendantsFromRoot(objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter, includeTrashed: false) + : Services.EntityService.GetPagedDescendants(aids, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter); } else { @@ -676,6 +678,8 @@ namespace Umbraco.Web.Editors string type; var searcher = Constants.Examine.InternalSearcher; var fields = new[] { "id", "__NodeId" }; + + totalFound = 0; //TODO: WE should really just allow passing in a lucene raw query switch (entityType) @@ -694,15 +698,21 @@ namespace Umbraco.Web.Editors case UmbracoEntityTypes.Media: type = "media"; + var mediaStartNodeIds = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); + if (mediaStartNodeIds.Length == 0) return Enumerable.Empty(); + AddExamineSearchFrom(searchFrom, sb); - AddExamineUserStartNode(Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService), sb); + AddExamineUserStartNode(mediaStartNodeIds, sb); break; case UmbracoEntityTypes.Document: type = "content"; + var contentStartNodeIds = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); + if (contentStartNodeIds.Length == 0) return Enumerable.Empty(); + AddExamineSearchFrom(searchFrom, sb); - AddExamineUserStartNode(Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService), sb); + AddExamineUserStartNode(contentStartNodeIds, sb); break; default: @@ -957,7 +967,7 @@ namespace Umbraco.Web.Editors break; } - if (aids != null && aids.Length > 0) + if (aids != null) { var lids = new List(); var ok = false; diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 2dde698a28..c3eab40300 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -43,8 +43,8 @@ namespace Umbraco.Web.Trees { var node = base.CreateRootNode(queryStrings); - //if the user's start node is not default, then ensure the root doesn't have a menu - if (UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) + // if the user's start node is not default, then ensure the root doesn't have a menu + if (UserStartNodes.Contains(Constants.System.Root) == false) { node.MenuUrl = ""; } @@ -123,8 +123,8 @@ namespace Umbraco.Web.Trees { var menu = new MenuItemCollection(); - //if the user's start node is not the root then ensure the root menu is empty/doesn't exist - if (UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) + // if the user's start node is not the root then ensure the root menu is empty/doesn't exist + if (UserStartNodes.Contains(Constants.System.Root) == false) { return menu; } diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index f8e8bec293..fa18e703bd 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -105,7 +105,7 @@ namespace Umbraco.Web.Trees // Therefore, in the latter case, we want to change the id to -1 since we want to render the current user's root node // and the GetChildEntities method will take care of rendering the correct root node. // If it is in dialog mode, then we don't need to change anything and the children will just render as per normal. - if (IsDialog(queryStrings) == false && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) + if (IsDialog(queryStrings) == false && UserStartNodes.Contains(Constants.System.Root) == false) { id = Constants.System.Root.ToString(CultureInfo.InvariantCulture); } @@ -128,7 +128,6 @@ namespace Umbraco.Web.Trees // look up from GUID if it's not an integer if (int.TryParse(id, out iid) == false) { - var idEntity = GetEntityFromId(id); if (idEntity == null) { @@ -137,13 +136,13 @@ namespace Umbraco.Web.Trees iid = idEntity.Id; } - - //if a request is made for the root node data but the user's start node is not the default, then - // we need to return their start nodes - if (iid == Constants.System.Root && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) + // if a request is made for the root node but user has no access to + // root node, return start nodes instead + if (iid == Constants.System.Root && UserStartNodes.Contains(Constants.System.Root) == false) { - var nodes = Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes); - return nodes; + return UserStartNodes.Length > 0 + ? Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes) + : Enumerable.Empty(); } return Services.EntityService.GetChildren(iid, UmbracoObjectType).ToArray(); @@ -169,7 +168,7 @@ namespace Umbraco.Web.Trees protected sealed override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { //check if we're rendering the root - if (id == Constants.System.Root.ToInvariantString() && (UserStartNodes.Length == 0 || UserStartNodes.Contains(Constants.System.Root))) + if (id == Constants.System.Root.ToInvariantString() && UserStartNodes.Contains(Constants.System.Root)) { var altStartId = string.Empty; diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index f3d583f1e3..82925cf445 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -34,8 +34,9 @@ namespace Umbraco.Web.Trees protected override TreeNode CreateRootNode(FormDataCollection queryStrings) { var node = base.CreateRootNode(queryStrings); - //if the user's start node is not default, then ensure the root doesn't have a menu - if (UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) + + // if the user's start node is not default, then ensure the root doesn't have a menu + if (UserStartNodes.Contains(Constants.System.Root) == false) { node.MenuUrl = ""; } @@ -101,7 +102,7 @@ namespace Umbraco.Web.Trees if (id == Constants.System.Root.ToInvariantString()) { //if the user's start node is not the root then ensure the root menu is empty/doesn't exist - if (UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) + if (UserStartNodes.Contains(Constants.System.Root) == false) { return menu; }