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)