From ecc761a232d7ba2f2d75c3d61ca3090b3889a794 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 31 Aug 2017 22:19:01 +1000 Subject: [PATCH 01/14] Change EntityService GetDescendents to lookup the item path to use in the LIKE query to be a 'prefix' query not a 'contains' query which will be much better for SQL --- src/Umbraco.Core/Services/EntityService.cs | 47 ++++++++++++++++++--- src/Umbraco.Core/Services/IEntityService.cs | 7 +++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 6e4b5ef17c..de7274433b 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -349,10 +349,20 @@ namespace Umbraco.Core.Services { var repository = RepositoryFactory.CreateEntityRepository(uow); + //lookup the path so we can use it in the prefix query below + var itemPath = repository.GetAllPaths(objectTypeId, id).ToArray(); + if (itemPath.Length == 0) + { + totalRecords = 0; + return Enumerable.Empty(); + } + var query = Query.Builder; //if the id is System Root, then just get all if (id != Constants.System.Root) - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + { + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", itemPath[0].Path), TextColumnType.NVarchar)); + } IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) @@ -381,15 +391,25 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateEntityRepository(uow); - + var query = Query.Builder; if (idsA.All(x => x != Constants.System.Root)) { + //lookup the paths so we can use it in the prefix query below + var itemPaths = repository.GetAllPaths(objectTypeId, idsA).ToArray(); + if (itemPaths.Length == 0) + { + totalRecords = 0; + return Enumerable.Empty(); + } + var clauses = new List>>(); foreach (var id in idsA) { + var path = itemPaths.FirstOrDefault(x => x.Id == id); + if (path == null) continue; var qid = id; - clauses.Add(x => x.Path.SqlContains(string.Format(",{0},", qid), TextColumnType.NVarchar) || x.Path.SqlEndsWith(string.Format(",{0}", qid), TextColumnType.NVarchar)); + clauses.Add(x => x.Path.SqlStartsWith(string.Format("{0},", path), TextColumnType.NVarchar) || x.Path.SqlEndsWith(string.Format(",{0}", qid), TextColumnType.NVarchar)); } query.WhereAny(clauses); } @@ -443,6 +463,21 @@ namespace Umbraco.Core.Services } } + /// + public virtual IEnumerable GetAncestors(int id) + { + using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) + { + var repository = RepositoryFactory.CreateEntityRepository(uow); + var entity = repository.Get(id); + var pathMatch = entity.Path + ","; + var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != id); + + var entities = repository.GetByQuery(query); + return entities; + } + } + /// /// Gets a collection of descendents by the parents Id /// @@ -601,13 +636,13 @@ namespace Umbraco.Core.Services return repository.GetAllPaths(objectTypeId, keys); } } - + /// - /// Gets a collection of + /// Gets a collection of /// /// Guid id of the UmbracoObjectType /// - /// An enumerable list of objects + /// An enumerable list of objects public virtual IEnumerable GetAll(Guid objectTypeId, params int[] ids) { var umbracoObjectType = UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 3136a49bf3..21bd8a3c6e 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -203,6 +203,13 @@ namespace Umbraco.Core.Services /// An enumerable list of objects IEnumerable GetDescendents(int id); + /// + /// Gets a collection of ancestors by the parents Id + /// + /// Id of entity to retrieve ancestors for + /// An enumerable list of objects + IEnumerable GetAncestors(int id); + /// /// Gets a collection of descendents by the parents Id /// From 51e9b9885757cc75f69cc24112ab3a86b7856925 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 31 Aug 2017 22:54:30 +1000 Subject: [PATCH 02/14] Updates Content and Media services GetAncestors to use a prefix query intead of contains, fixes tests --- src/Umbraco.Core/Services/ContentService.cs | 25 ++++++++-- src/Umbraco.Core/Services/EntityService.cs | 55 +++++++++------------ src/Umbraco.Core/Services/IEntityService.cs | 7 --- src/Umbraco.Core/Services/MediaService.cs | 9 +++- 4 files changed, 53 insertions(+), 43 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 42ea80a717..84651f5a6f 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -676,7 +676,17 @@ namespace Umbraco.Core.Services // get query - if the id is System Root, then just get all var query = Query.Builder; if (id != Constants.System.Root) - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + { + var entityRepository = RepositoryFactory.CreateEntityRepository(uow); + var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray(); + if (contentPath.Length == 0) + { + totalChildren = 0; + return Enumerable.Empty(); + } + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0]), TextColumnType.NVarchar)); + } + // get filter IQuery filterQuery = null; @@ -711,7 +721,16 @@ namespace Umbraco.Core.Services // get query - if the id is System Root, then just get all var query = Query.Builder; if (id != Constants.System.Root) - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + { + var entityRepository = RepositoryFactory.CreateEntityRepository(uow); + var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray(); + if (contentPath.Length == 0) + { + totalChildren = 0; + return Enumerable.Empty(); + } + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0]), TextColumnType.NVarchar)); + } return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); } @@ -1645,7 +1664,7 @@ namespace Umbraco.Core.Services { using (new WriteLock(Locker)) { - var nodeObjectType = new Guid(Constants.ObjectTypes.Document); + var nodeObjectType = Constants.ObjectTypes.DocumentGuid; using (var uow = UowProvider.GetUnitOfWork()) { diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index de7274433b..ae4f1bbf72 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -348,20 +348,21 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { var repository = RepositoryFactory.CreateEntityRepository(uow); - - //lookup the path so we can use it in the prefix query below - var itemPath = repository.GetAllPaths(objectTypeId, id).ToArray(); - if (itemPath.Length == 0) - { - totalRecords = 0; - return Enumerable.Empty(); - } - + var query = Query.Builder; //if the id is System Root, then just get all if (id != Constants.System.Root) - { - query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", itemPath[0].Path), TextColumnType.NVarchar)); + { + //lookup the path so we can use it in the prefix query below + var itemPaths = repository.GetAllPaths(objectTypeId, id).ToArray(); + if (itemPaths.Length == 0) + { + totalRecords = 0; + return Enumerable.Empty(); + } + var itemPath = itemPaths[0].Path; + + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", itemPath), TextColumnType.NVarchar)); } IQuery filterQuery = null; @@ -405,11 +406,16 @@ namespace Umbraco.Core.Services var clauses = new List>>(); foreach (var id in idsA) - { - var path = itemPaths.FirstOrDefault(x => x.Id == id); - if (path == null) continue; - var qid = id; - clauses.Add(x => x.Path.SqlStartsWith(string.Format("{0},", path), TextColumnType.NVarchar) || x.Path.SqlEndsWith(string.Format(",{0}", qid), TextColumnType.NVarchar)); + { + //if the id is root then don't add any clauses + if (id != Constants.System.Root) + { + var itemPath = itemPaths.FirstOrDefault(x => x.Id == id); + if (itemPath == null) continue; + var path = itemPath.Path; + var qid = id; + clauses.Add(x => x.Path.SqlStartsWith(string.Format("{0},", path), TextColumnType.NVarchar) || x.Path.SqlEndsWith(string.Format(",{0}", qid), TextColumnType.NVarchar)); + } } query.WhereAny(clauses); } @@ -462,22 +468,7 @@ namespace Umbraco.Core.Services return contents; } } - - /// - public virtual IEnumerable GetAncestors(int id) - { - using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) - { - var repository = RepositoryFactory.CreateEntityRepository(uow); - var entity = repository.Get(id); - var pathMatch = entity.Path + ","; - var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != id); - - var entities = repository.GetByQuery(query); - return entities; - } - } - + /// /// Gets a collection of descendents by the parents Id /// diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 21bd8a3c6e..3136a49bf3 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -203,13 +203,6 @@ namespace Umbraco.Core.Services /// An enumerable list of objects IEnumerable GetDescendents(int id); - /// - /// Gets a collection of ancestors by the parents Id - /// - /// Id of entity to retrieve ancestors for - /// An enumerable list of objects - IEnumerable GetAncestors(int id); - /// /// Gets a collection of descendents by the parents Id /// diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index ee4cddb382..ecf2c750af 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -593,7 +593,14 @@ namespace Umbraco.Core.Services //if the id is System Root, then just get all if (id != Constants.System.Root) { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + var entityRepository = RepositoryFactory.CreateEntityRepository(uow); + var mediaPath = entityRepository.GetAllPaths(Constants.ObjectTypes.MediaGuid, id).ToArray(); + if (mediaPath.Length == 0) + { + totalChildren = 0; + return Enumerable.Empty(); + } + query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", mediaPath[0]), TextColumnType.NVarchar)); } IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) From 1cec38c679acc4dded07f62fd4c8751b4619122c Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 1 Sep 2017 00:32:01 +1000 Subject: [PATCH 03/14] Gets site nodes showing for users with multiple root nodes and updates UI to support showing them what they have and don't have access to, also updates the content/media tree controllers performance for when looking up entities and if the user has access to the path. --- src/Umbraco.Core/Models/UserExtensions.cs | 14 ++ .../components/tree/umbtreeitem.directive.js | 15 +- .../src/common/services/tree.service.js | 46 +++-- src/Umbraco.Web.UI.Client/src/less/tree.less | 5 + .../Trees/ContentTreeController.cs | 12 +- .../Trees/ContentTreeControllerBase.cs | 160 ++++++++++++++---- src/Umbraco.Web/Trees/MediaTreeController.cs | 12 +- 7 files changed, 192 insertions(+), 72 deletions(-) diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 96fa197269..9d13b1d9de 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -6,6 +6,7 @@ using System.Net; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.IO; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -144,6 +145,19 @@ namespace Umbraco.Core.Models return HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } + internal static bool HasPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId) + { + switch (recycleBinId) + { + case Constants.System.RecycleBinMedia: + return HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), recycleBinId); + case Constants.System.RecycleBinContent: + return HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), recycleBinId); + default: + throw new NotSupportedException("Path access is only determined on content or media"); + } + } + internal static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId) { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", "path"); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js index b32942791c..2c72441308 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js @@ -89,7 +89,7 @@ angular.module("umbraco.directives") element.find("a:first").text(node.name); - if (!node.menuUrl) { + if (!node.menuUrl || (node.metaData && node.metaData.noAccess === true)) { element.find("a.umb-options").remove(); } @@ -140,6 +140,9 @@ angular.module("umbraco.directives") about it. */ scope.options = function (n, ev) { + if (n.metaData && n.metaData.noAccess === true) { + return; + } emitEvent("treeOptionsClick", { element: element, tree: scope.tree, node: n, event: ev }); }; @@ -158,6 +161,11 @@ angular.module("umbraco.directives") return; } + if (n.metaData && n.metaData.noAccess === true) { + ev.preventDefault(); + return; + } + emitEvent("treeNodeSelect", { element: element, tree: scope.tree, node: n, event: ev }); ev.preventDefault(); }; @@ -168,7 +176,10 @@ angular.module("umbraco.directives") and emits it as a treeNodeSelect element if there is a callback object defined on the tree */ - scope.altSelect = function (n, ev) { + scope.altSelect = function (n, ev) { + if (n.metaData && n.metaData.noAccess === true) { + return; + } emitEvent("treeNodeAltSelect", { element: element, tree: scope.tree, node: n, event: ev }); }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index 5469c12c30..325aa4690f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -50,46 +50,55 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS }; for (var i = 0; i < treeNodes.length; i++) { - treeNodes[i].level = childLevel; + var treeNode = treeNodes[i]; + + treeNode.level = childLevel; //create a function to get the parent node, we could assign the parent node but // then we cannot serialize this entity because we have a cyclical reference. // Instead we just make a function to return the parentNode. - treeNodes[i].parent = funcParent; + treeNode.parent = funcParent; //set the section for each tree node - this allows us to reference this easily when accessing tree nodes - treeNodes[i].section = section; + treeNode.section = section; //if there is not route path specified, then set it automatically, //if this is a tree root node then we want to route to the section's dashboard - if (!treeNodes[i].routePath) { + if (!treeNode.routePath) { - if (treeNodes[i].metaData && treeNodes[i].metaData["treeAlias"]) { + if (treeNode.metaData && treeNode.metaData["treeAlias"]) { //this is a root node - treeNodes[i].routePath = section; + treeNode.routePath = section; } else { - var treeAlias = this.getTreeAlias(treeNodes[i]); - treeNodes[i].routePath = section + "/" + treeAlias + "/edit/" + treeNodes[i].id; + var treeAlias = this.getTreeAlias(treeNode); + treeNode.routePath = section + "/" + treeAlias + "/edit/" + treeNode.id; } } - + //now, format the icon data - if (treeNodes[i].iconIsClass === undefined || treeNodes[i].iconIsClass) { - var converted = iconHelper.convertFromLegacyTreeNodeIcon(treeNodes[i]); - treeNodes[i].cssClass = standardCssClass + " " + converted; + if (treeNode.iconIsClass === undefined || treeNode.iconIsClass) { + var converted = iconHelper.convertFromLegacyTreeNodeIcon(treeNode); + treeNode.cssClass = standardCssClass + " " + converted; if (converted.startsWith('.')) { //its legacy so add some width/height - treeNodes[i].style = "height:16px;width:16px;"; + treeNode.style = "height:16px;width:16px;"; } else { - treeNodes[i].style = ""; + treeNode.style = ""; } } else { - treeNodes[i].style = "background-image: url('" + treeNodes[i].iconFilePath + "');"; + treeNode.style = "background-image: url('" + treeNode.iconFilePath + "');"; //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this - treeNodes[i].cssClass = standardCssClass + " legacy-custom-file"; + treeNode.cssClass = standardCssClass + " legacy-custom-file"; + } + + if (treeNode.metaData && treeNode.metaData.noAccess === true) { + if (!treeNode.cssClasses) { + treeNode.cssClasses = []; + } + treeNode.cssClasses.push("no-access"); } } }, @@ -375,9 +384,10 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS } for (var i = 0; i < treeNode.children.length; i++) { - if (treeNode.children[i].children && angular.isArray(treeNode.children[i].children) && treeNode.children[i].children.length > 0) { + var child = treeNode.children[i]; + if (child.children && angular.isArray(child.children) && child.children.length > 0) { //recurse - found = this.getDescendantNode(treeNode.children[i], id); + found = this.getDescendantNode(child, id); if (found) { return found; } diff --git a/src/Umbraco.Web.UI.Client/src/less/tree.less b/src/Umbraco.Web.UI.Client/src/less/tree.less index 5de27de9f3..814c414eea 100644 --- a/src/Umbraco.Web.UI.Client/src/less/tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/tree.less @@ -377,6 +377,11 @@ div.locked:before{ bottom: 0; } +.umb-tree li div.no-access *:not(ins) { + color: @gray-7; + cursor:not-allowed; +} + // Tree context menu // ------------------------- .umb-actions { diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 9b3a6e82f8..d71726a873 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -210,17 +210,7 @@ namespace Umbraco.Web.Trees protected override bool HasPathAccess(string id, FormDataCollection queryStrings) { var entity = GetEntityFromId(id); - if (entity == null) - { - return false; - } - - var content = Services.ContentService.GetById(entity.Id); - if (content == null) - { - return false; - } - return Security.CurrentUser.HasPathAccess(content, Services.EntityService); + return HasPathAccess(entity, queryStrings); } /// diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index fa18e703bd..10f3f19c7e 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Net; using System.Net.Http; @@ -54,6 +56,25 @@ namespace Umbraco.Web.Trees #endregion protected abstract TreeNode GetSingleTreeNode(IUmbracoEntity e, string parentId, FormDataCollection queryStrings); + + /// + /// Returns a for the and + /// attaches some meta data to the node if the user doesn't have start node access to it when in dialog mode + /// + /// + /// + /// + /// + internal TreeNode GetSingleTreeNodeWithAccessCheck(IUmbracoEntity e, string parentId, FormDataCollection queryStrings) + { + var treeNode = GetSingleTreeNode(e, parentId, queryStrings); + var hasAccess = Security.CurrentUser.HasPathAccess(e, Services.EntityService, RecycleBinId); + if (hasAccess == false) + { + treeNode.AdditionalData["noAccess"] = true; + } + return treeNode; + } /// /// Returns the @@ -69,13 +90,7 @@ namespace Umbraco.Web.Trees /// Returns the user's start node for this tree /// protected abstract int[] UserStartNodes { get; } - - /// - /// Gets the tree nodes for the given id - /// - /// - /// - /// + protected virtual TreeNodeCollection PerformGetTreeNodes(string id, FormDataCollection queryStrings) { var nodes = new TreeNodeCollection(); @@ -83,9 +98,10 @@ namespace Umbraco.Web.Trees var altStartId = string.Empty; if (queryStrings.HasKey(TreeQueryStringParameters.StartNodeId)) altStartId = queryStrings.GetValue(TreeQueryStringParameters.StartNodeId); + var rootIdString = Constants.System.Root.ToString(CultureInfo.InvariantCulture); //check if a request has been made to render from a specific start node - if (string.IsNullOrEmpty(altStartId) == false && altStartId != "undefined" && altStartId != Constants.System.Root.ToString(CultureInfo.InvariantCulture)) + if (string.IsNullOrEmpty(altStartId) == false && altStartId != "undefined" && altStartId != rootIdString) { id = altStartId; @@ -111,10 +127,42 @@ namespace Umbraco.Web.Trees } } - var entities = GetChildEntities(id); - nodes.AddRange(entities.Select(entity => GetSingleTreeNode(entity, id, queryStrings)).Where(node => node != null)); + var entities = GetChildEntities(id).ToList(); + + //If we are looking up the root and there is more than one node ... + //then we want to lookup those nodes' 'site' nodes and render those so that the + //user has some context of where they are in the tree, this is generally for pickers in a dialog. + //for any node they don't have access too, we need to add some metadata + if (id == rootIdString && entities.Count > 1) + { + var siteNodeIds = new List(); + //put into array since we might modify the list + foreach (var e in entities.ToArray()) + { + var pathParts = e.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); + if (pathParts.Length < 2) + continue; // this should never happen but better to check + + int siteNodeId; + if (int.TryParse(pathParts[1], out siteNodeId) == false) + continue; + + //we'll look up this + siteNodeIds.Add(siteNodeId); + } + var siteNodes = Services.EntityService.GetAll(UmbracoObjectType, siteNodeIds.ToArray()) + .DistinctBy(e => e.Id) + .ToArray(); + + //add site nodes + nodes.AddRange(siteNodes.Select(e => GetSingleTreeNodeWithAccessCheck(e, id, queryStrings)).Where(node => node != null)); + + return nodes; + } + + nodes.AddRange(entities.Select(e => GetSingleTreeNodeWithAccessCheck(e, id, queryStrings)).Where(node => node != null)); return nodes; - } + } protected abstract MenuItemCollection PerformGetMenuForNode(string id, FormDataCollection queryStrings); @@ -154,8 +202,21 @@ namespace Umbraco.Web.Trees /// /// /// + //we should remove this in v8, it's now here for backwards compat only protected abstract bool HasPathAccess(string id, FormDataCollection queryStrings); + /// + /// Returns true or false if the current user has access to the node based on the user's allowed start node (path) access + /// + /// + /// + /// + protected bool HasPathAccess(IUmbracoEntity entity, FormDataCollection queryStrings) + { + if (entity == null) return false; + return Security.CurrentUser.HasPathAccess(entity, Services.EntityService, RecycleBinId); + } + /// /// Ensures the recycle bin is appended when required (i.e. user has access to the root and it's not in dialog mode) /// @@ -214,7 +275,7 @@ namespace Umbraco.Web.Trees /// private TreeNodeCollection GetTreeNodesInternal(string id, FormDataCollection queryStrings) { - IUmbracoEntity current = GetEntityFromId(id); + var current = GetEntityFromId(id); //before we get the children we need to see if this is a container node @@ -274,7 +335,7 @@ namespace Umbraco.Web.Trees var actions = global::umbraco.BusinessLogic.Actions.Action.FromString(UmbracoUser.GetPermissions(dd.Path)); // A user is allowed to delete their own stuff - if (dd.CreatorId == UmbracoUser.Id && actions.Contains(ActionDelete.Instance) == false) + if (dd.CreatorId == Security.GetUserId() && actions.Contains(ActionDelete.Instance) == false) actions.Add(ActionDelete.Instance); return actions.Select(x => new MenuItem(x)); @@ -291,39 +352,78 @@ namespace Umbraco.Web.Trees { return allowedUserOptions.Select(x => x.Action).OfType().Any(); } - + /// - /// Get an entity via an id that can be either an integer, Guid or UDI + /// this will parse the string into either a GUID or INT /// /// /// - internal IUmbracoEntity GetEntityFromId(string id) + internal Tuple GetIdentifierFromString(string id) { - IUmbracoEntity entity; - Guid idGuid; int idInt; Udi idUdi; if (Guid.TryParse(id, out idGuid)) { - entity = Services.EntityService.GetByKey(idGuid, UmbracoObjectType); + return new Tuple(idGuid, null); } - else if (int.TryParse(id, out idInt)) + if (int.TryParse(id, out idInt)) { - entity = Services.EntityService.Get(idInt, UmbracoObjectType); + return new Tuple(null, idInt); } - else if (Udi.TryParse(id, out idUdi)) + if (Udi.TryParse(id, out idUdi)) { var guidUdi = idUdi as GuidUdi; - entity = guidUdi != null ? Services.EntityService.GetByKey(guidUdi.Guid, UmbracoObjectType) : null; - } - else - { - return null; - } + if (guidUdi != null) + return new Tuple(guidUdi.Guid, null); + } - return entity; - } + return null; + } + + /// + /// Get an entity via an id that can be either an integer, Guid or UDI + /// + /// + /// + /// + /// This object has it's own contextual cache for these lookups + /// + internal IUmbracoEntity GetEntityFromId(string id) + { + return _entityCache.GetOrAdd(id, s => + { + IUmbracoEntity entity; + + Guid idGuid; + int idInt; + Udi idUdi; + + if (Guid.TryParse(s, out idGuid)) + { + entity = Services.EntityService.GetByKey(idGuid, UmbracoObjectType); + } + else if (int.TryParse(s, out idInt)) + { + entity = Services.EntityService.Get(idInt, UmbracoObjectType); + } + else if (Udi.TryParse(s, out idUdi)) + { + var guidUdi = idUdi as GuidUdi; + entity = guidUdi != null ? Services.EntityService.GetByKey(guidUdi.Guid, UmbracoObjectType) : null; + } + else + { + return null; + } + + return entity; + }); + + + } + + private readonly ConcurrentDictionary _entityCache = new ConcurrentDictionary(); } } \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index 679d4a2d6a..fb134ae7f4 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -160,17 +160,7 @@ namespace Umbraco.Web.Trees protected override bool HasPathAccess(string id, FormDataCollection queryStrings) { var entity = GetEntityFromId(id); - if (entity == null) - { - return false; - } - - var media = Services.MediaService.GetById(entity.Id); - if (media == null) - { - return false; - } - return Security.CurrentUser.HasPathAccess(media, Services.EntityService); + return HasPathAccess(entity, queryStrings); } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) From 66da774bf758d06d78bc6848a99cb21251423e4d Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 1 Sep 2017 00:46:01 +1000 Subject: [PATCH 04/14] Revert "Change EntityService GetDescendents to lookup the item path to use in the LIKE query to be a 'prefix' query not a 'contains' query which will be much better for SQL" This reverts commit ecc761a232d7ba2f2d75c3d61ca3090b3889a794. --- src/Umbraco.Core/Services/ContentService.cs | 25 ++--------- src/Umbraco.Core/Services/EntityService.cs | 46 +++++---------------- src/Umbraco.Core/Services/MediaService.cs | 9 +--- 3 files changed, 14 insertions(+), 66 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 84651f5a6f..42ea80a717 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -676,17 +676,7 @@ namespace Umbraco.Core.Services // get query - if the id is System Root, then just get all var query = Query.Builder; if (id != Constants.System.Root) - { - var entityRepository = RepositoryFactory.CreateEntityRepository(uow); - var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray(); - if (contentPath.Length == 0) - { - totalChildren = 0; - return Enumerable.Empty(); - } - query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0]), TextColumnType.NVarchar)); - } - + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); // get filter IQuery filterQuery = null; @@ -721,16 +711,7 @@ namespace Umbraco.Core.Services // get query - if the id is System Root, then just get all var query = Query.Builder; if (id != Constants.System.Root) - { - var entityRepository = RepositoryFactory.CreateEntityRepository(uow); - var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray(); - if (contentPath.Length == 0) - { - totalChildren = 0; - return Enumerable.Empty(); - } - query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", contentPath[0]), TextColumnType.NVarchar)); - } + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); } @@ -1664,7 +1645,7 @@ namespace Umbraco.Core.Services { using (new WriteLock(Locker)) { - var nodeObjectType = Constants.ObjectTypes.DocumentGuid; + var nodeObjectType = new Guid(Constants.ObjectTypes.Document); using (var uow = UowProvider.GetUnitOfWork()) { diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index ae4f1bbf72..6e4b5ef17c 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -348,22 +348,11 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(readOnly:true)) { var repository = RepositoryFactory.CreateEntityRepository(uow); - + var query = Query.Builder; //if the id is System Root, then just get all if (id != Constants.System.Root) - { - //lookup the path so we can use it in the prefix query below - var itemPaths = repository.GetAllPaths(objectTypeId, id).ToArray(); - if (itemPaths.Length == 0) - { - totalRecords = 0; - return Enumerable.Empty(); - } - var itemPath = itemPaths[0].Path; - - query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", itemPath), TextColumnType.NVarchar)); - } + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) @@ -392,30 +381,15 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(readOnly: true)) { var repository = RepositoryFactory.CreateEntityRepository(uow); - + var query = Query.Builder; if (idsA.All(x => x != Constants.System.Root)) { - //lookup the paths so we can use it in the prefix query below - var itemPaths = repository.GetAllPaths(objectTypeId, idsA).ToArray(); - if (itemPaths.Length == 0) - { - totalRecords = 0; - return Enumerable.Empty(); - } - var clauses = new List>>(); foreach (var id in idsA) - { - //if the id is root then don't add any clauses - if (id != Constants.System.Root) - { - var itemPath = itemPaths.FirstOrDefault(x => x.Id == id); - if (itemPath == null) continue; - var path = itemPath.Path; - var qid = id; - clauses.Add(x => x.Path.SqlStartsWith(string.Format("{0},", path), TextColumnType.NVarchar) || x.Path.SqlEndsWith(string.Format(",{0}", qid), TextColumnType.NVarchar)); - } + { + var qid = id; + clauses.Add(x => x.Path.SqlContains(string.Format(",{0},", qid), TextColumnType.NVarchar) || x.Path.SqlEndsWith(string.Format(",{0}", qid), TextColumnType.NVarchar)); } query.WhereAny(clauses); } @@ -468,7 +442,7 @@ namespace Umbraco.Core.Services return contents; } } - + /// /// Gets a collection of descendents by the parents Id /// @@ -627,13 +601,13 @@ namespace Umbraco.Core.Services return repository.GetAllPaths(objectTypeId, keys); } } - + /// - /// Gets a collection of + /// Gets a collection of /// /// Guid id of the UmbracoObjectType /// - /// An enumerable list of objects + /// An enumerable list of objects public virtual IEnumerable GetAll(Guid objectTypeId, params int[] ids) { var umbracoObjectType = UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index ecf2c750af..ee4cddb382 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -593,14 +593,7 @@ namespace Umbraco.Core.Services //if the id is System Root, then just get all if (id != Constants.System.Root) { - var entityRepository = RepositoryFactory.CreateEntityRepository(uow); - var mediaPath = entityRepository.GetAllPaths(Constants.ObjectTypes.MediaGuid, id).ToArray(); - if (mediaPath.Length == 0) - { - totalChildren = 0; - return Enumerable.Empty(); - } - query.Where(x => x.Path.SqlStartsWith(string.Format("{0},", mediaPath[0]), TextColumnType.NVarchar)); + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); } IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) From c860069215c92f04e15024955f0c1a404feda6ce Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Sep 2017 00:38:31 +1000 Subject: [PATCH 05/14] Updates logic to ensure that if we are rendering site nodes that we don't render all of their children, we will filter out any nodes that do not belong to the user's start nodes' branches --- src/Umbraco.Core/Models/UserExtensions.cs | 105 ++++++++++++++++-- .../Trees/ContentTreeControllerBase.cs | 12 +- 2 files changed, 103 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index d2b0278886..c775131fa0 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -162,8 +162,8 @@ namespace Umbraco.Core.Models default: throw new NotSupportedException("Path access is only determined on content or media"); } - } - + } + internal static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId) { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", "path"); @@ -176,16 +176,73 @@ namespace Umbraco.Core.Models if (startNodeIds.Contains(Constants.System.Root)) return true; - var formattedPath = "," + path + ","; + var formattedPath = string.Concat(",", 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 + ",")) + if (formattedPath.Contains(string.Concat(",", recycleBinId, ","))) return false; // check for a start node in the path - return startNodeIds.Any(x => formattedPath.Contains("," + x + ",")); + return startNodeIds.Any(x => formattedPath.Contains(string.Concat(",", x, ","))); } + + internal static bool IsInBranchOfStartNode(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId, out bool hasPathAccess) + { + switch (recycleBinId) + { + case Constants.System.RecycleBinMedia: + return IsInBranchOfStartNode(entity.Path, user.CalculateMediaStartNodeIds(entityService), user.GetMediaStartNodePaths(entityService), out hasPathAccess); + case Constants.System.RecycleBinContent: + return IsInBranchOfStartNode(entity.Path, user.CalculateContentStartNodeIds(entityService), user.GetContentStartNodePaths(entityService), out hasPathAccess); + default: + throw new NotSupportedException("Path access is only determined on content or media"); + } + } + + internal static bool IsInBranchOfStartNode(string path, int[] startNodeIds, string[] startNodePaths, out bool hasPathAccess) + { + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", "path"); + + hasPathAccess = false; + + // check for no access + if (startNodeIds.Length == 0) + return false; + + // check for root access + if (startNodeIds.Contains(Constants.System.Root)) + { + hasPathAccess = true; + return true; + } + + //is it self? + var self = startNodePaths.Any(x => x == path); + if (self) + { + hasPathAccess = true; + return true; + } + + //is it ancestor? + var ancestor = startNodePaths.Any(x => x.StartsWith(path)); + if (ancestor) + { + hasPathAccess = false; + return true; + } + + //is it descendant? + var descendant = startNodePaths.Any(x => path.StartsWith(x)); + if (descendant) + { + hasPathAccess = true; + return true; + } + + return false; + } /// /// Determines whether this user is an admin. @@ -205,7 +262,7 @@ namespace Umbraco.Core.Models { const string cacheKey = "AllContentStartNodes"; //try to look them up from cache so we don't recalculate - var valuesInUserCache = FromUserCache(user, cacheKey); + var valuesInUserCache = FromUserCache(user, cacheKey); if (valuesInUserCache != null) return valuesInUserCache; var gsn = user.Groups.Where(x => x.StartContentId.HasValue).Select(x => x.StartContentId.Value).Distinct().ToArray(); @@ -220,7 +277,7 @@ namespace Umbraco.Core.Models { const string cacheKey = "AllMediaStartNodes"; //try to look them up from cache so we don't recalculate - var valuesInUserCache = FromUserCache(user, cacheKey); + var valuesInUserCache = FromUserCache(user, cacheKey); if (valuesInUserCache != null) return valuesInUserCache; var gsn = user.Groups.Where(x => x.StartMediaId.HasValue).Select(x => x.StartMediaId.Value).Distinct().ToArray(); @@ -228,9 +285,36 @@ namespace Umbraco.Core.Models var vals = CombineStartNodes(UmbracoObjectTypes.Media, gsn, usn, entityService); ToUserCache(user, cacheKey, vals); return vals; + } + + public static string[] GetMediaStartNodePaths(this IUser user, IEntityService entityService) + { + const string cacheKey = "MediaStartNodePaths"; + //try to look them up from cache so we don't recalculate + var valuesInUserCache = FromUserCache(user, cacheKey); + if (valuesInUserCache != null) return valuesInUserCache; + + var startNodeIds = user.CalculateMediaStartNodeIds(entityService); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Media, startNodeIds).Select(x => x.Path).ToArray(); + ToUserCache(user, cacheKey, vals); + return vals; + } + + public static string[] GetContentStartNodePaths(this IUser user, IEntityService entityService) + { + const string cacheKey = "ContentStartNodePaths"; + //try to look them up from cache so we don't recalculate + var valuesInUserCache = FromUserCache(user, cacheKey); + if (valuesInUserCache != null) return valuesInUserCache; + + var startNodeIds = user.CalculateContentStartNodeIds(entityService); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path).ToArray(); + ToUserCache(user, cacheKey, vals); + return vals; } - private static int[] FromUserCache(IUser user, string cacheKey) + private static T FromUserCache(IUser user, string cacheKey) + where T: class { var entityUser = user as User; if (entityUser == null) return null; @@ -239,12 +323,13 @@ namespace Umbraco.Core.Models { object allContentStartNodes; return entityUser.AdditionalData.TryGetValue(cacheKey, out allContentStartNodes) - ? allContentStartNodes as int[] + ? allContentStartNodes as T : null; } } - private static void ToUserCache(IUser user, string cacheKey, int[] vals) + private static void ToUserCache(IUser user, string cacheKey, T vals) + where T: class { var entityUser = user as User; if (entityUser == null) return; diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 10f3f19c7e..ee38b182b7 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -66,10 +66,14 @@ namespace Umbraco.Web.Trees /// /// internal TreeNode GetSingleTreeNodeWithAccessCheck(IUmbracoEntity e, string parentId, FormDataCollection queryStrings) - { - var treeNode = GetSingleTreeNode(e, parentId, queryStrings); - var hasAccess = Security.CurrentUser.HasPathAccess(e, Services.EntityService, RecycleBinId); - if (hasAccess == false) + { + bool hasPathAccess; + var entityIsAncestorOfStartNodes = Security.CurrentUser.IsInBranchOfStartNode(e, Services.EntityService, RecycleBinId, out hasPathAccess); + if (entityIsAncestorOfStartNodes == false) + return null; + + var treeNode = GetSingleTreeNode(e, parentId, queryStrings); + if (hasPathAccess == false) { treeNode.AdditionalData["noAccess"] = true; } From 6cc4ab9348b1e0788ea97d9d0981811a7e28ff94 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Sep 2017 10:45:38 +1000 Subject: [PATCH 06/14] Ensure the user can still refresh nodes they don't have access to when viewing 'site'/branch nodes with multiple start nodes --- .../components/tree/umbtreeitem.directive.js | 10 ++---- .../Trees/ContentTreeController.cs | 33 +++++++++---------- .../Trees/ContentTreeControllerBase.cs | 3 +- src/Umbraco.Web/Trees/MediaTreeController.cs | 30 ++++++++--------- 4 files changed, 35 insertions(+), 41 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js index 2c72441308..03ef2cdfb4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js @@ -89,7 +89,7 @@ angular.module("umbraco.directives") element.find("a:first").text(node.name); - if (!node.menuUrl || (node.metaData && node.metaData.noAccess === true)) { + if (!node.menuUrl) { element.find("a.umb-options").remove(); } @@ -140,9 +140,6 @@ angular.module("umbraco.directives") about it. */ scope.options = function (n, ev) { - if (n.metaData && n.metaData.noAccess === true) { - return; - } emitEvent("treeOptionsClick", { element: element, tree: scope.tree, node: n, event: ev }); }; @@ -176,10 +173,7 @@ angular.module("umbraco.directives") and emits it as a treeNodeSelect element if there is a callback object defined on the tree */ - scope.altSelect = function (n, ev) { - if (n.metaData && n.metaData.noAccess === true) { - return; - } + scope.altSelect = function (n, ev) { emitEvent("treeNodeAltSelect", { element: element, tree: scope.tree, node: n, event: ev }); }; diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index d71726a873..ea644f5dc8 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -41,22 +41,8 @@ namespace Umbraco.Web.Trees [SearchableTree("searchResultFormatter", "configureContentResult")] public class ContentTreeController : ContentTreeControllerBase, ISearchableTree { - private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher(); - - 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.Contains(Constants.System.Root) == false) - { - node.MenuUrl = ""; - } - node.Name = ui.Text("sections", Constants.Trees.Content); - return node; - } - + protected override int RecycleBinId { get { return Constants.System.RecycleBinContent; } @@ -123,14 +109,17 @@ namespace Umbraco.Web.Trees } protected override MenuItemCollection PerformGetMenuForNode(string id, FormDataCollection queryStrings) - { + { if (id == Constants.System.Root.ToInvariantString()) { 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 the user's start node is not the root then the only menu item to display is refresh if (UserStartNodes.Contains(Constants.System.Root) == false) { + menu.Items.Add( + Services.TextService.Localize(string.Concat("actions/", ActionRefresh.Instance.Alias)), + true); return menu; } @@ -174,6 +163,16 @@ namespace Umbraco.Web.Trees { throw new HttpResponseException(HttpStatusCode.NotFound); } + + //if the user has no path access for this node, all they can do is refresh + if (Security.CurrentUser.HasPathAccess(item, Services.EntityService, RecycleBinId) == false) + { + var menu = new MenuItemCollection(); + menu.Items.Add( + Services.TextService.Localize(string.Concat("actions/", ActionRefresh.Instance.Alias)), + true); + return menu; + } var nodeMenu = GetAllNodeMenuItems(item); var allowedMenuItems = GetAllowedUserMenuItemsForNode(item); diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index ee38b182b7..bca36c8f40 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -307,7 +307,8 @@ namespace Umbraco.Web.Trees menu.Items.Add(ui.Text("actions", "emptyTrashcan")); menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); return menu; - } + } + return PerformGetMenuForNode(id, queryStrings); } diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index fb134ae7f4..99117b3b04 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -36,20 +36,7 @@ namespace Umbraco.Web.Trees public class MediaTreeController : ContentTreeControllerBase, ISearchableTree { private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher(); - - 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.Contains(Constants.System.Root) == false) - { - node.MenuUrl = ""; - } - node.Name = ui.Text("sections", Constants.Trees.Media); - return node; - } - + protected override int RecycleBinId { get { return Constants.System.RecycleBinMedia; } @@ -107,9 +94,12 @@ 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 the user's start node is not the root then the only menu item to display is refresh if (UserStartNodes.Contains(Constants.System.Root) == false) { + menu.Items.Add( + Services.TextService.Localize(string.Concat("actions/", ActionRefresh.Instance.Alias)), + true); return menu; } @@ -130,6 +120,16 @@ namespace Umbraco.Web.Trees { throw new HttpResponseException(HttpStatusCode.NotFound); } + + //if the user has no path access for this node, all they can do is refresh + if (Security.CurrentUser.HasPathAccess(item, Services.EntityService, RecycleBinId) == false) + { + menu.Items.Add( + Services.TextService.Localize(string.Concat("actions/", ActionRefresh.Instance.Alias)), + true); + return menu; + } + //return a normal node menu: menu.Items.Add(ui.Text("actions", ActionNew.Instance.Alias)); menu.Items.Add(ui.Text("actions", ActionMove.Instance.Alias)); From 8ad1d247fa5aea715e8bc128fb356375912166d1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Sep 2017 11:49:00 +1000 Subject: [PATCH 07/14] U4-10389 Some webforms editors do not authorize on the user's path access or permissions set for the editing node --- src/Umbraco.Core/ActionsResolver.cs | 27 +- src/Umbraco.Core/Models/UmbracoEntity.cs | 2 + src/Umbraco.Core/Models/UserExtensions.cs | 7 +- .../Trees/ContentTreeControllerBase.cs | 5 +- .../UI/Pages/UmbracoEnsuredPage.cs | 36 ++ src/Umbraco.Web/Umbraco.Web.csproj | 3 - .../umbraco/dialogs/AssignDomain2.aspx.cs | 19 +- .../umbraco/dialogs/moveOrCopy.aspx.cs | 442 ------------------ .../umbraco/dialogs/sort.aspx.cs | 40 +- .../BasePages/UmbracoEnsuredPage.cs | 38 +- src/umbraco.cms/Actions/Action.cs | 25 +- 11 files changed, 155 insertions(+), 489 deletions(-) delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs diff --git a/src/Umbraco.Core/ActionsResolver.cs b/src/Umbraco.Core/ActionsResolver.cs index 206182c6f2..ff2eaa8553 100644 --- a/src/Umbraco.Core/ActionsResolver.cs +++ b/src/Umbraco.Core/ActionsResolver.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; using Umbraco.Core.Logging; @@ -34,7 +35,31 @@ namespace Umbraco.Core { return Values; } - } + } + + /// + /// This method will return a list of IAction's based on a string (letter) list. Each character in the list may represent + /// an IAction. This will associate any found IActions based on the Letter property of the IAction with the character being referenced. + /// + /// + /// returns a list of actions that have an associated letter found in the action string list + public IEnumerable FromActionSymbols(IEnumerable actions) + { + var allActions = Actions.ToArray(); + return actions + .Select(c => allActions.FirstOrDefault(a => a.Letter.ToString(CultureInfo.InvariantCulture) == c)) + .WhereNotNull() + .ToArray(); + } + + /// + /// Returns the string (letter) representation of the actions that make up the actions collection + /// + /// + public IEnumerable ToActionSymbols(IEnumerable actions) + { + return actions.Select(x => x.Letter.ToString(CultureInfo.InvariantCulture)).ToArray(); + } /// /// Gets an Action if it exists. diff --git a/src/Umbraco.Core/Models/UmbracoEntity.cs b/src/Umbraco.Core/Models/UmbracoEntity.cs index 48752468d5..b789698704 100644 --- a/src/Umbraco.Core/Models/UmbracoEntity.cs +++ b/src/Umbraco.Core/Models/UmbracoEntity.cs @@ -51,6 +51,8 @@ namespace Umbraco.Core.Models private string _contentTypeIcon; private string _contentTypeThumbnail; + public static readonly UmbracoEntity Root = new UmbracoEntity(false) {Path = "-1", Name = "root", HasChildren = true}; + public UmbracoEntity() { AdditionalData = new Dictionary(); diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index b842fb8f54..7455d840c6 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -13,7 +13,12 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Models { public static class UserExtensions - { + { + public static IEnumerable GetPermissions(this IUser user, string path, IUserService userService) + { + return userService.GetPermissionsForPath(user, path).GetAllPermissions(); + } + public static bool HasSectionAccess(this IUser user, string app) { var apps = user.AllowedSections; diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index fa18e703bd..1232aaf832 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -271,8 +271,9 @@ namespace Umbraco.Web.Trees internal IEnumerable GetAllowedUserMenuItemsForNode(IUmbracoEntity dd) { - var actions = global::umbraco.BusinessLogic.Actions.Action.FromString(UmbracoUser.GetPermissions(dd.Path)); - + var actions = ActionsResolver.Current.FromActionSymbols(Security.CurrentUser.GetPermissions(dd.Path, Services.UserService)) + .ToList(); + // A user is allowed to delete their own stuff if (dd.CreatorId == UmbracoUser.Id && actions.Contains(ActionDelete.Instance) == false) actions.Add(ActionDelete.Instance); diff --git a/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs b/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs index c09fd0fa0c..c78f66ab97 100644 --- a/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs +++ b/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs @@ -7,7 +7,9 @@ using Umbraco.Web.Security; using umbraco; using umbraco.BusinessLogic; using umbraco.businesslogic.Exceptions; +using umbraco.interfaces; using Umbraco.Core; +using Umbraco.Core.Models; using Umbraco.Core.Security; namespace Umbraco.Web.UI.Pages @@ -32,6 +34,40 @@ namespace Umbraco.Web.UI.Pages } } + /// + /// Performs an authorization check for the user against the requested entity/path and permission set, this is only relevant to content and media + /// + /// + /// + /// + protected void CheckPathAndPermissions(int entityId, UmbracoObjectTypes objectType, IAction actionToCheck) + { + if (objectType == UmbracoObjectTypes.Document || objectType == UmbracoObjectTypes.Media) + { + //check path access + + var entity = entityId == Constants.System.Root + ? UmbracoEntity.Root + : Services.EntityService.Get( + entityId, + objectType); + var hasAccess = Security.CurrentUser.HasPathAccess( + entity, + Services.EntityService, + objectType == UmbracoObjectTypes.Document ? Constants.System.RecycleBinContent : Constants.System.RecycleBinMedia); + if (hasAccess == false) + throw new UserAuthorizationException(string.Format("The current user doesn't have access to the path '{0}'", entity.Path)); + + //only documents have action permissions + if (objectType == UmbracoObjectTypes.Document) + { + var allowedActions = ActionsResolver.Current.FromActionSymbols(Security.CurrentUser.GetPermissions(entity.Path, Services.UserService)).ToArray(); + if (allowedActions.Contains(actionToCheck) == false) + throw new UserAuthorizationException(string.Format("The current user doesn't have permission to {0} on the path '{1}'", actionToCheck.Alias, entity.Path)); + } + } + } + private bool _hasValidated = false; /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a210c269d0..fb628ad137 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1137,9 +1137,6 @@ - - ASPXCodeBehind - ASPXCodeBehind diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs index 4f71e20d08..de1e563776 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs @@ -15,6 +15,14 @@ namespace umbraco.dialogs { public partial class AssignDomain2 : UmbracoEnsuredPage { + protected override void OnInit(EventArgs e) + { + base.OnInit(e); + + var nodeId = GetNodeId(); + CheckPathAndPermissions(nodeId, UmbracoObjectTypes.Document, ActionAssignDomain.Instance); + } + protected override void OnLoad(EventArgs e) { base.OnLoad(e); @@ -29,16 +37,7 @@ namespace umbraco.dialogs pane_domains.Visible = false; p_buttons.Visible = false; return; - } - - if (UmbracoUser.GetPermissions(node.Path).Contains(ActionAssignDomain.Instance.Letter) == false) - { - feedback.Text = ui.Text("assignDomain", "permissionDenied"); - pane_language.Visible = false; - pane_domains.Visible = false; - p_buttons.Visible = false; - return; - } + } pane_language.Title = ui.Text("assignDomain", "setLanguage"); pane_domains.Title = ui.Text("assignDomain", "setDomains"); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs deleted file mode 100644 index f8fc1d3ea3..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs +++ /dev/null @@ -1,442 +0,0 @@ -using System; -using System.Collections; -using System.Globalization; -using System.Web.UI; -using System.Web.UI.WebControls; -using System.Xml; -using Umbraco.Core; -using Umbraco.Core.IO; -using Umbraco.Core.Models; -using umbraco.BasePages; -using System.Linq; -using umbraco.interfaces; -using Umbraco.Web; -using Umbraco.Core; - -namespace umbraco.dialogs -{ - /// - /// Summary description for moveOrCopy. - /// - public partial class moveOrCopy : UmbracoEnsuredPage - { - - protected override void OnInit(EventArgs e) - { - CurrentApp = Request["app"]; - - base.OnInit(e); - } - - protected void Page_Load(object sender, EventArgs e) - { - JTree.DataBind(); - - // Put user code to initialize the page here - if (IsPostBack == false) - { - pp_relate.Text = ui.Text("moveOrCopy", "relateToOriginal"); - - //Document Type copy Hack... - - if (CurrentApp == Constants.Applications.Settings) - { - pane_form.Visible = false; - pane_form_notice.Visible = false; - pane_settings.Visible = true; - - ok.Text = ui.Text("general", "ok", UmbracoUser); - ok.Attributes.Add("style", "width: 60px"); - - var documentType = Services.ContentTypeService.GetContentType(int.Parse(Request.GetItemAsString("id"))); - - //Load master types... - masterType.Attributes.Add("style", "width: 350px;"); - masterType.Items.Add(new ListItem(ui.Text("none") + "...", "0")); - - foreach (var docT in Services.ContentTypeService.GetAllContentTypes().OrderBy(x => x.Name)) - { - masterType.Items.Add(new ListItem(docT.Name, docT.Id.ToString(CultureInfo.InvariantCulture))); - } - - masterType.SelectedValue = (documentType.ParentId > 0 ? documentType.ParentId : 0).ToString(CultureInfo.InvariantCulture); - - rename.Text = documentType.Name + " (copy)"; - pane_settings.Text = "Make a copy of the document type '" + documentType.Name + "' and save it under a new name"; - - } - else - { - pane_form.Visible = true; - pane_form_notice.Visible = true; - - pane_settings.Visible = false; - - // Caption and properies on BUTTON - ok.Text = ui.Text("general", "ok", UmbracoUser); - ok.Attributes.Add("style", "width: 60px"); - ok.Attributes.Add("disabled", "true"); - - IContentBase currContent; - if (CurrentApp == "content") - { - currContent = Services.ContentService.GetById(Request.GetItemAs("id")); - } - else - { - currContent = Services.MediaService.GetById(Request.GetItemAs("id")); - } - - // Preselect the parent of the seslected item. - if (currContent.ParentId > 0) - JTree.SelectedNodePath = currContent.Path.Substring(0, currContent.Path.LastIndexOf(',')); - - var validAction = true; - if (CurrentApp == Constants.Applications.Content && Umbraco.Core.Models.ContentExtensions.HasChildren(currContent, Services)) - { - validAction = ValidAction(currContent, Request.GetItemAsString("mode") == "cut" ? 'M' : 'O'); - } - - if (Request.GetItemAsString("mode") == "cut") - { - pane_form.Text = ui.Text("moveOrCopy", "moveTo", currContent.Name, UmbracoUser); - pp_relate.Visible = false; - } - else - { - pane_form.Text = ui.Text("moveOrCopy", "copyTo", currContent.Name, UmbracoUser); - pp_relate.Visible = true; - } - - if (validAction == false) - { - panel_buttons.Visible = false; - ScriptManager.RegisterStartupScript(this, GetType(), "notvalid", "notValid();", true); - } - } - } - - } - - private bool ValidAction(IContentBase cmsNode, char actionLetter) - { - var currentAction = BusinessLogic.Actions.Action.GetPermissionAssignable().First(a => a.Letter == actionLetter); - return CheckPermissions(cmsNode, currentAction); - } - - /// - /// Checks if the current user has permissions to execute this action against this node - /// - /// - /// - /// - /// - /// This used to do a recursive check for all descendent nodes but this is not required and is a massive CPU hog. - /// See: http://issues.umbraco.org/issue/U4-2632, https://groups.google.com/forum/?fromgroups=#!topic/umbraco-dev/L1D4LwVSP2Y - /// - private bool CheckPermissions(IContentBase node, IAction currentAction) - { - var userService = ApplicationContext.Current.Services.UserService; - var currUserPermissions = userService.GetPermissions(UmbracoContext.Current.Security.CurrentUser, node.Id).GetAllPermissions(); - return currUserPermissions != null && currUserPermissions.Contains(currentAction.Letter.ToString(CultureInfo.InvariantCulture)); - } - - private void HandleDocumentTypeCopy() - { - var contentTypeService = ApplicationContext.Current.Services.ContentTypeService; - var contentType = contentTypeService.GetContentType( - int.Parse(Request.GetItemAsString("id"))); - - //set the master - //http://issues.umbraco.org/issue/U4-2843 - //http://issues.umbraco.org/issue/U4-3552 - var parentId = int.Parse(masterType.SelectedValue); - - var alias = rename.Text.Trim().Replace("'", "''"); - var clone = contentTypeService.Copy(contentType, alias, rename.Text.Trim(), parentId); - - var returnUrl = string.Format("{0}/settings/editNodeTypeNew.aspx?id={1}", SystemDirectories.Umbraco, clone.Id); - - pane_settings.Visible = false; - panel_buttons.Visible = false; - - feedback.Text = "Document type copied"; - feedback.type = uicontrols.Feedback.feedbacktype.success; - - ClientTools.ChangeContentFrameUrl(returnUrl); - } - - public void HandleMoveOrCopy(object sender, EventArgs e) - { - if (CurrentApp == Constants.Applications.Settings) - HandleDocumentTypeCopy(); - else - HandleDocumentMoveOrCopy(); - } - - protected override void OnPreRender(EventArgs e) - { - base.OnPreRender(e); - ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference("../webservices/cmsnode.asmx")); - ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference("../webservices/legacyAjaxCalls.asmx")); - } - - private void HandleDocumentMoveOrCopy() - { - if (Request.GetItemAsString("copyTo") != "" && Request.GetItemAsString("id") != "") - { - // Check if the current node is allowed at new position - var nodeAllowed = false; - - IContentBase currContent; - IContentBase parentContent = null; - IContentTypeBase parentContentType = null; - if (CurrentApp == "content") - { - currContent = Services.ContentService.GetById(Request.GetItemAs("id")); - if (Request.GetItemAs("copyTo") != -1) - { - parentContent = Services.ContentService.GetById(Request.GetItemAs("copyTo")); - if (parentContent != null) - { - parentContentType = Services.ContentTypeService.GetContentType(parentContent.ContentTypeId); - } - } - } - else - { - currContent = Services.MediaService.GetById(Request.GetItemAs("id")); - if (Request.GetItemAs("copyTo") != -1) - { - parentContent = Services.MediaService.GetById(Request.GetItemAs("copyTo")); - if (parentContent != null) - { - parentContentType = Services.ContentTypeService.GetMediaType(parentContent.ContentTypeId); - } - } - } - - // Check on contenttypes - if (parentContentType == null) - { - //check if this is allowed at root - IContentTypeBase currContentType; - if (CurrentApp == "content") - { - currContentType = Services.ContentTypeService.GetContentType(currContent.ContentTypeId); - } - else - { - currContentType = Services.ContentTypeService.GetMediaType(currContent.ContentTypeId); - } - nodeAllowed = currContentType.AllowedAsRoot; - if (!nodeAllowed) - { - feedback.Text = ui.Text("moveOrCopy", "notAllowedAtRoot", UmbracoUser); - feedback.type = uicontrols.Feedback.feedbacktype.error; - } - } - else - { - var allowedChildContentTypeIds = parentContentType.AllowedContentTypes.Select(x => x.Id).ToArray(); - if (allowedChildContentTypeIds.Any(x => x.Value == currContent.ContentTypeId)) - { - nodeAllowed = true; - } - - if (nodeAllowed == false) - { - feedback.Text = ui.Text("moveOrCopy", "notAllowedByContentType", UmbracoUser); - feedback.type = uicontrols.Feedback.feedbacktype.error; - } - else - { - // Check on paths - if ((string.Format(",{0},", parentContent.Path)).IndexOf(string.Format(",{0},", currContent.Id)) > -1) - { - nodeAllowed = false; - feedback.Text = ui.Text("moveOrCopy", "notAllowedByPath", UmbracoUser); - feedback.type = uicontrols.Feedback.feedbacktype.error; - } - } - } - - if (nodeAllowed) - { - pane_form.Visible = false; - pane_form_notice.Visible = false; - panel_buttons.Visible = false; - - var newNodeCaption = parentContent == null - ? ui.Text(CurrentApp) - : parentContent.Name; - - string[] nodes = { currContent.Name, newNodeCaption }; - - if (Request["mode"] == "cut") - { - if (CurrentApp == Constants.Applications.Content) - { - var doc = (IContent)currContent; - var copyToId = Request.GetItemAs("copyTo"); - Services.ContentService.Move(doc, copyToId, UmbracoUser.Id); - - } - else - { - var media = (IMedia)currContent; - var copyToId = Request.GetItemAs("copyTo"); - Services.MediaService.Move(media, copyToId, UmbracoUser.Id); - } - - feedback.Text = ui.Text("moveOrCopy", "moveDone", nodes, UmbracoUser) + "

" + ui.Text("closeThisWindow") + ""; - feedback.type = uicontrols.Feedback.feedbacktype.success; - - // refresh tree - ClientTools.MoveNode(currContent.Id.ToString(), currContent.Path); - } - else - { - //NOTE: We ONLY support Copy on content not media for some reason. - - var newContent = (IContent)currContent; - Services.ContentService.Copy(newContent, Request.GetItemAs("copyTo"), RelateDocuments.Checked, UmbracoUser.Id); - - feedback.Text = ui.Text("moveOrCopy", "copyDone", nodes, UmbracoUser) + "

" + ui.Text("closeThisWindow") + ""; - feedback.type = uicontrols.Feedback.feedbacktype.success; - - // refresh tree - ClientTools.CopyNode(currContent.Id.ToString(), newContent.Path); - } - } - } - } - - ///

- /// JsInclude1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::ClientDependency.Core.Controls.JsInclude JsInclude1; - - /// - /// feedback control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Feedback feedback; - - /// - /// pane_form control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pane_form; - - /// - /// JTree control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.controls.Tree.TreeControl JTree; - - /// - /// pp_relate control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_relate; - - /// - /// RelateDocuments control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBox RelateDocuments; - - /// - /// pane_form_notice control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.PlaceHolder pane_form_notice; - - /// - /// pane_settings control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pane_settings; - - /// - /// PropertyPanel1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel PropertyPanel1; - - /// - /// masterType control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.ListBox masterType; - - /// - /// rename control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.TextBox rename; - - /// - /// RequiredFieldValidator1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.RequiredFieldValidator RequiredFieldValidator1; - - /// - /// panel_buttons control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Panel panel_buttons; - - /// - /// ok control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button ok; - - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sort.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sort.aspx.cs index f3dbb43519..f3bb4c8fdc 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sort.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sort.aspx.cs @@ -13,6 +13,8 @@ using umbraco.cms.businesslogic.media; using umbraco.cms.businesslogic.web; using System.Web.UI; using System.Collections.Generic; +using umbraco.businesslogic.Exceptions; +using Umbraco.Core.Models; namespace umbraco.cms.presentation { @@ -20,7 +22,13 @@ namespace umbraco.cms.presentation /// Summary description for sort. ///
public partial class sort : UmbracoEnsuredPage - { + { + /// + /// The Parent Id being sorted + /// + protected int? ParentIdAsInt { get; private set; } + protected string ParentIdAsString { get; private set; } + private readonly List _nodes = new List(); protected bool HideDateColumn @@ -33,6 +41,21 @@ namespace umbraco.cms.presentation { CurrentApp = helper.Request("app"); + ParentIdAsString = Request.GetItemAsString("ID"); + int parentId; + if (int.TryParse(ParentIdAsString, out parentId)) + { + ParentIdAsInt = parentId; + + if (CurrentApp == Constants.Applications.Content || CurrentApp == Constants.Applications.Media) + { + CheckPathAndPermissions( + ParentIdAsInt.Value, + CurrentApp == Constants.Applications.Content ? UmbracoObjectTypes.Document : UmbracoObjectTypes.Media, + ActionSort.Instance); + } + } + base.OnInit(e); } @@ -50,23 +73,22 @@ namespace umbraco.cms.presentation var app = Request.GetItemAsString("app"); var icon = "../images/umbraco/doc.gif"; - - int parentId; - if (int.TryParse(Request.GetItemAsString("ID"), out parentId)) + + if (ParentIdAsInt.HasValue) { if (app == Constants.Applications.Media) { icon = "../images/umbraco/mediaPhoto.gif"; var mediaService = ApplicationContext.Current.Services.MediaService; - if (parentId == -1) + if (ParentIdAsInt.Value == -1) { foreach (var child in mediaService.GetRootMedia().ToList().OrderBy(x => x.SortOrder)) _nodes.Add(CreateNode(child.Id.ToInvariantString(), child.SortOrder, child.Name, child.CreateDate, icon)); } else { - var children = mediaService.GetChildren(parentId); + var children = mediaService.GetChildren(ParentIdAsInt.Value); foreach (var child in children.OrderBy(x => x.SortOrder)) _nodes.Add(CreateNode(child.Id.ToInvariantString(), child.SortOrder, child.Name, child.CreateDate, icon)); } @@ -76,14 +98,14 @@ namespace umbraco.cms.presentation { var contentService = ApplicationContext.Current.Services.ContentService; - if (parentId == -1) + if (ParentIdAsInt.Value == -1) { foreach (var child in contentService.GetRootContent().ToList().OrderBy(x => x.SortOrder)) _nodes.Add(CreateNode(child.Id.ToInvariantString(), child.SortOrder, child.Name, child.CreateDate, icon)); } else { - var children = contentService.GetChildren(parentId); + var children = contentService.GetChildren(ParentIdAsInt.Value); foreach (var child in children) _nodes.Add(CreateNode(child.Id.ToInvariantString(), child.SortOrder, child.Name, child.CreateDate, icon)); } @@ -100,7 +122,7 @@ namespace umbraco.cms.presentation HideDateColumn = true; - var stylesheetName = Request.GetItemAsString("ID"); + var stylesheetName = ParentIdAsString; if (stylesheetName.IsNullOrWhiteSpace())throw new NullReferenceException("No Id passed in to editor"); var stylesheet = Services.FileService.GetStylesheetByName(stylesheetName.EnsureEndsWith(".css")); if (stylesheet == null) throw new InvalidOperationException("No stylesheet found by name " + stylesheetName); diff --git a/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs b/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs index 58362d50f2..36a0f63f36 100644 --- a/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs +++ b/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs @@ -7,6 +7,8 @@ using Umbraco.Core.IO; using Umbraco.Core.Logging; using umbraco.BusinessLogic; using umbraco.businesslogic.Exceptions; +using umbraco.interfaces; +using Umbraco.Core.Models; using Umbraco.Core.Security; namespace umbraco.BasePages @@ -16,7 +18,41 @@ namespace umbraco.BasePages ///
[Obsolete("This class has been superceded by Umbraco.Web.UI.Pages.UmbracoEnsuredPage")] public class UmbracoEnsuredPage : BasePage - { + { + /// + /// Performs an authorization check for the user against the requested entity/path and permission set, this is only relevant to content and media + /// + /// + /// + /// + protected void CheckPathAndPermissions(int entityId, UmbracoObjectTypes objectType, IAction actionToCheck) + { + if (objectType == UmbracoObjectTypes.Document || objectType == UmbracoObjectTypes.Media) + { + //check path access + + var entity = entityId == Constants.System.Root + ? UmbracoEntity.Root + : Services.EntityService.Get( + entityId, + objectType); + var hasAccess = CurrentUser.UserEntity.HasPathAccess( + entity, + Services.EntityService, + objectType == UmbracoObjectTypes.Document ? Constants.System.RecycleBinContent : Constants.System.RecycleBinMedia); + if (hasAccess == false) + throw new UserAuthorizationException(string.Format("The current user doesn't have access to the path '{0}'", entity.Path)); + + //only documents have action permissions + if (objectType == UmbracoObjectTypes.Document) + { + var allowedActions = ActionsResolver.Current.FromActionSymbols(CurrentUser.UserEntity.GetPermissions(entity.Path, Services.UserService)).ToArray(); + if (allowedActions.Contains(actionToCheck) == false) + throw new UserAuthorizationException(string.Format("The current user doesn't have permission to {0} on the path '{1}'", actionToCheck.Alias, entity.Path)); + } + } + } + /// /// Checks if the page exists outside of the /umbraco route, in which case the request will not have been authenticated for the back office /// so we'll force authentication. diff --git a/src/umbraco.cms/Actions/Action.cs b/src/umbraco.cms/Actions/Action.cs index 81c78ade1b..c7a9a6242b 100644 --- a/src/umbraco.cms/Actions/Action.cs +++ b/src/umbraco.cms/Actions/Action.cs @@ -128,30 +128,20 @@ namespace umbraco.BusinessLogic.Actions /// /// /// returns a list of actions that have an associated letter found in the action string list + [Obsolete("Use ActionsResolver.Current.FromActionSymbols instead")] public static List FromString(string actions) { - List list = new List(); - foreach (char c in actions.ToCharArray()) - { - IAction action = ActionsResolver.Current.Actions.ToList().Find( - delegate(IAction a) - { - return a.Letter == c; - } - ); - if (action != null) - list.Add(action); - } - return list; + return ActionsResolver.Current.FromActionSymbols(actions.ToCharArray().Select(x => x.ToString())).ToList(); } /// /// Returns the string representation of the actions that make up the actions collection /// /// + [Obsolete("Use ActionsResolver.Current.ToActionSymbols instead")] public static string ToString(List actions) { - string[] strMenu = Array.ConvertAll(actions.ToArray(), delegate(IAction a) { return (a.Letter.ToString(CultureInfo.InvariantCulture)); }); + string[] strMenu = Array.ConvertAll(actions.ToArray(), a => (a.Letter.ToString(CultureInfo.InvariantCulture))); return string.Join("", strMenu); } @@ -161,12 +151,7 @@ namespace umbraco.BusinessLogic.Actions /// public static List GetPermissionAssignable() { - return ActionsResolver.Current.Actions.ToList().FindAll( - delegate(IAction a) - { - return (a.CanBePermissionAssigned); - } - ); + return ActionsResolver.Current.Actions.ToList().FindAll(a => (a.CanBePermissionAssigned)); } /// From 2efe2cc88a5aecc5dd461cf4fd4602e345d4c807 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Sep 2017 12:54:32 +1000 Subject: [PATCH 08/14] Ensures that a user that has multiple start nodes cannot select the root start node in the tree dialogs --- .../components/tree/umbtree.directive.js | 27 ++++++++++++++++++- .../components/tree/umbtreeitem.directive.js | 9 +++++-- .../src/common/services/tree.service.js | 8 ++++++ .../Trees/ContentTreeControllerBase.cs | 17 ++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js index 1a652a787c..6ed1a21306 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js @@ -31,7 +31,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat //var showheader = (attrs.showheader !== 'false'); var hideoptions = (attrs.hideoptions === 'true') ? "hide-options" : ""; var template = '
  • '; - template += '
    ' + + template += '
    ' + '
    ' + ' {{tree.name}}
    ' + '' + @@ -310,6 +310,25 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat } + /** Returns the css classses assigned to the node (div element) */ + scope.getNodeCssClass = function (node) { + if (!node) { + return ''; + } + + //TODO: This is called constantly because as a method in a template it's re-evaluated pretty much all the time + // it would be better if we could cache the processing. The problem is that some of these things are dynamic. + + var css = []; + if (node.cssClasses) { + _.each(node.cssClasses, function (c) { + css.push(c); + }); + } + + return css.join(" "); + }; + scope.selectEnabledNodeClass = function (node) { return node ? node.selected ? @@ -383,6 +402,12 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat defined on the tree */ scope.select = function (n, ev) { + + if (n.metaData && n.metaData.noAccess === true) { + ev.preventDefault(); + return; + } + //on tree select we need to remove the current node - // whoever handles this will need to make sure the correct node is selected //reset current node selection diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js index 03ef2cdfb4..ce74767007 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js @@ -111,7 +111,11 @@ angular.module("umbraco.directives") scope.getNodeCssClass = function (node) { if (!node) { return ''; - } + } + + //TODO: This is called constantly because as a method in a template it's re-evaluated pretty much all the time + // it would be better if we could cache the processing. The problem is that some of these things are dynamic. + var css = []; if (node.cssClasses) { _.each(node.cssClasses, function(c) { @@ -120,7 +124,8 @@ angular.module("umbraco.directives") } if (node.selected) { css.push("umb-tree-node-checked"); - } + } + return css.join(" "); }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index 325aa4690f..888a067a66 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -44,6 +44,14 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS if (!parentNode.section) { parentNode.section = section; } + + if (parentNode.metaData && parentNode.metaData.noAccess === true) { + if (!parentNode.cssClasses) { + parentNode.cssClasses = []; + } + parentNode.cssClasses.push("no-access"); + } + //create a method outside of the loop to return the parent - otherwise jshint blows up var funcParent = function() { return parentNode; diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index c025f027cb..8fabc1677e 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -54,6 +54,23 @@ namespace Umbraco.Web.Trees } #endregion + + /// + /// Ensure the noAccess metadata is applied for the root node if in dialog mode and the user doesn't have path access to it + /// + /// + /// + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) + { + var node = base.CreateRootNode(queryStrings); + + if (IsDialog(queryStrings) && UserStartNodes.Contains(Constants.System.Root) == false) + { + node.AdditionalData["noAccess"] = true; + } + + return node; + } protected abstract TreeNode GetSingleTreeNode(IUmbracoEntity e, string parentId, FormDataCollection queryStrings); From 119dd5af298aa7dd267418959e6e0aba609a399e Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 6 Sep 2017 13:11:58 +0200 Subject: [PATCH 09/14] auto expand tree to nodes with access --- .../components/tree/umbtreeitem.directive.js | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js index ce74767007..3240c5ba4f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js @@ -111,10 +111,10 @@ angular.module("umbraco.directives") scope.getNodeCssClass = function (node) { if (!node) { return ''; - } - - //TODO: This is called constantly because as a method in a template it's re-evaluated pretty much all the time - // it would be better if we could cache the processing. The problem is that some of these things are dynamic. + } + + //TODO: This is called constantly because as a method in a template it's re-evaluated pretty much all the time + // it would be better if we could cache the processing. The problem is that some of these things are dynamic. var css = []; if (node.cssClasses) { @@ -124,7 +124,7 @@ angular.module("umbraco.directives") } if (node.selected) { css.push("umb-tree-node-checked"); - } + } return css.join(" "); }; @@ -164,9 +164,9 @@ angular.module("umbraco.directives") } if (n.metaData && n.metaData.noAccess === true) { - ev.preventDefault(); + ev.preventDefault(); return; - } + } emitEvent("treeNodeSelect", { element: element, tree: scope.tree, node: n, event: ev }); ev.preventDefault(); @@ -239,6 +239,12 @@ angular.module("umbraco.directives") setupNodeDom(scope.node, scope.tree); + // load the children if the current user don't have access to the node + // it is used to auto expand the tree to the start nodes the user has access to + if(scope.node.hasChildren && scope.node.metaData.noAccess) { + scope.loadChildren(scope.node); + } + var template = '
    '; var newElement = angular.element(template); $compile(newElement)(scope); From 77d240c69affa6b513088549a007d3a488ff5cb2 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 6 Sep 2017 13:31:13 +0200 Subject: [PATCH 10/14] don't show disallow cursor on the more button on a tree item (the three dots) --- .../directives/components/tree/umbtreeitem.directive.js | 2 +- src/Umbraco.Web.UI.Client/src/less/tree.less | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js index 3240c5ba4f..25c1becc87 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js @@ -41,7 +41,7 @@ angular.module("umbraco.directives") //'' + ' ' + '' + - '' + + '' + //NOTE: These are the 'option' elipses '' + '
    ' + diff --git a/src/Umbraco.Web.UI.Client/src/less/tree.less b/src/Umbraco.Web.UI.Client/src/less/tree.less index 814c414eea..045a741e90 100644 --- a/src/Umbraco.Web.UI.Client/src/less/tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/tree.less @@ -377,10 +377,11 @@ div.locked:before{ bottom: 0; } -.umb-tree li div.no-access *:not(ins) { - color: @gray-7; - cursor:not-allowed; -} +.umb-tree li div.no-access .umb-tree-icon, +.umb-tree li div.no-access .umb-tree-item__label { + color: @gray-7; + cursor: not-allowed; +} // Tree context menu // ------------------------- From 12b4587828d15881f581257a321276153bcb0664 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 6 Sep 2017 14:33:01 +0200 Subject: [PATCH 11/14] null check for change password --- src/Umbraco.Web.UI.Client/src/views/users/user.controller.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index cf83aa4db5..57477eebeb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -121,7 +121,9 @@ vm.user.resetPasswordValue = null; //anytime a user is changing another user's password, we are in effect resetting it so we need to set that flag here + if(vm.user.changePassword) { vm.user.changePassword.reset = !vm.user.changePassword.oldPassword && !vm.user.isCurrentUser; + } contentEditingHelper.contentEditorPerformSave({ statusMessage: vm.labels.saving, From 858c21c79fb030535ddbdc610a7955a8ec82127b Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 8 Sep 2017 14:28:18 +1000 Subject: [PATCH 12/14] Ensures Content header node isn't shown in copy/move dialogs when the user has a start node set that is not the root --- src/Umbraco.Web.UI.Client/src/less/tree.less | 1 + .../src/views/common/overlays/user/user.controller.js | 4 ++-- .../src/views/content/content.copy.controller.js | 8 +++++++- .../src/views/content/content.move.controller.js | 10 ++++++++-- src/Umbraco.Web.UI.Client/src/views/content/copy.html | 2 +- src/Umbraco.Web.UI.Client/src/views/content/move.html | 2 +- 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/tree.less b/src/Umbraco.Web.UI.Client/src/less/tree.less index 045a741e90..c859fae991 100644 --- a/src/Umbraco.Web.UI.Client/src/less/tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/tree.less @@ -378,6 +378,7 @@ div.locked:before{ } .umb-tree li div.no-access .umb-tree-icon, +.umb-tree li div.no-access .root-link, .umb-tree li div.no-access .umb-tree-item__label { color: @gray-7; cursor: not-allowed; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js index 6820e81f31..b18aa96588 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js @@ -169,10 +169,10 @@ angular.module("umbraco") $scope.showPasswordFields = !$scope.showPasswordFields; } - function clearPasswordFields() { + function clearPasswordFields() { $scope.changePasswordModel.value.oldPassword = ""; $scope.changePasswordModel.value.newPassword = ""; $scope.changePasswordModel.value.confirm = ""; } - + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js index 9940aebe64..0e28a09236 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js @@ -1,5 +1,5 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", - function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { + function ($scope, userService, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { var dialogOptions = $scope.dialogOptions; var searchText = "Search..."; @@ -18,6 +18,12 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", results: [], selectedSearchResults: [] } + $scope.treeModel = { + hideHeader: false + } + userService.getCurrentUser().then(function (userData) { + $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; + }); var node = dialogOptions.currentNode; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js index ce81a60a38..984c147ee4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js @@ -1,5 +1,5 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", - function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { + function ($scope, userService, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { var dialogOptions = $scope.dialogOptions; var searchText = "Search..."; @@ -15,7 +15,13 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", showSearch: false, results: [], selectedSearchResults: [] - } + } + $scope.treeModel = { + hideHeader: false + } + userService.getCurrentUser().then(function (userData) { + $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; + }); var node = dialogOptions.currentNode; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/copy.html b/src/Umbraco.Web.UI.Client/src/views/content/copy.html index c19ec72dbd..0248dd7d92 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -48,7 +48,7 @@
    Date: Fri, 8 Sep 2017 15:11:39 +1000 Subject: [PATCH 13/14] fixed media start node mapping for user --- src/Umbraco.Web/Editors/UsersController.cs | 3 ++- src/Umbraco.Web/Models/Mapping/UserModelMapper.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 63484d1cec..45fce0e33f 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -189,7 +189,8 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException(HttpStatusCode.NotFound); } - return Mapper.Map(user); + var result = Mapper.Map(user); + return result; } /// diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 7f4fb222ca..55e4c1f013 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -249,7 +249,7 @@ namespace Umbraco.Web.Models.Mapping user.CalculateMediaStartNodeIds(applicationContext.Services.EntityService), applicationContext.Services.TextService, applicationContext.Services.EntityService, - UmbracoObjectTypes.Document, + UmbracoObjectTypes.Media, "media/mediaRoot"))) .ForMember( detail => detail.StartContentIds, From 7ae95e31a1e0b3a51342b2ef1548b17a7697b840 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 8 Sep 2017 15:16:13 +1000 Subject: [PATCH 14/14] Ensures the root node is not shown in the copy/media dialogs for content/media when start ndoes are assigned, fixes entity searching since there was js errors (maybe from a bad merge) --- .../src/common/services/search.service.js | 6 +++--- .../src/views/content/content.copy.controller.js | 2 +- src/Umbraco.Web.UI.Client/src/views/content/copy.html | 2 +- src/Umbraco.Web.UI.Client/src/views/content/move.html | 4 ++-- .../src/views/media/media.move.controller.js | 9 ++++++++- src/Umbraco.Web.UI.Client/src/views/media/move.html | 2 +- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js index c692fda7ba..8738c1011e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js @@ -44,7 +44,7 @@ angular.module('umbraco.services') return entityResource.search(args.term, "Member", args.searchFrom).then(function (data) { _.each(data, function (item) { - configureMemberResult(item); + searchResultFormatter.configureMemberResult(item); }); return data; }); @@ -69,7 +69,7 @@ angular.module('umbraco.services') return entityResource.search(args.term, "Document", args.searchFrom, args.canceler).then(function (data) { _.each(data, function (item) { - configureContentResult(item); + searchResultFormatter.configureContentResult(item); }); return data; }); @@ -94,7 +94,7 @@ angular.module('umbraco.services') return entityResource.search(args.term, "Media", args.searchFrom).then(function (data) { _.each(data, function (item) { - configureMediaResult(item); + searchResultFormatter.configureMediaResult(item); }); return data; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js index 0e28a09236..fa2d831eac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js @@ -22,7 +22,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", hideHeader: false } userService.getCurrentUser().then(function (userData) { - $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; + $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; }); var node = dialogOptions.currentNode; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/copy.html b/src/Umbraco.Web.UI.Client/src/views/content/copy.html index 0248dd7d92..ca7714c284 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -48,7 +48,7 @@
    - 0 && userData.startMediaIds.indexOf(-1) == -1; + }); + function nodeSelectHandler(ev, args) { if(args && args.event) { diff --git a/src/Umbraco.Web.UI.Client/src/views/media/move.html b/src/Umbraco.Web.UI.Client/src/views/media/move.html index 52be1832f0..d04d66f706 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/move.html @@ -27,7 +27,7 @@