2013-08-12 15:06:12 +02:00
using System ;
2017-09-01 00:32:01 +10:00
using System.Collections.Concurrent ;
2013-08-12 15:06:12 +02:00
using System.Collections.Generic ;
2017-09-01 00:32:01 +10:00
using System.ComponentModel ;
2013-08-12 15:06:12 +02:00
using System.Linq ;
2013-12-12 14:10:03 +11:00
using System.Net ;
using System.Net.Http ;
2013-08-12 15:06:12 +02:00
using System.Net.Http.Formatting ;
2013-12-12 14:10:03 +11:00
using System.Web.Http ;
2013-08-12 15:06:12 +02:00
using Umbraco.Core ;
2014-09-23 16:50:05 +10:00
using Umbraco.Core.Logging ;
2013-08-12 15:06:12 +02:00
using Umbraco.Core.Models ;
using Umbraco.Core.Models.EntityBase ;
2014-02-17 11:50:13 +11:00
using Umbraco.Core.Persistence ;
2013-10-08 12:38:27 +11:00
using Umbraco.Web.Models.Trees ;
2013-12-12 14:10:03 +11:00
using Umbraco.Web.WebApi.Filters ;
2013-08-12 15:06:12 +02:00
using umbraco ;
using umbraco.BusinessLogic.Actions ;
2013-11-18 22:29:19 +01:00
using System.Globalization ;
2013-08-12 15:06:12 +02:00
namespace Umbraco.Web.Trees
{
2013-09-26 15:55:38 +10:00
public abstract class ContentTreeControllerBase : TreeController
2013-08-12 15:06:12 +02:00
{
2013-12-12 14:10:03 +11:00
#region Actions
/// <summary>
/// Gets an individual tree node
/// </summary>
/// <param name="id"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
[HttpQueryStringFilter("queryStrings")]
public TreeNode GetTreeNode ( string id , FormDataCollection queryStrings )
{
int asInt ;
2018-02-27 01:17:21 +11:00
Guid asGuid = Guid . Empty ;
2013-12-12 14:10:03 +11:00
if ( int . TryParse ( id , out asInt ) == false )
{
2018-02-27 01:17:21 +11:00
if ( Guid . TryParse ( id , out asGuid ) == false )
{
throw new HttpResponseException ( Request . CreateResponse ( HttpStatusCode . NotFound ));
}
2013-12-12 14:10:03 +11:00
}
2018-02-27 01:17:21 +11:00
var entity = asGuid == Guid . Empty
? Services . EntityService . Get ( asInt , UmbracoObjectType )
: Services . EntityService . GetByKey ( asGuid , UmbracoObjectType );
2013-12-12 14:10:03 +11:00
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
2018-01-17 10:47:35 +01:00
2017-09-06 12:54:32 +10:00
/// <summary>
/// Ensure the noAccess metadata is applied for the root node if in dialog mode and the user doesn't have path access to it
/// </summary>
/// <param name="queryStrings"></param>
/// <returns></returns>
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 ;
2018-01-17 10:47:35 +01:00
}
2013-12-12 14:10:03 +11:00
protected abstract TreeNode GetSingleTreeNode ( IUmbracoEntity e , string parentId , FormDataCollection queryStrings );
2018-01-17 10:47:35 +01:00
2017-09-01 00:32:01 +10:00
/// <summary>
2018-01-17 10:47:35 +01:00
/// Returns a <see cref="TreeNode"/> for the <see cref="IUmbracoEntity"/> and
2017-09-01 00:32:01 +10:00
/// attaches some meta data to the node if the user doesn't have start node access to it when in dialog mode
/// </summary>
/// <param name="e"></param>
/// <param name="parentId"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
internal TreeNode GetSingleTreeNodeWithAccessCheck ( IUmbracoEntity e , string parentId , FormDataCollection queryStrings )
2018-01-17 10:47:35 +01:00
{
bool hasPathAccess ;
var entityIsAncestorOfStartNodes = Security . CurrentUser . IsInBranchOfStartNode ( e , Services . EntityService , RecycleBinId , out hasPathAccess );
if ( entityIsAncestorOfStartNodes == false )
return null ;
2017-09-06 00:38:31 +10:00
2018-01-17 10:47:35 +01:00
var treeNode = GetSingleTreeNode ( e , parentId , queryStrings );
2018-04-19 16:20:18 +10:00
if ( treeNode == null )
{
//this means that the user has NO access to this node via permissions! They at least need to have browse permissions to see
//the node so we need to return null;
return null ;
}
2017-09-06 00:38:31 +10:00
if ( hasPathAccess == false )
2017-09-01 00:32:01 +10:00
{
treeNode . AdditionalData [ "noAccess" ] = true ;
}
return treeNode ;
}
2013-12-12 14:10:03 +11:00
2013-08-12 15:06:12 +02:00
/// <summary>
2018-01-17 10:47:35 +01:00
/// Returns the
2013-08-12 15:06:12 +02:00
/// </summary>
protected abstract int RecycleBinId { get ; }
/// <summary>
/// Returns true if the recycle bin has items in it
/// </summary>
protected abstract bool RecycleBinSmells { get ; }
2013-10-01 16:38:10 +10:00
/// <summary>
/// Returns the user's start node for this tree
/// </summary>
2017-05-25 02:03:41 +10:00
protected abstract int [] UserStartNodes { get ; }
2018-01-17 10:47:35 +01:00
2014-09-23 16:50:05 +10:00
protected virtual TreeNodeCollection PerformGetTreeNodes ( string id , FormDataCollection queryStrings )
{
var nodes = new TreeNodeCollection ();
2018-01-17 10:47:35 +01:00
var rootIdString = Constants . System . Root . ToString ( CultureInfo . InvariantCulture );
var hasAccessToRoot = UserStartNodes . Contains ( Constants . System . Root );
2014-09-23 16:50:05 +10:00
2018-01-17 10:47:35 +01:00
var startNodeId = queryStrings . HasKey ( TreeQueryStringParameters . StartNodeId )
? queryStrings . GetValue < string >( TreeQueryStringParameters . StartNodeId )
: string . Empty ;
if ( string . IsNullOrEmpty ( startNodeId ) == false && startNodeId != "undefined" && startNodeId != rootIdString )
2014-09-23 16:50:05 +10:00
{
2018-01-17 10:47:35 +01:00
// request has been made to render from a specific, non-root, start node
id = startNodeId ;
2014-09-23 16:50:05 +10:00
2018-01-17 10:47:35 +01:00
// 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
2014-09-23 16:50:05 +10:00
if ( HasPathAccess ( id , queryStrings ) == false )
{
2018-01-17 10:47:35 +01:00
LogHelper . Warn < ContentTreeControllerBase >( "User " + Security . CurrentUser . Username + " does not have access to node with id " + id );
return nodes ;
2014-09-23 16:50:05 +10:00
}
2017-01-09 14:45:40 +01:00
2018-01-17 10:47:35 +01:00
// 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 )
2014-09-23 16:50:05 +10:00
{
2018-01-17 10:47:35 +01:00
var topNodes = Services . EntityService . GetAll ( UmbracoObjectType , topNodeIds . ToArray ());
nodes . AddRange ( topNodes . Select ( x => GetSingleTreeNodeWithAccessCheck ( x , id , queryStrings )). Where ( x => x != null ));
2014-09-23 16:50:05 +10:00
}
}
2017-01-09 14:45:40 +01:00
2014-09-23 16:50:05 +10:00
return nodes ;
2018-01-17 10:47:35 +01:00
}
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 ;
}
2014-01-17 13:00:11 +11:00
2013-08-12 15:06:12 +02:00
protected abstract MenuItemCollection PerformGetMenuForNode ( string id , FormDataCollection queryStrings );
protected abstract UmbracoObjectTypes UmbracoObjectType { get ; }
protected IEnumerable < IUmbracoEntity > GetChildEntities ( string id )
{
2018-01-17 10:47:35 +01:00
// try to parse id as an integer else use GetEntityFromId
// which will grok Guids, Udis, etc and let use obtain the id
2018-04-17 15:07:59 +02:00
if ( int . TryParse ( id , out var entityId ) == false )
2013-08-12 15:06:12 +02:00
{
2018-01-17 10:47:35 +01:00
var entity = GetEntityFromId ( id );
if ( entity == null )
2017-01-10 12:47:00 +01:00
throw new HttpResponseException ( HttpStatusCode . NotFound );
2018-01-17 10:47:35 +01:00
entityId = entity . Id ;
2013-08-12 15:06:12 +02:00
}
2017-01-09 14:45:40 +01:00
2018-06-20 14:35:48 +02:00
return Services . EntityService . GetChildren ( entityId , UmbracoObjectType ). ToList ();
2013-08-12 15:06:12 +02:00
}
2013-11-19 12:28:50 +11:00
/// <summary>
/// Returns true or false if the current user has access to the node based on the user's allowed start node (path) access
/// </summary>
/// <param name="id"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
2017-09-01 00:32:01 +10:00
//we should remove this in v8, it's now here for backwards compat only
2013-11-19 12:28:50 +11:00
protected abstract bool HasPathAccess ( string id , FormDataCollection queryStrings );
2017-09-01 00:32:01 +10:00
/// <summary>
/// Returns true or false if the current user has access to the node based on the user's allowed start node (path) access
/// </summary>
/// <param name="entity"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
protected bool HasPathAccess ( IUmbracoEntity entity , FormDataCollection queryStrings )
{
if ( entity == null ) return false ;
return Security . CurrentUser . HasPathAccess ( entity , Services . EntityService , RecycleBinId );
2018-01-17 10:47:35 +01:00
}
2017-09-01 00:32:01 +10:00
2013-08-12 15:06:12 +02:00
/// <summary>
2014-09-23 16:50:05 +10:00
/// Ensures the recycle bin is appended when required (i.e. user has access to the root and it's not in dialog mode)
2013-08-12 15:06:12 +02:00
/// </summary>
/// <param name="id"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
2014-09-23 16:50:05 +10:00
/// <remarks>
/// This method is overwritten strictly to render the recycle bin, it should serve no other purpose
/// </remarks>
2013-08-12 15:06:12 +02:00
protected sealed override TreeNodeCollection GetTreeNodes ( string id , FormDataCollection queryStrings )
{
2013-11-19 12:28:50 +11:00
//check if we're rendering the root
2017-07-26 08:35:08 +02:00
if ( id == Constants . System . Root . ToInvariantString () && UserStartNodes . Contains ( Constants . System . Root ))
2013-08-12 15:06:12 +02:00
{
2014-09-23 16:50:05 +10:00
var altStartId = string . Empty ;
2014-01-17 13:00:11 +11:00
2014-09-23 16:50:05 +10:00
if ( queryStrings . HasKey ( TreeQueryStringParameters . StartNodeId ))
altStartId = queryStrings . GetValue < string >( TreeQueryStringParameters . StartNodeId );
2013-11-19 11:06:09 +01:00
2018-01-17 10:47:35 +01:00
//check if a request has been made to render from a specific start node
2014-09-23 16:50:05 +10:00
if ( string . IsNullOrEmpty ( altStartId ) == false && altStartId != "undefined" && altStartId != Constants . System . Root . ToString ( CultureInfo . InvariantCulture ))
2013-11-19 12:28:50 +11:00
{
2014-09-23 16:50:05 +10:00
id = altStartId ;
2013-11-19 12:28:50 +11:00
}
2017-01-09 14:45:40 +01:00
2014-09-23 16:50:05 +10:00
var nodes = GetTreeNodesInternal ( id , queryStrings );
2017-01-09 14:45:40 +01:00
2014-09-23 16:50:05 +10:00
//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 ())
2013-09-04 17:36:05 +10:00
{
nodes . Add ( CreateTreeNode (
2013-10-24 16:54:17 +11:00
RecycleBinId . ToInvariantString (),
2014-09-23 16:50:05 +10:00
id ,
2013-09-04 17:36:05 +10:00
queryStrings ,
ui . GetText ( "general" , "recycleBin" ),
"icon-trash" ,
RecycleBinSmells ,
2014-06-08 16:15:28 +01:00
queryStrings . GetValue < string >( "application" ) + TreeAlias . EnsureStartsWith ( '/' ) + "/recyclebin" ));
2014-09-23 16:50:05 +10:00
2013-09-04 17:36:05 +10:00
}
2013-08-12 15:06:12 +02:00
return nodes ;
}
2013-12-04 15:54:50 +11:00
return GetTreeNodesInternal ( id , queryStrings );
}
2018-05-15 11:21:37 +02:00
/// <summary>
/// Check to see if we should return children of a container node
/// </summary>
/// <param name="e"></param>
/// <returns></returns>
/// <remarks>
/// This is required in case a user has custom start nodes that are children of a list view since in that case we'll need to render the tree node. In normal cases we don't render
/// children of a list view.
/// </remarks>
protected bool ShouldRenderChildrenOfContainer ( IUmbracoEntity e )
{
var isContainer = e . IsContainer ();
var renderChildren = e . HasChildren () && ( isContainer == false );
//Here we need to figure out if the node is a container and if so check if the user has a custom start node, then check if that start node is a child
// of this container node. If that is true, the HasChildren must be true so that the tree node still renders even though this current node is a container/list view.
if ( isContainer && UserStartNodes . Length > 0 && UserStartNodes . Contains ( Constants . System . Root ) == false )
{
var startNodes = Services . EntityService . GetAll ( UmbracoObjectType , UserStartNodes );
//if any of these start nodes' parent is current, then we need to render children normally so we need to switch some logic and tell
// the UI that this node does have children and that it isn't a container
if ( startNodes . Any ( x => x . ParentId == e . Id ))
{
renderChildren = true ;
}
}
return renderChildren ;
}
2013-12-04 15:54:50 +11:00
/// <summary>
/// Before we make a call to get the tree nodes we have to check if they can actually be rendered
/// </summary>
/// <param name="id"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
/// <remarks>
/// 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.
/// </remarks>
private TreeNodeCollection GetTreeNodesInternal ( string id , FormDataCollection queryStrings )
{
2017-09-01 00:32:01 +10:00
var current = GetEntityFromId ( id );
2017-01-09 14:45:40 +01:00
2013-11-15 16:19:46 +11:00
//before we get the children we need to see if this is a container node
2014-01-17 13:00:11 +11:00
2014-01-28 15:49:04 +01:00
//test if the parent is a listview / container
2018-05-15 11:21:37 +02:00
if ( current != null && ShouldRenderChildrenOfContainer ( current ) == false )
2013-11-15 16:19:46 +11:00
{
//no children!
return new TreeNodeCollection ();
}
2014-01-27 22:03:52 +01:00
2013-08-12 15:06:12 +02:00
return PerformGetTreeNodes ( id , queryStrings );
}
/// <summary>
/// Checks if the menu requested is for the recycle bin and renders that, otherwise renders the result of PerformGetMenuForNode
/// </summary>
/// <param name="id"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
protected sealed override MenuItemCollection GetMenuForNode ( string id , FormDataCollection queryStrings )
{
if ( RecycleBinId . ToInvariantString () == id )
{
var menu = new MenuItemCollection ();
2013-10-09 17:35:16 +11:00
menu . Items . Add < ActionEmptyTranscan >( ui . Text ( "actions" , "emptyTrashcan" ));
2013-10-03 15:05:48 +10:00
menu . Items . Add < ActionRefresh >( ui . Text ( "actions" , ActionRefresh . Instance . Alias ), true );
2013-08-12 15:06:12 +02:00
return menu ;
2018-01-17 10:47:35 +01:00
}
2017-09-06 10:45:38 +10:00
2013-08-12 15:06:12 +02:00
return PerformGetMenuForNode ( id , queryStrings );
}
/// <summary>
/// Based on the allowed actions, this will filter the ones that the current user is allowed
/// </summary>
2013-09-26 15:55:38 +10:00
/// <param name="menuWithAllItems"></param>
2013-08-12 15:06:12 +02:00
/// <param name="userAllowedMenuItems"></param>
/// <returns></returns>
2013-09-26 15:55:38 +10:00
protected void FilterUserAllowedMenuItems ( MenuItemCollection menuWithAllItems , IEnumerable < MenuItem > userAllowedMenuItems )
2013-08-12 15:06:12 +02:00
{
var userAllowedActions = userAllowedMenuItems . Where ( x => x . Action != null ). Select ( x => x . Action ). ToArray ();
2013-09-26 15:55:38 +10:00
2013-10-03 15:05:48 +10:00
var notAllowed = menuWithAllItems . Items . Where (
2013-09-26 15:55:38 +10:00
a => ( a . Action != null
&& a . Action . CanBePermissionAssigned
&& ( a . Action . CanBePermissionAssigned == false || userAllowedActions . Contains ( a . Action ) == false )))
. ToArray ();
//remove the ones that aren't allowed.
foreach ( var m in notAllowed )
{
2013-10-03 15:05:48 +10:00
menuWithAllItems . Items . Remove ( m );
2013-09-26 15:55:38 +10:00
}
2013-08-12 15:06:12 +02:00
}
2013-09-26 15:55:38 +10:00
internal IEnumerable < MenuItem > GetAllowedUserMenuItemsForNode ( IUmbracoEntity dd )
2013-08-12 15:06:12 +02:00
{
2017-09-06 11:49:00 +10:00
var actions = ActionsResolver . Current . FromActionSymbols ( Security . CurrentUser . GetPermissions ( dd . Path , Services . UserService ))
2018-01-17 10:47:35 +01:00
. ToList ();
2013-08-12 15:06:12 +02:00
// A user is allowed to delete their own stuff
2017-09-01 00:32:01 +10:00
if ( dd . CreatorId == Security . GetUserId () && actions . Contains ( ActionDelete . Instance ) == false )
2013-08-12 15:06:12 +02:00
actions . Add ( ActionDelete . Instance );
2013-09-26 15:55:38 +10:00
return actions . Select ( x => new MenuItem ( x ));
2013-08-12 15:06:12 +02:00
}
/// <summary>
/// Determins if the user has access to view the node/document
/// </summary>
/// <param name="doc">The Document to check permissions against</param>
/// <param name="allowedUserOptions">A list of MenuItems that the user has permissions to execute on the current document</param>
/// <remarks>By default the user must have Browse permissions to see the node in the Content tree</remarks>
2018-01-17 10:47:35 +01:00
/// <returns></returns>
2013-08-12 15:06:12 +02:00
internal bool CanUserAccessNode ( IUmbracoEntity doc , IEnumerable < MenuItem > allowedUserOptions )
{
return allowedUserOptions . Select ( x => x . Action ). OfType < ActionBrowse >(). Any ();
}
2018-01-17 10:47:35 +01:00
2017-01-09 14:45:40 +01:00
/// <summary>
2017-09-01 00:32:01 +10:00
/// this will parse the string into either a GUID or INT
2017-01-09 14:45:40 +01:00
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
2017-09-01 00:32:01 +10:00
internal Tuple < Guid ?, int? > GetIdentifierFromString ( string id )
2017-01-09 14:45:40 +01:00
{
2017-03-23 12:53:48 +11:00
Guid idGuid ;
2017-01-09 14:45:40 +01:00
int idInt ;
2017-02-08 16:15:27 +11:00
Udi idUdi ;
2017-01-09 14:45:40 +01:00
if ( Guid . TryParse ( id , out idGuid ))
{
2017-09-01 00:32:01 +10:00
return new Tuple < Guid ?, int? >( idGuid , null );
2017-01-09 14:45:40 +01:00
}
2017-09-01 00:32:01 +10:00
if ( int . TryParse ( id , out idInt ))
2017-01-09 14:45:40 +01:00
{
2017-09-01 00:32:01 +10:00
return new Tuple < Guid ?, int? >( null , idInt );
2017-01-09 14:45:40 +01:00
}
2017-09-01 00:32:01 +10:00
if ( Udi . TryParse ( id , out idUdi ))
2017-02-08 16:15:27 +11:00
{
var guidUdi = idUdi as GuidUdi ;
2017-09-01 00:32:01 +10:00
if ( guidUdi != null )
2018-01-17 10:47:35 +01:00
return new Tuple < Guid ?, int? >( guidUdi . Guid , null );
}
2017-01-09 14:45:40 +01:00
2017-09-01 00:32:01 +10:00
return null ;
2018-01-17 10:47:35 +01:00
}
2017-09-01 00:32:01 +10:00
/// <summary>
/// Get an entity via an id that can be either an integer, Guid or UDI
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
/// <remarks>
/// This object has it's own contextual cache for these lookups
/// </remarks>
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 ;
2018-01-17 10:47:35 +01:00
});
}
2017-09-01 00:32:01 +10:00
private readonly ConcurrentDictionary < string , IUmbracoEntity > _entityCache = new ConcurrentDictionary < string , IUmbracoEntity >();
2013-08-12 15:06:12 +02:00
}
2018-02-27 01:17:21 +11:00
}