using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Primitives; using Umbraco.Core; using Umbraco.Core.Services; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Trees; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Common.Filters; using Umbraco.Web.Common.ModelBinders; using Umbraco.Web.Models.Trees; using Umbraco.Web.Services; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { /// /// Used to return tree root nodes /// [AngularJsonOnlyConfiguration] [PluginController("UmbracoTrees")] public class ApplicationTreeController : UmbracoAuthorizedApiController { private readonly ITreeService _treeService; private readonly ISectionService _sectionService; private readonly ILocalizedTextService _localizedTextService; private readonly IControllerFactory _controllerFactory; public ApplicationTreeController( ITreeService treeService, ISectionService sectionService, ILocalizedTextService localizedTextService, IControllerFactory controllerFactory ) { _treeService = treeService; _sectionService = sectionService; _localizedTextService = localizedTextService; _controllerFactory = controllerFactory; } /// /// Returns the tree nodes for an application /// /// The application to load tree for /// An optional single tree alias, if specified will only load the single tree for the request app /// /// Tree use. /// public async Task GetApplicationTrees(string application, string tree, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings, TreeUse use = TreeUse.Main) { application = application.CleanForXss(); if (string.IsNullOrEmpty(application)) throw new HttpResponseException(HttpStatusCode.NotFound); var section = _sectionService.GetByAlias(application); if (section == null) throw new HttpResponseException(HttpStatusCode.NotFound); //find all tree definitions that have the current application alias var groupedTrees = _treeService.GetBySectionGrouped(application, use); var allTrees = groupedTrees.Values.SelectMany(x => x).ToList(); 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 = _localizedTextService.Localize("sections/" + application); return TreeRootNode.CreateSingleTreeRoot(Constants.System.RootString, null, null, name, TreeNodeCollection.Empty, true); } // handle request for a specific tree / or when there is only one tree if (!tree.IsNullOrWhiteSpace() || allTrees.Count == 1) { var t = tree.IsNullOrWhiteSpace() ? allTrees[0] : allTrees.FirstOrDefault(x => x.TreeAlias == tree); if (t == null) throw new HttpResponseException(HttpStatusCode.NotFound); var treeRootNode = await GetTreeRootNode(t, Constants.System.Root, queryStrings); if (treeRootNode != null) return treeRootNode; throw new HttpResponseException(HttpStatusCode.NotFound); } // handle requests for all trees // for only 1 group if (groupedTrees.Count == 1) { var nodes = new TreeNodeCollection(); foreach (var t in allTrees) { var node = await TryGetRootNode(t, queryStrings); if (node != null) nodes.Add(node); } var name = _localizedTextService.Localize("sections/" + application); 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); } // for many groups var treeRootNodes = new List(); foreach (var (groupName, trees) in groupedTrees) { var nodes = new TreeNodeCollection(); foreach (var t in trees) { var node = await TryGetRootNode(t, queryStrings); if (node != null) nodes.Add(node); } if (nodes.Count == 0) continue; // no name => third party // use localization key treeHeaders/thirdPartyGroup // todo this is an odd convention var name = groupName.IsNullOrWhiteSpace() ? "thirdPartyGroup" : groupName; var groupRootNode = TreeRootNode.CreateGroupNode(nodes, application); groupRootNode.Name = _localizedTextService.Localize("treeHeaders/" + name); treeRootNodes.Add(groupRootNode); } return TreeRootNode.CreateGroupedMultiTreeRoot(new TreeNodeCollection(treeRootNodes.OrderBy(x => x.Name))); } /// /// Tries to get the root node of a tree. /// /// /// 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. /// private async Task TryGetRootNode(Tree tree, FormCollection querystring) { if (tree == null) throw new ArgumentNullException(nameof(tree)); try { return await GetRootNode(tree, querystring); } 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; } } /// /// Get the tree root node of a tree. /// private async Task GetTreeRootNode(Tree tree, int id, FormCollection querystring) { if (tree == null) throw new ArgumentNullException(nameof(tree)); var children = await GetChildren(tree, id, querystring); var rootNode = await GetRootNode(tree, querystring); var sectionRoot = TreeRootNode.CreateSingleTreeRoot( Constants.System.RootString, rootNode.ChildNodesUrl, rootNode.MenuUrl, rootNode.Name, children, tree.IsSingleNodeTree); // 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; foreach (var d in rootNode.AdditionalData) sectionRoot.AdditionalData[d.Key] = d.Value; return sectionRoot; } /// /// Gets the root node of a tree. /// private async Task GetRootNode(Tree tree, FormCollection querystring) { if (tree == null) throw new ArgumentNullException(nameof(tree)); 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; } /// /// Get the child nodes of a tree node. /// private async Task GetChildren(Tree tree, int id, FormCollection querystring) { if (tree == null) throw new ArgumentNullException(nameof(tree)); // 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(); d["id"] = StringValues.Empty; var proxyQuerystring = new FormCollection(d); var controller = (TreeController) await GetApiControllerProxy(tree.TreeControllerType, "GetNodes", proxyQuerystring); return controller.GetNodes(id.ToInvariantString(), querystring); } /// /// Gets a proxy to a controller for a specified action. /// /// The type of the controller. /// The action. /// The querystring. /// An instance of the controller. /// /// Creates an instance of the and initializes it with a route /// and context etc. so it can execute the specified . Runs the authorization /// filters for that action, to ensure that the user has permission to execute it. /// private async Task GetApiControllerProxy(Type controllerType, string action, FormCollection querystring) { // 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. // create proxy route data specifying the action & controller to execute var routeData = new RouteData(new RouteValueDictionary() { ["action"] = action, ["controller"] = controllerType.Name.Substring(0,controllerType.Name.Length-10) // remove controller part of name; }); var controllerContext = new ControllerContext( new ActionContext( HttpContext, routeData, new ControllerActionDescriptor() { ControllerTypeInfo = controllerType.GetTypeInfo() } )); var controller = (TreeController) _controllerFactory.CreateController(controllerContext); //TODO Refactor trees or reimplement this hacks to check authentication. //https://dev.azure.com/umbraco/D-Team%20Tracker/_workitems/edit/3694 // var context = ControllerContext; // // // get the controller // var controller = (TreeController) DependencyResolver.Current.GetService(controllerType) // ?? throw new Exception($"Failed to create controller of type {controllerType.FullName}."); // // // create the proxy URL for the controller action // var proxyUrl = HttpContext.Request.RequestUri.GetLeftPart(UriPartial.Authority) // + HttpContext.Request.GetUrlHelper().GetUmbracoApiService(action, controllerType) // + "?" + querystring.ToQueryString(); // // // // // create a proxy request // var proxyRequest = new HttpRequestMessage(HttpMethod.Get, proxyUrl); // // // 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; } } }