using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Web.Http;
using Umbraco.Core;
using Umbraco.Core.Services;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.WebApi.Filters;
using System.Globalization;
using Umbraco.Core.Models.Entities;
using Umbraco.Web._Legacy.Actions;
namespace Umbraco.Web.Trees
{
public abstract class ContentTreeControllerBase : TreeController
{
#region Actions
///
/// Gets an individual tree node
///
///
///
///
[HttpQueryStringFilter("queryStrings")]
public TreeNode GetTreeNode(string id, FormDataCollection queryStrings)
{
int asInt;
Guid asGuid = Guid.Empty;
if (int.TryParse(id, out asInt) == false)
{
if (Guid.TryParse(id, out asGuid) == false)
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
}
}
var entity = asGuid == Guid.Empty
? Services.EntityService.Get(asInt, UmbracoObjectType)
: Services.EntityService.Get(asGuid, UmbracoObjectType);
if (entity == null)
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
}
var node = GetSingleTreeNode(entity, entity.ParentId.ToInvariantString(), queryStrings);
//add the tree alias to the node since it is standalone (has no root for which this normally belongs)
node.AdditionalData["treeAlias"] = TreeAlias;
return node;
}
#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(IEntitySlim entity, 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(IEntitySlim e, string parentId, FormDataCollection queryStrings)
{
var entityIsAncestorOfStartNodes = Security.CurrentUser.IsInBranchOfStartNode(e, Services.EntityService, RecycleBinId, out var hasPathAccess);
if (entityIsAncestorOfStartNodes == false)
return null;
var treeNode = GetSingleTreeNode(e, parentId, queryStrings);
if (hasPathAccess == false)
{
treeNode.AdditionalData["noAccess"] = true;
}
return treeNode;
}
///
/// Returns the
///
protected abstract int RecycleBinId { get; }
///
/// Returns true if the recycle bin has items in it
///
protected abstract bool RecycleBinSmells { get; }
///
/// 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 rootIdString = Constants.System.Root.ToString(CultureInfo.InvariantCulture);
var hasAccessToRoot = UserStartNodes.Contains(Constants.System.Root);
var startNodeId = queryStrings.HasKey(TreeQueryStringParameters.StartNodeId)
? queryStrings.GetValue(TreeQueryStringParameters.StartNodeId)
: string.Empty;
if (string.IsNullOrEmpty(startNodeId) == false && startNodeId != "undefined" && startNodeId != rootIdString)
{
// request has been made to render from a specific, non-root, start node
id = startNodeId;
// 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)
{
Logger.Warn("User " + Security.CurrentUser.Username + " does not have access to node with id " + id);
return nodes;
}
// 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 culture = queryStrings["culture"].TryConvertTo();
var entities = GetChildEntities(id, culture.Success ? culture.Result : null).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)
{
var topNodes = Services.EntityService.GetAll(UmbracoObjectType, topNodeIds.ToArray());
nodes.AddRange(topNodes.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings)).Where(x => x != 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);
protected abstract UmbracoObjectTypes UmbracoObjectType { get; }
protected IEnumerable GetChildEntities(string id, string culture)
{
// try to parse id as an integer else use GetEntityFromId
// which will grok Guids, Udis, etc and let use obtain the id
if (int.TryParse(id, out var entityId) == false)
{
var entity = GetEntityFromId(id);
if (entity == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
entityId = entity.Id;
}
IEntitySlim[] result;
// if a request is made for the root node but user has no access to
// root node, return start nodes instead
if (entityId == Constants.System.Root && UserStartNodes.Contains(Constants.System.Root) == false)
{
result = UserStartNodes.Length > 0
? Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes).ToArray()
: Array.Empty();
}
else
{
result = Services.EntityService.GetChildren(entityId, UmbracoObjectType).ToArray();
}
//This should really never be null, but we'll error check anyways
culture = culture ?? Services.LocalizationService.GetDefaultLanguageIsoCode();
//Try to see if there is a variant name for the current language for the item and set the name accordingly.
//If any of this fails, the tree node name will remain the default invariant culture name.
//fixme - what if there is no name found at all ? This could occur if the doc type is variant and the user fills in all language values, then creates a new lang and sets it as the default
//fixme - what if the user changes this document type to not allow culture variants after it's already been created with culture variants, this means we'll be displaying the culture variant name when in fact we should be displaying the invariant name... but that would be null
if (!culture.IsNullOrWhiteSpace())
{
foreach (var e in result)
{
if (e.AdditionalData.TryGetValue("CultureNames", out var cultureNames)
&& cultureNames is IDictionary cnd)
{
if (cnd.TryGetValue(culture, out var name))
{
e.Name = name;
}
}
}
}
return result;
}
///
/// Returns true or false if the current user has access to the node based on the user's allowed start node (path) access
///
///
///
///
//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)
///
///
///
///
///
/// This method is overwritten strictly to render the recycle bin, it should serve no other purpose
///
protected sealed override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)
{
//check if we're rendering the root
if (id == Constants.System.Root.ToInvariantString() && UserStartNodes.Contains(Constants.System.Root))
{
var altStartId = string.Empty;
if (queryStrings.HasKey(TreeQueryStringParameters.StartNodeId))
altStartId = queryStrings.GetValue(TreeQueryStringParameters.StartNodeId);
//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;
}
var nodes = GetTreeNodesInternal(id, queryStrings);
//only render the recycle bin if we are not in dialog and the start id id still the root
if (IsDialog(queryStrings) == false && id == Constants.System.Root.ToInvariantString())
{
nodes.Add(CreateTreeNode(
RecycleBinId.ToInvariantString(),
id,
queryStrings,
Services.TextService.Localize("general/recycleBin"),
"icon-trash",
RecycleBinSmells,
queryStrings.GetValue("application") + TreeAlias.EnsureStartsWith('/') + "/recyclebin"));
}
return nodes;
}
return GetTreeNodesInternal(id, queryStrings);
}
///
/// Before we make a call to get the tree nodes we have to check if they can actually be rendered
///
///
///
///
///
/// Currently this just checks if it is a container type, if it is we cannot render children. In the future this might check for other things.
///
private TreeNodeCollection GetTreeNodesInternal(string id, FormDataCollection queryStrings)
{
var current = GetEntityFromId(id);
//before we get the children we need to see if this is a container node
//test if the parent is a listview / container
if (current != null && current.IsContainer)
{
//no children!
return new TreeNodeCollection();
}
return PerformGetTreeNodes(id, queryStrings);
}
///
/// Checks if the menu requested is for the recycle bin and renders that, otherwise renders the result of PerformGetMenuForNode
///
///
///
///
protected sealed override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings)
{
if (RecycleBinId.ToInvariantString() == id)
{
var menu = new MenuItemCollection();
menu.Items.Add(Services.TextService.Localize("actions/emptyTrashcan"));
menu.Items.Add(Services.TextService.Localize("actions", ActionRefresh.Instance.Alias), true);
return menu;
}
return PerformGetMenuForNode(id, queryStrings);
}
///
/// Based on the allowed actions, this will filter the ones that the current user is allowed
///
///
///
///
protected void FilterUserAllowedMenuItems(MenuItemCollection menuWithAllItems, IEnumerable