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.Globalization;
|
|
|
|
|
|
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;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
using Umbraco.Core.Models;
|
2019-01-17 13:20:19 +11:00
|
|
|
|
using Umbraco.Core.Models.ContentEditing;
|
|
|
|
|
|
using Umbraco.Core.Persistence;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
using Umbraco.Core.Services;
|
2019-01-17 13:20:19 +11:00
|
|
|
|
using Umbraco.Web.Models.ContentEditing;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
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
|
2019-01-17 13:20:19 +11:00
|
|
|
|
{
|
2019-01-17 16:40:11 +11:00
|
|
|
|
private readonly ITreeService _treeService;
|
2019-01-17 13:20:19 +11:00
|
|
|
|
|
|
|
|
|
|
public ApplicationTreeController(IGlobalSettings globalSettings, UmbracoContext umbracoContext,
|
|
|
|
|
|
ISqlContext sqlContext, ServiceContext services, CacheHelper applicationCache, IProfilingLogger logger,
|
2019-01-17 16:40:11 +11:00
|
|
|
|
IRuntimeState runtimeState, ITreeService treeService)
|
2019-01-17 13:20:19 +11:00
|
|
|
|
: base(globalSettings, umbracoContext, sqlContext, services, applicationCache, logger, runtimeState)
|
|
|
|
|
|
{
|
|
|
|
|
|
_treeService = treeService;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
/// <param name="queryStrings"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
[HttpQueryStringFilter("queryStrings")]
|
2019-01-17 16:40:11 +11:00
|
|
|
|
public async Task<TreeRootNode> GetApplicationTrees(string application, string tree, FormDataCollection queryStrings)
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
2018-09-06 13:07:09 +02:00
|
|
|
|
application = application.CleanForXss();
|
|
|
|
|
|
|
2018-06-29 19:52:40 +02:00
|
|
|
|
if (string.IsNullOrEmpty(application)) throw new HttpResponseException(HttpStatusCode.NotFound);
|
|
|
|
|
|
|
|
|
|
|
|
//find all tree definitions that have the current application alias
|
2019-01-17 13:20:19 +11:00
|
|
|
|
var groupedTrees = _treeService.GetGroupedApplicationTrees(application);
|
2018-10-23 18:28:55 +11:00
|
|
|
|
var allTrees = groupedTrees.Values.SelectMany(x => x).ToList();
|
2018-06-29 19:52:40 +02:00
|
|
|
|
|
2018-10-25 09:06:53 +02:00
|
|
|
|
if (string.IsNullOrEmpty(tree) == false || allTrees.Count == 1)
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
2018-10-23 18:28:55 +11:00
|
|
|
|
var apptree = !tree.IsNullOrWhiteSpace()
|
2019-01-17 16:40:11 +11:00
|
|
|
|
? allTrees.FirstOrDefault(x => x.TreeAlias == tree)
|
2018-10-23 18:28:55 +11:00
|
|
|
|
: allTrees.FirstOrDefault();
|
2018-06-29 19:52:40 +02:00
|
|
|
|
|
|
|
|
|
|
if (apptree == null) throw new HttpResponseException(HttpStatusCode.NotFound);
|
|
|
|
|
|
|
|
|
|
|
|
var result = await GetRootForSingleAppTree(
|
|
|
|
|
|
apptree,
|
|
|
|
|
|
Constants.System.Root.ToString(CultureInfo.InvariantCulture),
|
|
|
|
|
|
queryStrings,
|
|
|
|
|
|
application);
|
|
|
|
|
|
|
2018-10-16 16:41:06 +11:00
|
|
|
|
//this will be null if it cannot convert to a single root section
|
2018-06-29 19:52:40 +02:00
|
|
|
|
if (result != null)
|
2018-10-08 13:33:14 +01:00
|
|
|
|
{
|
2018-10-16 16:41:06 +11:00
|
|
|
|
return result;
|
2018-10-08 13:33:14 +01:00
|
|
|
|
}
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2018-10-23 18:28:55 +11:00
|
|
|
|
//Don't apply fancy grouping logic futher down, if we only have one group of items
|
|
|
|
|
|
var hasGroups = groupedTrees.Count > 1;
|
|
|
|
|
|
if (!hasGroups)
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
2018-10-23 18:28:55 +11:00
|
|
|
|
var collection = new TreeNodeCollection();
|
|
|
|
|
|
foreach (var apptree in allTrees)
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
2018-10-23 18:28:55 +11:00
|
|
|
|
//return the root nodes for each tree in the app
|
|
|
|
|
|
var rootNode = await GetRootForMultipleAppTree(apptree, queryStrings);
|
|
|
|
|
|
//This could be null if the tree decides not to return it's root (i.e. the member type tree does this when not in umbraco membership mode)
|
|
|
|
|
|
if (rootNode != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
collection.Add(rootNode);
|
|
|
|
|
|
}
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2018-12-11 14:09:44 +00:00
|
|
|
|
if(collection.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
var multiTree = TreeRootNode.CreateMultiTreeRoot(collection);
|
|
|
|
|
|
multiTree.Name = Services.TextService.Localize("sections/" + application);
|
|
|
|
|
|
|
|
|
|
|
|
return multiTree;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//Otherwise its a application/section with no trees (aka a full screen app)
|
|
|
|
|
|
//For example we do not have a Forms tree definied in C# & can not attribute with [Tree(isSingleNodeTree:true0]
|
|
|
|
|
|
var rootId = Constants.System.Root.ToString(CultureInfo.InvariantCulture);
|
|
|
|
|
|
var section = Services.TextService.Localize("sections/" + application);
|
2018-10-08 13:33:14 +01:00
|
|
|
|
|
2018-12-11 14:09:44 +00:00
|
|
|
|
return TreeRootNode.CreateSingleTreeRoot(rootId, null, null, section, TreeNodeCollection.Empty, true);
|
2018-10-08 13:33:14 +01:00
|
|
|
|
}
|
2018-10-16 16:41:06 +11:00
|
|
|
|
|
|
|
|
|
|
var rootNodeGroups = new List<TreeRootNode>();
|
|
|
|
|
|
|
2018-10-11 09:16:44 +01:00
|
|
|
|
//Group trees by [CoreTree] attribute with a TreeGroup property
|
2018-10-23 18:28:55 +11:00
|
|
|
|
foreach (var treeSectionGroup in groupedTrees)
|
2018-10-08 13:33:14 +01:00
|
|
|
|
{
|
|
|
|
|
|
var treeGroupName = treeSectionGroup.Key;
|
|
|
|
|
|
|
2018-10-08 14:10:11 +01:00
|
|
|
|
var groupNodeCollection = new TreeNodeCollection();
|
2018-10-23 18:28:55 +11:00
|
|
|
|
foreach (var appTree in treeSectionGroup.Value)
|
2018-10-08 13:33:14 +01:00
|
|
|
|
{
|
2018-10-23 18:28:55 +11:00
|
|
|
|
var rootNode = await GetRootForMultipleAppTree(appTree, queryStrings);
|
|
|
|
|
|
if (rootNode != null)
|
2018-10-08 13:33:14 +01:00
|
|
|
|
{
|
2018-10-23 18:28:55 +11:00
|
|
|
|
//Add to a new list/collection
|
|
|
|
|
|
groupNodeCollection.Add(rootNode);
|
2018-10-08 13:33:14 +01:00
|
|
|
|
}
|
2018-10-08 14:10:11 +01:00
|
|
|
|
}
|
2018-10-08 13:33:14 +01:00
|
|
|
|
|
2018-10-08 14:10:11 +01:00
|
|
|
|
//If treeGroupName == null then its third party
|
2018-10-23 18:28:55 +11:00
|
|
|
|
if (treeGroupName.IsNullOrWhiteSpace())
|
2018-10-08 14:10:11 +01:00
|
|
|
|
{
|
|
|
|
|
|
//This is used for the localisation key
|
|
|
|
|
|
//treeHeaders/thirdPartyGroup
|
|
|
|
|
|
treeGroupName = "thirdPartyGroup";
|
2018-10-08 13:33:14 +01:00
|
|
|
|
}
|
2018-10-08 14:10:11 +01:00
|
|
|
|
|
2018-10-11 09:16:44 +01:00
|
|
|
|
if (groupNodeCollection.Count > 0)
|
2018-10-10 12:25:19 +01:00
|
|
|
|
{
|
2018-10-16 18:08:57 +11:00
|
|
|
|
var groupRoot = TreeRootNode.CreateGroupNode(groupNodeCollection, application);
|
2018-10-10 12:25:19 +01:00
|
|
|
|
groupRoot.Name = Services.TextService.Localize("treeHeaders/" + treeGroupName);
|
2018-10-08 14:10:11 +01:00
|
|
|
|
|
2018-10-10 12:25:19 +01:00
|
|
|
|
rootNodeGroups.Add(groupRoot);
|
|
|
|
|
|
}
|
2018-10-08 13:33:14 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2018-10-16 16:41:06 +11:00
|
|
|
|
return TreeRootNode.CreateGroupedMultiTreeRoot(new TreeNodeCollection(rootNodeGroups.OrderBy(x => x.Name)));
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Get the root node for an application with multiple trees
|
|
|
|
|
|
/// </summary>
|
2019-01-17 16:40:11 +11:00
|
|
|
|
/// <param name="tree"></param>
|
2018-06-29 19:52:40 +02:00
|
|
|
|
/// <param name="queryStrings"></param>
|
|
|
|
|
|
/// <returns></returns>
|
2019-01-17 17:33:38 +11:00
|
|
|
|
private async Task<TreeNode> GetRootForMultipleAppTree(Tree tree, FormDataCollection queryStrings)
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
2019-01-17 16:40:11 +11:00
|
|
|
|
if (tree == null) throw new ArgumentNullException(nameof(tree));
|
2018-06-29 19:52:40 +02:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2019-01-17 16:40:11 +11:00
|
|
|
|
var byControllerAttempt = await TryGetRootNodeFromControllerTree(tree, queryStrings, ControllerContext);
|
2018-06-29 19:52:40 +02:00
|
|
|
|
if (byControllerAttempt.Success)
|
|
|
|
|
|
{
|
|
|
|
|
|
return byControllerAttempt.Result;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (HttpResponseException)
|
|
|
|
|
|
{
|
|
|
|
|
|
//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.
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-01-17 16:40:11 +11:00
|
|
|
|
throw new ApplicationException("Could not get root node for tree type " + tree.TreeAlias);
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Get the root node for an application with one tree
|
|
|
|
|
|
/// </summary>
|
2019-01-17 16:40:11 +11:00
|
|
|
|
/// <param name="tree"></param>
|
2018-06-29 19:52:40 +02:00
|
|
|
|
/// <param name="id"></param>
|
|
|
|
|
|
/// <param name="queryStrings"></param>
|
|
|
|
|
|
/// <param name="application"></param>
|
|
|
|
|
|
/// <returns></returns>
|
2019-01-17 17:33:38 +11:00
|
|
|
|
private async Task<TreeRootNode> GetRootForSingleAppTree(Tree tree, string id, FormDataCollection queryStrings, string application)
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
|
|
|
|
|
var rootId = Constants.System.Root.ToString(CultureInfo.InvariantCulture);
|
2019-01-17 16:40:11 +11:00
|
|
|
|
if (tree == null) throw new ArgumentNullException(nameof(tree));
|
|
|
|
|
|
var byControllerAttempt = TryLoadFromControllerTree(tree, id, queryStrings, ControllerContext);
|
|
|
|
|
|
if (!byControllerAttempt.Success)
|
|
|
|
|
|
throw new ApplicationException("Could not render a tree for type " + tree.TreeAlias);
|
|
|
|
|
|
|
|
|
|
|
|
var rootNode = await TryGetRootNodeFromControllerTree(tree, queryStrings, ControllerContext);
|
|
|
|
|
|
if (rootNode.Success == false)
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
2019-01-17 16:40:11 +11:00
|
|
|
|
//This should really never happen if we've successfully got the children above.
|
|
|
|
|
|
throw new InvalidOperationException("Could not create root node for tree " + tree.TreeAlias);
|
|
|
|
|
|
}
|
2018-06-29 19:52:40 +02:00
|
|
|
|
|
2019-01-17 16:40:11 +11:00
|
|
|
|
var sectionRoot = TreeRootNode.CreateSingleTreeRoot(
|
|
|
|
|
|
rootId,
|
|
|
|
|
|
rootNode.Result.ChildNodesUrl,
|
|
|
|
|
|
rootNode.Result.MenuUrl,
|
|
|
|
|
|
rootNode.Result.Name,
|
|
|
|
|
|
byControllerAttempt.Result,
|
|
|
|
|
|
tree.IsSingleNodeTree);
|
2018-10-25 08:54:17 +02:00
|
|
|
|
|
2019-01-17 16:40:11 +11: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.Result.RoutePath;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
|
2019-01-17 16:40:11 +11:00
|
|
|
|
foreach (var d in rootNode.Result.AdditionalData)
|
|
|
|
|
|
{
|
|
|
|
|
|
sectionRoot.AdditionalData[d.Key] = d.Value;
|
|
|
|
|
|
}
|
|
|
|
|
|
return sectionRoot;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
|
2019-01-17 16:40:11 +11:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Proxies a request to the destination tree controller to get it's root tree node
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="appTree"></param>
|
|
|
|
|
|
/// <param name="formCollection"></param>
|
|
|
|
|
|
/// <param name="controllerContext"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// This ensures that authorization filters are applied to the sub request
|
|
|
|
|
|
/// </remarks>
|
2019-01-17 17:33:38 +11:00
|
|
|
|
private async Task<Attempt<TreeNode>> TryGetRootNodeFromControllerTree(Tree appTree, FormDataCollection formCollection, HttpControllerContext controllerContext)
|
2019-01-17 16:40:11 +11:00
|
|
|
|
{
|
|
|
|
|
|
//instantiate it, since we are proxying, we need to setup the instance with our current context
|
|
|
|
|
|
var instance = (TreeController)DependencyResolver.Current.GetService(appTree.TreeControllerType);
|
|
|
|
|
|
|
|
|
|
|
|
//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.
|
|
|
|
|
|
|
|
|
|
|
|
var urlHelper = controllerContext.Request.GetUrlHelper();
|
|
|
|
|
|
//create the proxied URL for the controller action
|
|
|
|
|
|
var proxiedUrl = controllerContext.Request.RequestUri.GetLeftPart(UriPartial.Authority) +
|
|
|
|
|
|
urlHelper.GetUmbracoApiService("GetRootNode", instance.GetType());
|
|
|
|
|
|
//add the query strings to it
|
|
|
|
|
|
proxiedUrl += "?" + formCollection.ToQueryString();
|
|
|
|
|
|
//create proxy route data specifying the action / controller to execute
|
|
|
|
|
|
var proxiedRouteData = new HttpRouteData(
|
|
|
|
|
|
controllerContext.RouteData.Route,
|
|
|
|
|
|
new HttpRouteValueDictionary(new { action = "GetRootNode", controller = ControllerExtensions.GetControllerName(instance.GetType()) }));
|
2018-06-29 19:52:40 +02:00
|
|
|
|
|
2019-01-17 16:40:11 +11:00
|
|
|
|
//create a proxied controller context
|
|
|
|
|
|
var proxiedControllerContext = new HttpControllerContext(
|
|
|
|
|
|
controllerContext.Configuration,
|
|
|
|
|
|
proxiedRouteData,
|
|
|
|
|
|
new HttpRequestMessage(HttpMethod.Get, proxiedUrl))
|
|
|
|
|
|
{
|
|
|
|
|
|
ControllerDescriptor = new HttpControllerDescriptor(controllerContext.ControllerDescriptor.Configuration, ControllerExtensions.GetControllerName(instance.GetType()), instance.GetType()),
|
|
|
|
|
|
RequestContext = controllerContext.RequestContext
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
instance.ControllerContext = proxiedControllerContext;
|
|
|
|
|
|
instance.Request = controllerContext.Request;
|
|
|
|
|
|
instance.RequestContext.RouteData = proxiedRouteData;
|
|
|
|
|
|
|
|
|
|
|
|
//invoke auth filters for this sub request
|
|
|
|
|
|
var result = await instance.ControllerContext.InvokeAuthorizationFiltersForRequest();
|
|
|
|
|
|
//if a result is returned it means they are unauthorized, just throw the response.
|
|
|
|
|
|
if (result != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new HttpResponseException(result);
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2019-01-17 16:40:11 +11:00
|
|
|
|
//return the root
|
|
|
|
|
|
var node = instance.GetRootNode(formCollection);
|
|
|
|
|
|
return node == null
|
|
|
|
|
|
? Attempt<TreeNode>.Fail(new InvalidOperationException("Could not return a root node for tree " + appTree.TreeAlias))
|
|
|
|
|
|
: Attempt<TreeNode>.Succeed(node);
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
2019-01-17 16:40:11 +11:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Proxies a request to the destination tree controller to get it's tree node collection
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="appTree"></param>
|
|
|
|
|
|
/// <param name="id"></param>
|
|
|
|
|
|
/// <param name="formCollection"></param>
|
|
|
|
|
|
/// <param name="controllerContext"></param>
|
|
|
|
|
|
/// <returns></returns>
|
2019-01-17 17:33:38 +11:00
|
|
|
|
private Attempt<TreeNodeCollection> TryLoadFromControllerTree(Tree appTree, string id, FormDataCollection formCollection, HttpControllerContext controllerContext)
|
2019-01-17 16:40:11 +11:00
|
|
|
|
{
|
|
|
|
|
|
// instantiate it, since we are proxying, we need to setup the instance with our current context
|
|
|
|
|
|
var instance = (TreeController)DependencyResolver.Current.GetService(appTree.TreeControllerType);
|
|
|
|
|
|
if (instance == null)
|
|
|
|
|
|
throw new Exception("Failed to create tree " + appTree.TreeControllerType + ".");
|
|
|
|
|
|
|
|
|
|
|
|
//TODO: Shouldn't we be applying the same proxying logic as above so that filters work? seems like an oversight
|
|
|
|
|
|
|
|
|
|
|
|
instance.ControllerContext = controllerContext;
|
|
|
|
|
|
instance.Request = controllerContext.Request;
|
|
|
|
|
|
|
|
|
|
|
|
// return its data
|
|
|
|
|
|
return Attempt.Succeed(instance.GetNodes(id, formCollection));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|