diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 8fabc1677e..668e7c310a 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -54,7 +54,7 @@ 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 /// @@ -70,12 +70,12 @@ namespace Umbraco.Web.Trees } return node; - } + } protected abstract TreeNode GetSingleTreeNode(IUmbracoEntity e, string parentId, FormDataCollection queryStrings); - + /// - /// Returns a for the and + /// 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 /// /// @@ -83,13 +83,13 @@ namespace Umbraco.Web.Trees /// /// internal TreeNode GetSingleTreeNodeWithAccessCheck(IUmbracoEntity e, string parentId, FormDataCollection queryStrings) - { - bool hasPathAccess; - var entityIsAncestorOfStartNodes = Security.CurrentUser.IsInBranchOfStartNode(e, Services.EntityService, RecycleBinId, out hasPathAccess); - if (entityIsAncestorOfStartNodes == false) - return null; + { + bool hasPathAccess; + var entityIsAncestorOfStartNodes = Security.CurrentUser.IsInBranchOfStartNode(e, Services.EntityService, RecycleBinId, out hasPathAccess); + if (entityIsAncestorOfStartNodes == false) + return null; - var treeNode = GetSingleTreeNode(e, parentId, queryStrings); + var treeNode = GetSingleTreeNode(e, parentId, queryStrings); if (hasPathAccess == false) { treeNode.AdditionalData["noAccess"] = true; @@ -98,7 +98,7 @@ namespace Umbraco.Web.Trees } /// - /// Returns the + /// Returns the /// protected abstract int RecycleBinId { get; } @@ -111,79 +111,69 @@ namespace Umbraco.Web.Trees /// Returns the user's start node for this tree /// protected abstract int[] UserStartNodes { get; } - + protected virtual TreeNodeCollection PerformGetTreeNodes(string id, FormDataCollection queryStrings) { var nodes = new TreeNodeCollection(); - var altStartId = string.Empty; - if (queryStrings.HasKey(TreeQueryStringParameters.StartNodeId)) - altStartId = queryStrings.GetValue(TreeQueryStringParameters.StartNodeId); - var rootIdString = Constants.System.Root.ToString(CultureInfo.InvariantCulture); + var rootIdString = Constants.System.Root.ToString(CultureInfo.InvariantCulture); + var hasAccessToRoot = UserStartNodes.Contains(Constants.System.Root); - //check if a request has been made to render from a specific start node - if (string.IsNullOrEmpty(altStartId) == false && altStartId != "undefined" && altStartId != rootIdString) + var startNodeId = queryStrings.HasKey(TreeQueryStringParameters.StartNodeId) + ? queryStrings.GetValue(TreeQueryStringParameters.StartNodeId) + : string.Empty; + + if (string.IsNullOrEmpty(startNodeId) == false && startNodeId != "undefined" && startNodeId != rootIdString) { - id = altStartId; + // request has been made to render from a specific, non-root, start node + id = startNodeId; - //we need to verify that the user has access to view this node, otherwise we'll render an empty tree collection - // TODO: in the future we could return a validation statement so we can have some UI to notify the user they don't have access + // ensure that the user has access to that node, otherwise return the empty tree nodes collection + // TODO: in the future we could return a validation statement so we can have some UI to notify the user they don't have access if (HasPathAccess(id, queryStrings) == false) { - LogHelper.Warn("The user " + Security.CurrentUser.Username + " does not have access to the tree node " + id); - return new TreeNodeCollection(); + LogHelper.Warn("User " + Security.CurrentUser.Username + " does not have access to node with id " + id); + return nodes; } - // So there's an alt id specified, it's not the root node and the user has access to it, great! But there's one thing we - // need to consider: - // If the tree is being rendered in a dialog view we want to render only the children of the specified id, but - // when the tree is being rendered normally in a section and the current user's start node is not -1, then - // we want to include their start node in the tree as well. - // Therefore, in the latter case, we want to change the id to -1 since we want to render the current user's root node - // and the GetChildEntities method will take care of rendering the correct root node. - // If it is in dialog mode, then we don't need to change anything and the children will just render as per normal. - if (IsDialog(queryStrings) == false && UserStartNodes.Contains(Constants.System.Root) == false) + // if the tree is rendered... + // - in a dialog: render only the children of the specific start node, nothing to do + // - in a section: if the current user's start nodes do not contain the root node, we need + // to include these start nodes in the tree too, to provide some context - i.e. change + // start node back to root node, and then GetChildEntities method will take care of the rest. + if (IsDialog(queryStrings) == false && hasAccessToRoot == false) + id = rootIdString; + } + + // get child entities - if id is root, but user's start nodes do not contain the + // root node, this returns the start nodes instead of root's children + var entities = GetChildEntities(id).ToList(); + nodes.AddRange(entities.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings)).Where(x => x != null)); + + // if the user does not have access to the root node, what we have is the start nodes, + // but to provide some context we also need to add their topmost nodes when they are not + // topmost nodes themselves (level > 1). + if (id == rootIdString && hasAccessToRoot == false) + { + var topNodeIds = entities.Where(x => x.Level > 1).Select(GetTopNodeId).Where(x => x != 0).Distinct().ToArray(); + if (topNodeIds.Length > 0) { - id = Constants.System.Root.ToString(CultureInfo.InvariantCulture); + var topNodes = Services.EntityService.GetAll(UmbracoObjectType, topNodeIds.ToArray()); + nodes.AddRange(topNodes.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings)).Where(x => x != 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; - } + } + + private static readonly char[] Comma = { ',' }; + + private int GetTopNodeId(IUmbracoEntity entity) + { + int id; + var parts = entity.Path.Split(Comma, StringSplitOptions.RemoveEmptyEntries); + return parts.Length >= 2 && int.TryParse(parts[1], out id) ? id : 0; + } protected abstract MenuItemCollection PerformGetMenuForNode(string id, FormDataCollection queryStrings); @@ -191,30 +181,27 @@ namespace Umbraco.Web.Trees protected IEnumerable GetChildEntities(string id) { - // use helper method to ensure we support both integer and guid lookups - int iid; - - // look up from GUID if it's not an integer - if (int.TryParse(id, out iid) == false) + // try to parse id as an integer else use GetEntityFromId + // which will grok Guids, Udis, etc and let use obtain the id + int entityId; + if (int.TryParse(id, out entityId) == false) { - var idEntity = GetEntityFromId(id); - if (idEntity == null) - { + var entity = GetEntityFromId(id); + if (entity == null) throw new HttpResponseException(HttpStatusCode.NotFound); - } - iid = idEntity.Id; + entityId = entity.Id; } // if a request is made for the root node but user has no access to // root node, return start nodes instead - if (iid == Constants.System.Root && UserStartNodes.Contains(Constants.System.Root) == false) + if (entityId == Constants.System.Root && UserStartNodes.Contains(Constants.System.Root) == false) { return UserStartNodes.Length > 0 ? Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes) : Enumerable.Empty(); } - return Services.EntityService.GetChildren(iid, UmbracoObjectType).ToArray(); + return Services.EntityService.GetChildren(entityId, UmbracoObjectType).ToArray(); } /// @@ -236,7 +223,7 @@ namespace Umbraco.Web.Trees { 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) @@ -257,7 +244,7 @@ namespace Umbraco.Web.Trees if (queryStrings.HasKey(TreeQueryStringParameters.StartNodeId)) altStartId = queryStrings.GetValue(TreeQueryStringParameters.StartNodeId); - //check if a request has been made to render from a specific start node + //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)) { id = altStartId; @@ -324,7 +311,7 @@ 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); } @@ -355,8 +342,8 @@ namespace Umbraco.Web.Trees internal IEnumerable GetAllowedUserMenuItemsForNode(IUmbracoEntity dd) { var actions = ActionsResolver.Current.FromActionSymbols(Security.CurrentUser.GetPermissions(dd.Path, Services.UserService)) - .ToList(); - + .ToList(); + // A user is allowed to delete their own stuff if (dd.CreatorId == Security.GetUserId() && actions.Contains(ActionDelete.Instance) == false) actions.Add(ActionDelete.Instance); @@ -370,12 +357,12 @@ namespace Umbraco.Web.Trees /// The Document to check permissions against /// A list of MenuItems that the user has permissions to execute on the current document /// By default the user must have Browse permissions to see the node in the Content tree - /// + /// internal bool CanUserAccessNode(IUmbracoEntity doc, IEnumerable allowedUserOptions) { return allowedUserOptions.Select(x => x.Action).OfType().Any(); } - + /// /// this will parse the string into either a GUID or INT /// @@ -399,11 +386,11 @@ namespace Umbraco.Web.Trees { var guidUdi = idUdi as GuidUdi; if (guidUdi != null) - return new Tuple(guidUdi.Guid, null); - } + return new Tuple(guidUdi.Guid, null); + } return null; - } + } /// /// Get an entity via an id that can be either an integer, Guid or UDI @@ -442,11 +429,11 @@ namespace Umbraco.Web.Trees } return entity; - }); + }); + + + } - - } - private readonly ConcurrentDictionary _entityCache = new ConcurrentDictionary(); } } \ No newline at end of file