using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; 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; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; using Umbraco.Web.Services; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; 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; public ApplicationTreeController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, 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; } /// /// 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, [System.Web.Http.ModelBinding.ModelBinder(typeof(HttpQueryStringModelBinder))]FormDataCollection 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 = Services.TextService.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 = Services.TextService.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 = Services.TextService.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, FormDataCollection 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, FormDataCollection 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, FormDataCollection 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, FormDataCollection 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"] = null; var proxyQuerystring = new FormDataCollection(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, FormDataCollection 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. var context = ControllerContext; // get the controller var controller = (ApiController) 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 = 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); // 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; } } }