using System; using System.Collections.Concurrent; using System.Linq; using System.Management.Instrumentation; 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.Web.Models.Trees; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using umbraco.cms.presentation.Trees; using Umbraco.Core.Services; using ApplicationTree = Umbraco.Core.Models.ApplicationTree; using UrlHelper = System.Web.Http.Routing.UrlHelper; namespace Umbraco.Web.Trees { internal static class ApplicationTreeExtensions { private static readonly ConcurrentDictionary TreeAttributeCache = new ConcurrentDictionary(); internal static TreeAttribute GetTreeAttribute(this Type treeControllerType) { return TreeAttributeCache.GetOrAdd(treeControllerType, type => { //Locate the tree attribute var treeAttributes = type .GetCustomAttributes(false) .ToArray(); if (treeAttributes.Length == 0) { throw new InvalidOperationException("The Tree controller is missing the " + typeof(TreeAttribute).FullName + " attribute"); } //assign the properties of this object to those of the metadata attribute return treeAttributes[0]; }); } internal static TreeAttribute GetTreeAttribute(this ApplicationTree tree) { return tree.GetRuntimeType().GetTreeAttribute(); } internal static string GetRootNodeDisplayName(this TreeAttribute attribute, ILocalizedTextService textService) { //if title is defined, return that if (string.IsNullOrEmpty(attribute.Title) == false) return attribute.Title; //try to look up a tree header matching the tree alias var localizedLabel = textService.Localize("treeHeaders/" + attribute.Alias); if (string.IsNullOrEmpty(localizedLabel) == false) return localizedLabel; //is returned to signal that a label was not found return "[" + attribute.Alias + "]"; } internal static Attempt TryGetControllerTree(this ApplicationTree appTree) { //get reference to all TreeApiControllers var controllerTrees = UmbracoApiControllerResolver.Current.RegisteredUmbracoApiControllers .Where(TypeHelper.IsTypeAssignableFrom) .ToArray(); //find the one we're looking for var foundControllerTree = controllerTrees.FirstOrDefault(x => x == appTree.GetRuntimeType()); if (foundControllerTree == null) { return Attempt.Fail(new InstanceNotFoundException("Could not find tree of type " + appTree.Type + " in any loaded DLLs")); } return Attempt.Succeed(foundControllerTree); } /// /// This will go and get the root node from a controller tree by executing the tree's GetRootNode method /// /// /// /// /// /// /// This ensures that authorization filters are applied to the sub request /// internal static async Task> TryGetRootNodeFromControllerTree(this ApplicationTree appTree, FormDataCollection formCollection, HttpControllerContext controllerContext) { var foundControllerTreeAttempt = appTree.TryGetControllerTree(); if (foundControllerTreeAttempt.Success == false) { return Attempt.Fail(foundControllerTreeAttempt.Exception); } var foundControllerTree = foundControllerTreeAttempt.Result; //instantiate it, since we are proxying, we need to setup the instance with our current context var instance = (TreeController)DependencyResolver.Current.GetService(foundControllerTree); //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())})); //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()) }; if (WebApiVersionCheck.WebApiVersion >= Version.Parse("5.0.0")) { //In WebApi2, this is required to be set: // proxiedControllerContext.RequestContext = controllerContext.RequestContext // but we need to do this with reflection because of codebase changes between version 4/5 //NOTE: Use TypeHelper here since the reflection is cached var controllerContextRequestContext = TypeHelper.GetProperty(controllerContext.GetType(), "RequestContext").GetValue(controllerContext); TypeHelper.GetProperty(proxiedControllerContext.GetType(), "RequestContext").SetValue(proxiedControllerContext, controllerContextRequestContext); } instance.ControllerContext = proxiedControllerContext; instance.Request = controllerContext.Request; if (WebApiVersionCheck.WebApiVersion >= Version.Parse("5.0.0")) { //now we can change the request context's route data to be the proxied route data - NOTE: we cannot do this directly above // because it will detect that the request context is different throw an exception. This is a change in webapi2 and we need to set // this with reflection due to codebase changes between version 4/5 // instance.RequestContext.RouteData = proxiedRouteData; //NOTE: Use TypeHelper here since the reflection is cached var instanceRequestContext = TypeHelper.GetProperty(typeof(ApiController), "RequestContext").GetValue(instance); TypeHelper.GetProperty(instanceRequestContext.GetType(), "RouteData").SetValue(instanceRequestContext, 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); } //return the root var node = instance.GetRootNode(formCollection); return node == null ? Attempt.Fail(new InvalidOperationException("Could not return a root node for tree " + appTree.Type)) : Attempt.Succeed(node); } internal static Attempt TryLoadFromControllerTree(this ApplicationTree appTree, string id, FormDataCollection formCollection, HttpControllerContext controllerContext) { var foundControllerTreeAttempt = appTree.TryGetControllerTree(); if (foundControllerTreeAttempt.Success == false) { return Attempt.Fail(foundControllerTreeAttempt.Exception); } var foundControllerTree = foundControllerTreeAttempt.Result; //instantiate it, since we are proxying, we need to setup the instance with our current context var instance = (TreeController)DependencyResolver.Current.GetService(foundControllerTree); instance.ControllerContext = controllerContext; instance.Request = controllerContext.Request; //return it's data return Attempt.Succeed(instance.GetNodes(id, formCollection)); } internal static Attempt TryGetRootNodeFromLegacyTree(this ApplicationTree appTree, FormDataCollection formCollection, UrlHelper urlHelper, string currentSection) { var xmlTreeNodeAttempt = TryGetRootXmlNodeFromLegacyTree(appTree, formCollection, urlHelper); if (xmlTreeNodeAttempt.Success == false) { return Attempt.Fail(xmlTreeNodeAttempt.Exception); } //the root can potentially be null, in that case we'll just return a null success which means it won't be included if (xmlTreeNodeAttempt.Result == null) { return Attempt.Succeed(null); } var legacyController = new LegacyTreeController(xmlTreeNodeAttempt.Result, appTree.Alias, currentSection, urlHelper); var newRoot = legacyController.GetRootNode(formCollection); return Attempt.Succeed(newRoot); } internal static Attempt TryGetRootXmlNodeFromLegacyTree(this ApplicationTree appTree, FormDataCollection formCollection, UrlHelper urlHelper) { var treeDefAttempt = appTree.TryGetLegacyTreeDef(); if (treeDefAttempt.Success == false) { return Attempt.Fail(treeDefAttempt.Exception); } var treeDef = treeDefAttempt.Result; var bTree = treeDef.CreateInstance(); var treeParams = new LegacyTreeParams(formCollection); bTree.SetTreeParameters(treeParams); var xmlRoot = bTree.RootNode; return Attempt.Succeed(xmlRoot); } internal static Attempt TryGetLegacyTreeDef(this ApplicationTree appTree) { //This is how the legacy trees worked.... var treeDef = TreeDefinitionCollection.Instance.FindTree(appTree.Alias); return treeDef == null ? Attempt.Fail(new InstanceNotFoundException("Could not find tree of type " + appTree.Alias)) : Attempt.Succeed(treeDef); } internal static Attempt TryLoadFromLegacyTree(this ApplicationTree appTree, string id, FormDataCollection formCollection, UrlHelper urlHelper, string currentSection) { var xTreeAttempt = appTree.TryGetXmlTree(id, formCollection); if (xTreeAttempt.Success == false) { return Attempt.Fail(xTreeAttempt.Exception); } return Attempt.Succeed(LegacyTreeDataConverter.ConvertFromLegacy(id, xTreeAttempt.Result, urlHelper, currentSection, formCollection)); } internal static Attempt TryGetMenuFromLegacyTreeRootNode(this ApplicationTree appTree, FormDataCollection formCollection, UrlHelper urlHelper) { var rootAttempt = appTree.TryGetRootXmlNodeFromLegacyTree(formCollection, urlHelper); if (rootAttempt.Success == false) { return Attempt.Fail(rootAttempt.Exception); } var currentSection = formCollection.GetRequiredString("section"); var result = LegacyTreeDataConverter.ConvertFromLegacyMenu(rootAttempt.Result, currentSection); return Attempt.Succeed(result); } internal static Attempt TryGetMenuFromLegacyTreeNode(this ApplicationTree appTree, string parentId, string nodeId, FormDataCollection formCollection, UrlHelper urlHelper) { var xTreeAttempt = appTree.TryGetXmlTree(parentId, formCollection); if (xTreeAttempt.Success == false) { return Attempt.Fail(xTreeAttempt.Exception); } var currentSection = formCollection.GetRequiredString("section"); var result = LegacyTreeDataConverter.ConvertFromLegacyMenu(nodeId, xTreeAttempt.Result, currentSection); if (result == null) { return Attempt.Fail(new ApplicationException("Could not find the node with id " + nodeId + " in the collection of nodes contained with parent id " + parentId)); } return Attempt.Succeed(result); } private static Attempt TryGetXmlTree(this ApplicationTree appTree, string id, FormDataCollection formCollection) { var treeDefAttempt = appTree.TryGetLegacyTreeDef(); if (treeDefAttempt.Success == false) { return Attempt.Fail(treeDefAttempt.Exception); } var treeDef = treeDefAttempt.Result; //This is how the legacy trees worked.... var bTree = treeDef.CreateInstance(); var treeParams = new LegacyTreeParams(formCollection); //we currently only support an integer id or a string id, we'll refactor how this works //later but we'll get this working first int startId; if (int.TryParse(id, out startId)) { treeParams.StartNodeID = startId; } else { treeParams.NodeKey = id; } var xTree = new XmlTree(); bTree.SetTreeParameters(treeParams); bTree.Render(ref xTree); return Attempt.Succeed(xTree); } } }