2020-12-08 16:33:50 +11:00
using System ;
2018-10-08 13:33:14 +01:00
using System.Collections.Generic ;
2018-06-29 19:52:40 +02:00
using System.Linq ;
using System.Threading.Tasks ;
2020-06-08 13:14:23 +02:00
using Microsoft.AspNetCore.Http ;
using Microsoft.AspNetCore.Mvc ;
using Microsoft.AspNetCore.Mvc.Controllers ;
2020-09-22 13:19:54 +02:00
using Microsoft.AspNetCore.Mvc.Infrastructure ;
2020-06-08 13:14:23 +02:00
using Microsoft.AspNetCore.Routing ;
using Microsoft.Extensions.Primitives ;
2021-02-15 12:57:58 +01:00
using Umbraco.Cms.Core.Models.Trees ;
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Services ;
using Umbraco.Cms.Core.Trees ;
2021-02-10 11:11:18 +01:00
using Umbraco.Cms.Web.BackOffice.Controllers ;
using Umbraco.Cms.Web.BackOffice.Extensions ;
2021-02-10 11:42:04 +01:00
using Umbraco.Cms.Web.Common.Attributes ;
using Umbraco.Cms.Web.Common.Filters ;
using Umbraco.Cms.Web.Common.ModelBinders ;
2020-09-22 13:19:54 +02:00
using Umbraco.Extensions ;
2021-02-09 10:22:42 +01:00
using static Umbraco . Cms . Core . Constants . Web . Routing ;
using Constants = Umbraco . Cms . Core . Constants ;
2018-06-29 19:52:40 +02:00
2021-02-10 11:11:18 +01:00
namespace Umbraco.Cms.Web.BackOffice.Trees
2018-06-29 19:52:40 +02:00
{
2019-01-17 16:40:11 +11:00
/// <summary>
/// Used to return tree root nodes
/// </summary>
2018-06-29 19:52:40 +02:00
[AngularJsonOnlyConfiguration]
2020-09-23 08:55:25 +02:00
[PluginController(Constants.Web.Mvc.BackOfficeTreeArea)]
2018-06-29 19:52:40 +02:00
public class ApplicationTreeController : UmbracoAuthorizedApiController
2018-11-26 07:49:32 +01:00
{
2019-01-17 16:40:11 +11:00
private readonly ITreeService _treeService ;
2019-02-12 12:22:36 +11:00
private readonly ISectionService _sectionService ;
2020-06-08 13:14:23 +02:00
private readonly ILocalizedTextService _localizedTextService ;
private readonly IControllerFactory _controllerFactory ;
2020-09-22 13:19:54 +02:00
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider ;
2021-02-15 18:50:16 +11:00
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationTreeController"/> class.
/// </summary>
2020-06-08 13:14:23 +02:00
public ApplicationTreeController (
ITreeService treeService ,
ISectionService sectionService ,
ILocalizedTextService localizedTextService ,
2020-09-22 13:19:54 +02:00
IControllerFactory controllerFactory ,
2021-02-15 18:50:16 +11:00
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider )
2020-12-08 16:33:50 +11:00
{
2019-01-17 13:20:19 +11:00
_treeService = treeService ;
2019-02-12 12:22:36 +11:00
_sectionService = sectionService ;
2020-06-08 13:14:23 +02:00
_localizedTextService = localizedTextService ;
_controllerFactory = controllerFactory ;
2020-09-22 13:19:54 +02:00
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider ;
2020-12-08 16:33:50 +11:00
}
2019-01-17 13:20:19 +11:00
2018-06-29 19:52:40 +02:00
/// <summary>
/// Returns the tree nodes for an application
/// </summary>
/// <param name="application">The application to load tree for</param>
/// <param name="tree">An optional single tree alias, if specified will only load the single tree for the request app</param>
2021-02-15 18:50:16 +11:00
/// <param name="queryStrings">The query strings</param>
2019-01-22 17:23:30 +01:00
/// <param name="use">Tree use.</param>
2021-01-12 16:24:50 +01:00
public async Task < ActionResult < TreeRootNode > > GetApplicationTrees ( string application , string tree , [ ModelBinder ( typeof ( HttpQueryStringModelBinder ) ) ] FormCollection queryStrings , TreeUse use = TreeUse . Main )
2018-06-29 19:52:40 +02:00
{
2018-09-06 13:07:09 +02:00
application = application . CleanForXss ( ) ;
2019-01-22 17:23:30 +01:00
if ( string . IsNullOrEmpty ( application ) )
2021-02-15 18:50:16 +11:00
{
2021-01-12 16:24:50 +01:00
return NotFound ( ) ;
2021-02-15 18:50:16 +11:00
}
2018-06-29 19:52:40 +02:00
2019-02-12 12:22:36 +11:00
var section = _sectionService . GetByAlias ( application ) ;
if ( section = = null )
2021-02-15 18:50:16 +11:00
{
2021-01-12 16:24:50 +01:00
return NotFound ( ) ;
2021-02-15 18:50:16 +11:00
}
2019-02-12 12:22:36 +11:00
2021-02-15 18:50:16 +11:00
// find all tree definitions that have the current application alias
2019-01-22 17:23:30 +01:00
var groupedTrees = _treeService . GetBySectionGrouped ( application , use ) ;
2018-10-23 18:28:55 +11:00
var allTrees = groupedTrees . Values . SelectMany ( x = > x ) . ToList ( ) ;
2018-06-29 19:52:40 +02:00
2019-01-22 17:23:30 +01:00
if ( allTrees . Count = = 0 )
2019-02-12 12:22:36 +11:00
{
2021-02-15 18:50:16 +11:00
// if there are no trees defined for this section but the section is defined then we can have a simple
// full screen section without trees
2020-06-08 13:14:23 +02:00
var name = _localizedTextService . Localize ( "sections/" + application ) ;
2019-03-29 12:06:23 +00:00
return TreeRootNode . CreateSingleTreeRoot ( Constants . System . RootString , null , null , name , TreeNodeCollection . Empty , true ) ;
2019-02-14 12:40:45 +01:00
}
2019-01-22 17:23:30 +01:00
// handle request for a specific tree / or when there is only one tree
if ( ! tree . IsNullOrWhiteSpace ( ) | | allTrees . Count = = 1 )
2018-06-29 19:52:40 +02:00
{
2019-01-22 17:23:30 +01:00
var t = tree . IsNullOrWhiteSpace ( )
? allTrees [ 0 ]
: allTrees . FirstOrDefault ( x = > x . TreeAlias = = tree ) ;
2018-06-29 19:52:40 +02:00
2019-01-22 17:23:30 +01:00
if ( t = = null )
2021-02-15 18:50:16 +11:00
{
2021-01-12 16:24:50 +01:00
return NotFound ( ) ;
2021-02-15 18:50:16 +11:00
}
2018-06-29 19:52:40 +02:00
2019-01-30 19:48:42 +11:00
var treeRootNode = await GetTreeRootNode ( t , Constants . System . Root , queryStrings ) ;
2021-01-14 19:41:32 +01:00
2019-01-22 17:23:30 +01:00
if ( treeRootNode ! = null )
2021-01-14 19:41:32 +01:00
{
2019-01-22 17:23:30 +01:00
return treeRootNode ;
2021-01-14 19:41:32 +01:00
}
2018-06-29 19:52:40 +02:00
2021-01-12 16:24:50 +01:00
return NotFound ( ) ;
2018-06-29 19:52:40 +02:00
}
2019-01-22 17:23:30 +01:00
// handle requests for all trees
// for only 1 group
if ( groupedTrees . Count = = 1 )
2018-06-29 19:52:40 +02:00
{
2019-01-22 17:23:30 +01:00
var nodes = new TreeNodeCollection ( ) ;
foreach ( var t in allTrees )
2018-06-29 19:52:40 +02:00
{
2021-01-14 19:41:32 +01:00
var nodeResult = await TryGetRootNode ( t , queryStrings ) ;
if ( ! ( nodeResult . Result is null ) )
{
return nodeResult . Result ;
}
2021-02-15 18:50:16 +11:00
2021-01-14 19:41:32 +01:00
var node = nodeResult . Value ;
2019-01-22 17:23:30 +01:00
if ( node ! = null )
2021-02-15 18:50:16 +11:00
{
2019-01-22 17:23:30 +01:00
nodes . Add ( node ) ;
2021-02-15 18:50:16 +11:00
}
2018-06-29 19:52:40 +02:00
}
2020-06-08 13:14:23 +02:00
var name = _localizedTextService . Localize ( "sections/" + application ) ;
2018-12-11 14:09:44 +00:00
2019-01-22 17:23:30 +01:00
if ( nodes . Count > 0 )
{
var treeRootNode = TreeRootNode . CreateMultiTreeRoot ( nodes ) ;
treeRootNode . Name = name ;
return treeRootNode ;
2018-12-11 14:09:44 +00:00
}
2019-02-12 12:22:36 +11:00
// otherwise it's a section with all empty trees, aka a fullscreen section
// todo is this true? what if we just failed to TryGetRootNode on all of them? SD: Yes it's true but we should check the result of TryGetRootNode and throw?
2019-03-29 12:06:23 +00:00
return TreeRootNode . CreateSingleTreeRoot ( Constants . System . RootString , null , null , name , TreeNodeCollection . Empty , true ) ;
2018-10-08 13:33:14 +01:00
}
2018-10-16 16:41:06 +11:00
2019-01-22 17:23:30 +01:00
// for many groups
var treeRootNodes = new List < TreeRootNode > ( ) ;
foreach ( var ( groupName , trees ) in groupedTrees )
2018-10-08 13:33:14 +01:00
{
2019-01-22 17:23:30 +01:00
var nodes = new TreeNodeCollection ( ) ;
foreach ( var t in trees )
2018-10-08 13:33:14 +01:00
{
2021-01-14 19:41:32 +01:00
var nodeResult = await TryGetRootNode ( t , queryStrings ) ;
if ( ! ( nodeResult . Result is null ) )
{
return nodeResult . Result ;
}
var node = nodeResult . Value ;
2019-01-22 17:23:30 +01:00
if ( node ! = null )
2021-02-15 18:50:16 +11:00
{
2019-01-22 17:23:30 +01:00
nodes . Add ( node ) ;
2021-02-15 18:50:16 +11:00
}
2018-10-08 14:10:11 +01:00
}
2018-10-08 13:33:14 +01:00
2019-01-22 17:23:30 +01:00
if ( nodes . Count = = 0 )
2021-02-15 18:50:16 +11:00
{
2019-01-22 17:23:30 +01:00
continue ;
2021-02-15 18:50:16 +11:00
}
2018-10-08 14:10:11 +01:00
2019-01-22 17:23:30 +01:00
// no name => third party
// use localization key treeHeaders/thirdPartyGroup
// todo this is an odd convention
var name = groupName . IsNullOrWhiteSpace ( ) ? "thirdPartyGroup" : groupName ;
2018-10-08 14:10:11 +01:00
2019-01-22 17:23:30 +01:00
var groupRootNode = TreeRootNode . CreateGroupNode ( nodes , application ) ;
2020-06-08 13:14:23 +02:00
groupRootNode . Name = _localizedTextService . Localize ( "treeHeaders/" + name ) ;
2019-01-22 17:23:30 +01:00
treeRootNodes . Add ( groupRootNode ) ;
2018-10-08 13:33:14 +01:00
}
2019-01-22 17:23:30 +01:00
return TreeRootNode . CreateGroupedMultiTreeRoot ( new TreeNodeCollection ( treeRootNodes . OrderBy ( x = > x . Name ) ) ) ;
2018-06-29 19:52:40 +02:00
}
/// <summary>
2019-01-22 17:23:30 +01:00
/// Tries to get the root node of a tree.
2018-06-29 19:52:40 +02:00
/// </summary>
2019-01-22 17:23:30 +01:00
/// <remarks>
2021-01-13 16:17:39 +01:00
/// <para>Returns null if the root node could not be obtained due to that
/// the user isn't authorized to view that tree. In this case since we are
/// loading multiple trees we will just return null so that it's not added
/// to the list</para>
2019-01-22 17:23:30 +01:00
/// </remarks>
2021-01-14 19:41:32 +01:00
private async Task < ActionResult < TreeNode > > TryGetRootNode ( Tree tree , FormCollection querystring )
2018-06-29 19:52:40 +02:00
{
2020-12-08 16:33:50 +11:00
if ( tree = = null )
2021-02-15 18:50:16 +11:00
{
2020-12-08 16:33:50 +11:00
throw new ArgumentNullException ( nameof ( tree ) ) ;
2021-02-15 18:50:16 +11:00
}
2019-01-22 17:23:30 +01:00
2021-01-13 16:17:39 +01:00
return await GetRootNode ( tree , querystring ) ;
2018-06-29 19:52:40 +02:00
}
/// <summary>
2019-01-22 17:23:30 +01:00
/// Get the tree root node of a tree.
2018-06-29 19:52:40 +02:00
/// </summary>
2021-01-14 19:41:32 +01:00
private async Task < ActionResult < TreeRootNode > > GetTreeRootNode ( Tree tree , int id , FormCollection querystring )
2018-06-29 19:52:40 +02:00
{
2020-12-08 16:33:50 +11:00
if ( tree = = null )
2021-02-15 18:50:16 +11:00
{
2020-12-08 16:33:50 +11:00
throw new ArgumentNullException ( nameof ( tree ) ) ;
2021-02-15 18:50:16 +11:00
}
2019-01-17 16:40:11 +11:00
2021-01-14 19:41:32 +01:00
var childrenResult = await GetChildren ( tree , id , querystring ) ;
if ( ! ( childrenResult . Result is null ) )
{
return new ActionResult < TreeRootNode > ( childrenResult . Result ) ;
}
var children = childrenResult . Value ;
var rootNodeResult = await GetRootNode ( tree , querystring ) ;
if ( ! ( rootNodeResult . Result is null ) )
{
return rootNodeResult . Result ;
}
var rootNode = rootNodeResult . Value ;
2018-06-29 19:52:40 +02:00
2019-01-17 16:40:11 +11:00
var sectionRoot = TreeRootNode . CreateSingleTreeRoot (
2019-03-29 12:06:23 +00:00
Constants . System . RootString ,
2019-01-22 17:23:30 +01:00
rootNode . ChildNodesUrl ,
rootNode . MenuUrl ,
rootNode . Name ,
children ,
2019-01-17 16:40:11 +11:00
tree . IsSingleNodeTree ) ;
2018-06-29 19:52:40 +02:00
2019-01-22 17:23:30 +01:00
// assign the route path based on the root node, this means it will route there when the
// section is navigated to and no dashboards will be available for this section
sectionRoot . RoutePath = rootNode . RoutePath ;
sectionRoot . Path = rootNode . Path ;
2018-10-25 08:54:17 +02:00
2019-01-22 17:23:30 +01:00
foreach ( var d in rootNode . AdditionalData )
2021-02-15 18:50:16 +11:00
{
2019-01-17 16:40:11 +11:00
sectionRoot . AdditionalData [ d . Key ] = d . Value ;
2021-02-15 18:50:16 +11:00
}
2018-06-29 19:52:40 +02:00
2019-01-22 17:23:30 +01:00
return sectionRoot ;
2019-01-17 16:40:11 +11:00
}
2018-06-29 19:52:40 +02:00
2019-01-17 16:40:11 +11:00
/// <summary>
2019-01-22 17:23:30 +01:00
/// Gets the root node of a tree.
2019-01-17 16:40:11 +11:00
/// </summary>
2021-01-14 19:41:32 +01:00
private async Task < ActionResult < TreeNode > > GetRootNode ( Tree tree , FormCollection querystring )
2019-01-17 16:40:11 +11:00
{
2020-12-08 16:33:50 +11:00
if ( tree = = null )
2021-02-15 18:50:16 +11:00
{
2020-12-08 16:33:50 +11:00
throw new ArgumentNullException ( nameof ( tree ) ) ;
2021-02-15 18:50:16 +11:00
}
2019-01-17 16:40:11 +11:00
2021-01-13 16:17:39 +01:00
var result = await GetApiControllerProxy ( tree . TreeControllerType , "GetRootNode" , querystring ) ;
// return null if the user isn't authorized to view that tree
if ( ! ( ( ForbidResult ) result . Result is null ) )
{
return null ;
}
var controller = ( TreeControllerBase ) result . Value ;
2021-02-23 16:09:36 +01:00
var rootNodeResult = await controller . GetRootNode ( querystring ) ;
2021-01-14 19:41:32 +01:00
if ( ! ( rootNodeResult . Result is null ) )
{
return rootNodeResult . Result ;
}
var rootNode = rootNodeResult . Value ;
2019-01-22 17:23:30 +01:00
if ( rootNode = = null )
2021-02-15 18:50:16 +11:00
{
2019-01-22 17:23:30 +01:00
throw new InvalidOperationException ( $"Failed to get root node for tree \" { tree . TreeAlias } \ "." ) ;
2021-02-15 18:50:16 +11:00
}
2019-01-22 17:23:30 +01:00
return rootNode ;
}
2019-01-17 16:40:11 +11:00
2019-01-22 17:23:30 +01:00
/// <summary>
/// Get the child nodes of a tree node.
/// </summary>
2021-01-14 19:41:32 +01:00
private async Task < ActionResult < TreeNodeCollection > > GetChildren ( Tree tree , int id , FormCollection querystring )
2019-01-22 17:23:30 +01:00
{
2020-12-08 16:33:50 +11:00
if ( tree = = null )
2021-02-15 18:50:16 +11:00
{
2020-12-08 16:33:50 +11:00
throw new ArgumentNullException ( nameof ( tree ) ) ;
2021-02-15 18:50:16 +11:00
}
2019-01-17 16:40:11 +11:00
2019-01-22 17:23:30 +01:00
// the method we proxy has an 'id' parameter which is *not* in the querystring,
// we need to add it for the proxy to work (else, it does not find the method,
// when trying to run auth filters etc).
2020-06-08 13:14:23 +02:00
var d = querystring ? . ToDictionary ( x = > x . Key , x = > x . Value ) ? ? new Dictionary < string , StringValues > ( ) ;
d [ "id" ] = StringValues . Empty ;
var proxyQuerystring = new FormCollection ( d ) ;
2018-06-29 19:52:40 +02:00
2021-01-14 19:41:32 +01:00
var controllerResult = await GetApiControllerProxy ( tree . TreeControllerType , "GetNodes" , proxyQuerystring ) ;
if ( ! ( controllerResult . Result is null ) )
{
return new ActionResult < TreeNodeCollection > ( controllerResult . Result ) ;
}
var controller = ( TreeControllerBase ) controllerResult . Value ;
2021-02-23 16:09:36 +01:00
return await controller . GetNodes ( id . ToInvariantString ( ) , querystring ) ;
2018-06-29 19:52:40 +02:00
}
2019-01-17 16:40:11 +11:00
/// <summary>
2019-01-22 17:23:30 +01:00
/// Gets a proxy to a controller for a specified action.
2019-01-17 16:40:11 +11:00
/// </summary>
2019-01-22 17:23:30 +01:00
/// <param name="controllerType">The type of the controller.</param>
/// <param name="action">The action.</param>
/// <param name="querystring">The querystring.</param>
/// <returns>An instance of the controller.</returns>
/// <remarks>
/// <para>Creates an instance of the <paramref name="controllerType"/> and initializes it with a route
/// and context etc. so it can execute the specified <paramref name="action"/>. Runs the authorization
/// filters for that action, to ensure that the user has permission to execute it.</para>
/// </remarks>
2021-01-12 16:24:50 +01:00
private async Task < ActionResult < object > > GetApiControllerProxy ( Type controllerType , string action , FormCollection querystring )
2019-01-17 16:40:11 +11:00
{
2019-01-22 17:23:30 +01:00
// note: this is all required in order to execute the auth-filters for the sub request, we
2020-09-22 13:19:54 +02:00
// need to "trick" mvc into thinking that it is actually executing the proxied controller.
2019-01-22 17:23:30 +01:00
2021-02-17 12:00:57 +01:00
var controllerName = ControllerExtensions . GetControllerName ( controllerType ) ;
2021-02-15 18:50:16 +11:00
2019-01-22 17:23:30 +01:00
// create proxy route data specifying the action & controller to execute
2020-06-08 13:14:23 +02:00
var routeData = new RouteData ( new RouteValueDictionary ( )
2019-01-22 17:23:30 +01:00
{
2021-02-05 13:14:24 +11:00
[ActionToken] = action ,
[ControllerToken] = controllerName
2020-06-08 13:14:23 +02:00
} ) ;
2020-09-22 13:19:54 +02:00
if ( ! ( querystring is null ) )
{
2020-12-08 16:33:50 +11:00
foreach ( var ( key , value ) in querystring )
2020-09-22 13:19:54 +02:00
{
routeData . Values [ key ] = value ;
}
}
var actionDescriptor = _actionDescriptorCollectionProvider . ActionDescriptors . Items
. Cast < ControllerActionDescriptor > ( )
. First ( x = >
x . ControllerName . Equals ( controllerName ) & &
x . ActionName = = action ) ;
2020-06-08 13:14:23 +02:00
2020-09-22 13:19:54 +02:00
var actionContext = new ActionContext ( HttpContext , routeData , actionDescriptor ) ;
var proxyControllerContext = new ControllerContext ( actionContext ) ;
2020-12-08 16:33:50 +11:00
var controller = ( TreeController ) _controllerFactory . CreateController ( proxyControllerContext ) ;
2020-06-08 13:14:23 +02:00
2021-02-15 18:50:16 +11:00
// TODO: What about other filters? Will they execute?
2020-12-08 16:33:50 +11:00
var isAllowed = await controller . ControllerContext . InvokeAuthorizationFiltersForRequest ( actionContext ) ;
if ( ! isAllowed )
2021-02-15 18:50:16 +11:00
{
2021-01-12 16:24:50 +01:00
return Forbid ( ) ;
2021-02-15 18:50:16 +11:00
}
2019-01-22 17:23:30 +01:00
return controller ;
}
2018-06-29 19:52:40 +02:00
}
}