using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
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.Core.Models.EntityBase;
using Umbraco.Core.Persistence;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.WebApi.Filters;
using System.Globalization;
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;
if (int.TryParse(id, out asInt) == false)
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
}
var entity = Services.EntityService.Get(asInt, 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(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)
{
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;
}
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 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 != rootIdString)
{
id = altStartId;
//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
if (HasPathAccess(id, queryStrings) == false)
{
Logger.Warn("The user " + Security.CurrentUser.Username + " does not have access to the tree node " + id);
return new TreeNodeCollection();
}
// 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)
{
id = Constants.System.Root.ToString(CultureInfo.InvariantCulture);
}
}
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);
protected abstract UmbracoObjectTypes UmbracoObjectType { get; }
protected IEnumerable GetChildEntities(string id)
{
// use helper method to ensure we support both integer and guid lookups
if (int.TryParse(id, out int iid) == false)
{
var idEntity = GetEntityFromId(id);
if (idEntity == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
iid = idEntity.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)
{
return UserStartNodes.Length > 0
? Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes)
: Enumerable.Empty();
}
return Services.EntityService.GetChildren(iid, UmbracoObjectType).ToArray();
}
///
/// 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