2018-06-29 19:52:40 +02: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.Net ;
2019-01-17 16:40:11 +11:00
using System.Net.Http ;
2018-06-29 19:52:40 +02:00
using System.Net.Http.Formatting ;
using System.Threading.Tasks ;
using System.Web.Http ;
2019-01-17 16:40:11 +11:00
using System.Web.Http.Controllers ;
using System.Web.Http.Routing ;
using System.Web.Mvc ;
2018-09-06 14:10:10 +02:00
using Umbraco.Core ;
2019-01-17 13:20:19 +11:00
using Umbraco.Core.Cache ;
using Umbraco.Core.Configuration ;
using Umbraco.Core.Logging ;
using Umbraco.Core.Persistence ;
2018-06-29 19:52:40 +02:00
using Umbraco.Core.Services ;
using Umbraco.Web.Models.Trees ;
using Umbraco.Web.Mvc ;
2019-01-17 13:20:19 +11:00
using Umbraco.Web.Services ;
2018-06-29 19:52:40 +02:00
using Umbraco.Web.WebApi ;
using Umbraco.Web.WebApi.Filters ;
using Constants = Umbraco . Core . Constants ;
namespace Umbraco.Web.Trees
{
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]
[PluginController("UmbracoTrees")]
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 ;
2019-01-17 13:20:19 +11:00
2019-02-14 12:40:45 +01:00
public ApplicationTreeController ( IGlobalSettings globalSettings , IUmbracoContextAccessor umbracoContextAccessor ,
2019-01-18 15:05:20 +01:00
ISqlContext sqlContext , ServiceContext services , AppCaches appCaches , IProfilingLogger logger ,
2019-02-12 12:22:36 +11:00
IRuntimeState runtimeState , ITreeService treeService , ISectionService sectionService , UmbracoHelper umbracoHelper )
2019-02-14 12:40:45 +01:00
: base ( globalSettings , umbracoContextAccessor , sqlContext , services , appCaches , logger , runtimeState , umbracoHelper )
2019-01-17 13:20:19 +11:00
{
_treeService = treeService ;
2019-02-12 12:22:36 +11:00
_sectionService = sectionService ;
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>
2019-01-30 19:48:42 +11:00
/// <param name="queryStrings"></param>
2019-01-22 17:23:30 +01:00
/// <param name="use">Tree use.</param>
2018-06-29 19:52:40 +02:00
/// <returns></returns>
2019-02-06 17:03:23 +11:00
public async Task < TreeRootNode > GetApplicationTrees ( string application , string tree , [ System . Web . Http . ModelBinding . ModelBinder ( typeof ( HttpQueryStringModelBinder ) ) ] FormDataCollection 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 ) )
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
2018-06-29 19:52:40 +02:00
2019-02-12 12:22:36 +11:00
var section = _sectionService . GetByAlias ( application ) ;
if ( section = = null )
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
2018-06-29 19:52:40 +02: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
{
//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
var name = Services . TextService . 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 )
throw new HttpResponseException ( HttpStatusCode . NotFound ) ;
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 ) ;
2019-01-22 17:23:30 +01:00
if ( treeRootNode ! = null )
return treeRootNode ;
2018-06-29 19:52:40 +02:00
2019-01-22 17:23:30 +01:00
throw new HttpResponseException ( HttpStatusCode . 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
{
2019-01-30 19:48:42 +11:00
var node = await TryGetRootNode ( t , queryStrings ) ;
2019-01-22 17:23:30 +01:00
if ( node ! = null )
nodes . Add ( node ) ;
2018-06-29 19:52:40 +02:00
}
2019-01-22 17:23:30 +01:00
var name = Services . TextService . 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
{
2019-01-30 19:48:42 +11:00
var node = await TryGetRootNode ( t , queryStrings ) ;
2019-01-22 17:23:30 +01:00
if ( node ! = null )
nodes . Add ( node ) ;
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 )
continue ;
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 ) ;
groupRootNode . Name = Services . TextService . Localize ( "treeHeaders/" + name ) ;
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>
/// <para>Returns null if the root node could not be obtained due to an HttpResponseException,
/// which probably indicates that the user isn't authorized to view that tree.</para>
/// </remarks>
private async Task < TreeNode > TryGetRootNode ( Tree tree , FormDataCollection querystring )
2018-06-29 19:52:40 +02:00
{
2019-01-17 16:40:11 +11:00
if ( tree = = null ) throw new ArgumentNullException ( nameof ( tree ) ) ;
2019-01-22 17:23:30 +01:00
2018-06-29 19:52:40 +02:00
try
{
2019-01-22 17:23:30 +01:00
return await GetRootNode ( tree , querystring ) ;
2018-06-29 19:52:40 +02:00
}
catch ( HttpResponseException )
{
2019-01-22 17:23:30 +01:00
// if this occurs its because 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.
2018-06-29 19:52:40 +02:00
return null ;
}
}
/// <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>
2019-01-22 17:23:30 +01:00
private async Task < TreeRootNode > GetTreeRootNode ( Tree tree , int id , FormDataCollection querystring )
2018-06-29 19:52:40 +02:00
{
2019-01-17 16:40:11 +11:00
if ( tree = = null ) throw new ArgumentNullException ( nameof ( tree ) ) ;
2019-01-22 17:23:30 +01:00
var children = await GetChildren ( tree , id , querystring ) ;
var rootNode = await GetRootNode ( tree , querystring ) ;
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 )
2019-01-17 16:40:11 +11:00
sectionRoot . AdditionalData [ d . Key ] = d . Value ;
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>
2019-01-22 17:23:30 +01:00
private async Task < TreeNode > GetRootNode ( Tree tree , FormDataCollection querystring )
2019-01-17 16:40:11 +11:00
{
2019-01-22 17:23:30 +01:00
if ( tree = = null ) throw new ArgumentNullException ( nameof ( tree ) ) ;
2019-01-17 16:40:11 +11:00
2019-01-22 17:23:30 +01:00
var controller = ( TreeController ) await GetApiControllerProxy ( tree . TreeControllerType , "GetRootNode" , querystring ) ;
var rootNode = controller . GetRootNode ( querystring ) ;
if ( rootNode = = null )
throw new InvalidOperationException ( $"Failed to get root node for tree \" { tree . TreeAlias } \ "." ) ;
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>
private async Task < TreeNodeCollection > GetChildren ( Tree tree , int id , FormDataCollection querystring )
{
if ( tree = = null ) throw new ArgumentNullException ( nameof ( tree ) ) ;
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).
var d = querystring ? . ToDictionary ( x = > x . Key , x = > x . Value ) ? ? new Dictionary < string , string > ( ) ;
d [ "id" ] = null ;
var proxyQuerystring = new FormDataCollection ( d ) ;
2018-06-29 19:52:40 +02:00
2019-01-22 17:23:30 +01:00
var controller = ( TreeController ) await GetApiControllerProxy ( tree . TreeControllerType , "GetNodes" , proxyQuerystring ) ;
return 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>
private async Task < object > GetApiControllerProxy ( Type controllerType , string action , FormDataCollection 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
// need to "trick" web-api into thinking that it is actually executing the proxied controller.
2019-01-17 16:40:11 +11:00
2019-01-22 17:23:30 +01:00
var context = ControllerContext ;
2019-01-17 16:40:11 +11:00
2019-01-22 17:23:30 +01:00
// get the controller
var controller = ( ApiController ) DependencyResolver . Current . GetService ( controllerType )
? ? throw new Exception ( $"Failed to create controller of type {controllerType.FullName}." ) ;
2019-01-17 16:40:11 +11:00
2019-01-22 17:23:30 +01:00
// create the proxy URL for the controller action
var proxyUrl = context . Request . RequestUri . GetLeftPart ( UriPartial . Authority )
+ context . Request . GetUrlHelper ( ) . GetUmbracoApiService ( action , controllerType )
+ "?" + querystring . ToQueryString ( ) ;
// create proxy route data specifying the action & controller to execute
var proxyRoute = new HttpRouteData (
context . RouteData . Route ,
new HttpRouteValueDictionary ( new { action , controller = ControllerExtensions . GetControllerName ( controllerType ) } ) ) ;
// create a proxy request
var proxyRequest = new HttpRequestMessage ( HttpMethod . Get , proxyUrl ) ;
2019-01-17 16:40:11 +11:00
2019-01-22 17:23:30 +01:00
// create a proxy controller context
var proxyContext = new HttpControllerContext ( context . Configuration , proxyRoute , proxyRequest )
{
ControllerDescriptor = new HttpControllerDescriptor ( context . ControllerDescriptor . Configuration , ControllerExtensions . GetControllerName ( controllerType ) , controllerType ) ,
RequestContext = context . RequestContext ,
Controller = controller
} ;
// wire everything
controller . ControllerContext = proxyContext ;
controller . Request = proxyContext . Request ;
controller . RequestContext . RouteData = proxyRoute ;
// auth
var authResult = await controller . ControllerContext . InvokeAuthorizationFiltersForRequest ( ) ;
if ( authResult ! = null )
throw new HttpResponseException ( authResult ) ;
return controller ;
}
2018-06-29 19:52:40 +02:00
}
}