Files
Umbraco-CMS/src/Umbraco.Web/Trees/ApplicationTreeController.cs

291 lines
13 KiB
C#
Raw Normal View History

2018-06-29 19:52:40 +02:00
using System;
using System.Collections.Generic;
2018-06-29 19:52:40 +02:00
using System.Linq;
using System.Net;
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;
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;
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;
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
{
/// <summary>
/// Used to return tree root nodes
/// </summary>
2018-06-29 19:52:40 +02:00
[AngularJsonOnlyConfiguration]
[PluginController("UmbracoTrees")]
public class ApplicationTreeController : UmbracoAuthorizedApiController
{
private readonly ITreeService _treeService;
private readonly ISectionService _sectionService;
public ApplicationTreeController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor,
2019-01-18 15:05:20 +01:00
ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger,
IRuntimeState runtimeState, ITreeService treeService, ISectionService sectionService, UmbracoHelper umbracoHelper)
: base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper)
{
_treeService = treeService;
_sectionService = sectionService;
}
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>
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
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);
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)
{
//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);
return TreeRootNode.CreateSingleTreeRoot(Constants.System.RootString, null, null, name, TreeNodeCollection.Empty, true);
}
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);
2019-01-22 17:23:30 +01:00
if (nodes.Count > 0)
{
var treeRootNode = TreeRootNode.CreateMultiTreeRoot(nodes);
treeRootNode.Name = name;
return treeRootNode;
}
// 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?
return TreeRootNode.CreateSingleTreeRoot(Constants.System.RootString, null, null, name, TreeNodeCollection.Empty, true);
}
2019-01-22 17:23:30 +01:00
// for many groups
var treeRootNodes = new List<TreeRootNode>();
foreach (var (groupName, trees) in groupedTrees)
{
2019-01-22 17:23:30 +01:00
var nodes = new TreeNodeCollection();
foreach (var t in trees)
{
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);
}
2019-01-22 17:23:30 +01:00
if (nodes.Count == 0)
continue;
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;
2019-01-22 17:23:30 +01:00
var groupRootNode = TreeRootNode.CreateGroupNode(nodes, application);
groupRootNode.Name = Services.TextService.Localize("treeHeaders/" + name);
treeRootNodes.Add(groupRootNode);
}
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
{
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
{
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
var sectionRoot = TreeRootNode.CreateSingleTreeRoot(
Constants.System.RootString,
2019-01-22 17:23:30 +01:00
rootNode.ChildNodesUrl,
rootNode.MenuUrl,
rootNode.Name,
children,
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;
2019-01-22 17:23:30 +01:00
foreach (var d in rootNode.AdditionalData)
sectionRoot.AdditionalData[d.Key] = d.Value;
2018-06-29 19:52:40 +02:00
2019-01-22 17:23:30 +01:00
return sectionRoot;
}
2018-06-29 19:52:40 +02:00
/// <summary>
2019-01-22 17:23:30 +01:00
/// Gets the root node of a tree.
/// </summary>
2019-01-22 17:23:30 +01:00
private async Task<TreeNode> GetRootNode(Tree tree, FormDataCollection querystring)
{
2019-01-22 17:23:30 +01:00
if (tree == null) throw new ArgumentNullException(nameof(tree));
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-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-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
}
/// <summary>
2019-01-22 17:23:30 +01:00
/// Gets a proxy to a controller for a specified action.
/// </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-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-22 17:23:30 +01:00
var context = ControllerContext;
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-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-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
}
}