Merge remote-tracking branch 'origin/temp8' into temp8-remove-legacy-controls

# Conflicts:
#	src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs
This commit is contained in:
Shannon
2019-01-30 14:36:40 +11:00
159 changed files with 1553 additions and 1203 deletions

View File

@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Runtime.Serialization;
using System.Web;
using System.Web.Configuration;
using System.Web.Mvc;
@@ -158,7 +159,7 @@ namespace Umbraco.Web.Editors
},
{
"treeApplicationApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<ApplicationTreeController>(
controller => controller.GetApplicationTrees(null, null, null))
controller => controller.GetApplicationTrees(null, null, null, TreeUse.None))
},
{
"contentTypeApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<ContentTypeController>(
@@ -348,7 +349,10 @@ namespace Umbraco.Web.Editors
{
"umbracoPlugins", new Dictionary<string, object>
{
{"trees", GetTreePluginsMetaData()}
// for each tree that is [PluginController], get
// alias -> areaName
// so that routing (route.js) can look for views
{ "trees", GetPluginTrees().ToArray() }
}
},
{
@@ -389,43 +393,42 @@ namespace Umbraco.Web.Editors
return defaultVals;
}
private IEnumerable<Dictionary<string, string>> GetTreePluginsMetaData()
[DataContract]
private class PluginTree
{
var treeTypes = TreeControllerTypes.Value;
//get all plugin trees with their attributes
var treesWithAttributes = treeTypes.Select(x => new
{
tree = x,
attributes =
x.GetCustomAttributes(false)
}).ToArray();
var pluginTreesWithAttributes = treesWithAttributes
//don't resolve any tree decorated with CoreTreeAttribute
.Where(x => x.attributes.All(a => (a is CoreTreeAttribute) == false))
//we only care about trees with the PluginControllerAttribute
.Where(x => x.attributes.Any(a => a is PluginControllerAttribute))
.ToArray();
return (from p in pluginTreesWithAttributes
let treeAttr = p.attributes.OfType<TreeAttribute>().Single()
let pluginAttr = p.attributes.OfType<PluginControllerAttribute>().Single()
select new Dictionary<string, string>
{
{"alias", treeAttr.TreeAlias}, {"packageFolder", pluginAttr.AreaName}
}).ToArray();
[DataMember(Name = "alias")]
public string Alias { get; set; }
[DataMember(Name = "packageFolder")]
public string PackageFolder { get; set; }
}
/// <summary>
/// A lazy reference to all tree controller types
/// </summary>
/// <remarks>
/// We are doing this because if we constantly resolve the tree controller types from the PluginManager it will re-scan and also re-log that
/// it's resolving which is unnecessary and annoying.
/// </remarks>
private static readonly Lazy<IEnumerable<Type>> TreeControllerTypes
= new Lazy<IEnumerable<Type>>(() => Current.TypeLoader.GetAttributedTreeControllers().ToArray()); // TODO: inject
private IEnumerable<PluginTree> GetPluginTrees()
{
// used to be (cached)
//var treeTypes = Current.TypeLoader.GetAttributedTreeControllers();
//
// ie inheriting from TreeController and marked with TreeAttribute
//
// do this instead
// inheriting from TreeControllerBase and marked with TreeAttribute
var trees = Current.Factory.GetInstance<TreeCollection>();
foreach (var tree in trees)
{
var treeType = tree.TreeControllerType;
// exclude anything marked with CoreTreeAttribute
var coreTree = treeType.GetCustomAttribute<CoreTreeAttribute>(false);
if (coreTree != null) continue;
// exclude anything not marked with PluginControllerAttribute
var pluginController = treeType.GetCustomAttribute<PluginControllerAttribute>(false);
if (pluginController == null) continue;
yield return new PluginTree { Alias = tree.TreeAlias, PackageFolder = pluginController.AreaName };
}
}
/// <summary>
/// Returns the server variables regarding the application state

View File

@@ -75,11 +75,14 @@ namespace Umbraco.Web.Editors
/// <param name="name">The name of the container/folder</param>
/// <returns></returns>
[HttpPost]
public CodeFileDisplay PostCreateContainer(string type, string parentId, string name)
public HttpResponseMessage PostCreateContainer(string type, string parentId, string name)
{
if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Value cannot be null or whitespace.", "type");
if (string.IsNullOrWhiteSpace(parentId)) throw new ArgumentException("Value cannot be null or whitespace.", "parentId");
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name");
if (name.ContainsAny(Path.GetInvalidPathChars())) {
return Request.CreateNotificationValidationErrorResponse(Services.TextService.Localize("codefile/createFolderIllegalChars"));
}
// if the parentId is root (-1) then we just need an empty string as we are
// creating the path below and we don't want -1 in the path
@@ -118,11 +121,11 @@ namespace Umbraco.Web.Editors
}
return new CodeFileDisplay
return Request.CreateResponse(HttpStatusCode.OK, new CodeFileDisplay
{
VirtualPath = virtualPath,
Path = Url.GetTreePathFromFilePath(virtualPath)
};
});
}
/// <summary>

View File

@@ -33,7 +33,7 @@ namespace Umbraco.Web.Install.InstallSteps
/// <returns></returns>
public override Task<InstallSetupResult> ExecuteAsync(bool? model)
{
if (model.HasValue && model.Value == false) return null;
if (model.HasValue && model.Value == false) return Task.FromResult<InstallSetupResult>(null);
//install the machine key
var fileName = IOHelper.MapPath($"{SystemDirectories.Root}/web.config");
@@ -43,7 +43,7 @@ namespace Umbraco.Web.Install.InstallSteps
// Update appSetting if it exists, or else create a new appSetting for the given key and value
var machineKey = systemWeb.Descendants("machineKey").FirstOrDefault();
if (machineKey != null) return null;
if (machineKey != null) return Task.FromResult<InstallSetupResult>(null);
var generator = new MachineKeyGenerator();
var generatedSection = generator.GenerateConfigurationBlock();

View File

@@ -44,14 +44,6 @@ namespace Umbraco.Web.Mvc
id = @"[a-zA-Z]*"
},
new[] {typeof (BackOfficeController).Namespace});
//Create the REST/web/script service routes
context.MapRoute(
"Umbraco_web_services",
_globalSettings.GetUmbracoMvcArea() + "/RestServices/{controller}/{action}/{id}",
new {controller = "SaveFileController", action = "Index", id = UrlParameter.Optional},
//look in this namespace for controllers
new[] {"Umbraco.Web.WebServices"});
}
public override string AreaName => _globalSettings.GetUmbracoMvcArea();

View File

@@ -4,23 +4,27 @@ using System.Linq;
namespace Umbraco.Web.Mvc
{
/// <summary>
/// An attribute applied to a plugin controller that requires that it is routed to its own area
/// Indicates that a controller is a plugin tree controller and should be routed to its own area.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class PluginControllerAttribute : Attribute
{
public string AreaName { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="PluginControllerAttribute"/> class.
/// </summary>
/// <param name="areaName"></param>
public PluginControllerAttribute(string areaName)
{
//validate this, only letters and digits allowed.
if (areaName.Any(c => !Char.IsLetterOrDigit(c)))
{
throw new FormatException("The areaName specified " + areaName + " can only contains letters and digits");
}
// validate this, only letters and digits allowed.
if (areaName.Any(c => !char.IsLetterOrDigit(c)))
throw new FormatException($"Invalid area name \"{areaName}\": the area name can only contains letters and digits.");
AreaName = areaName;
}
/// <summary>
/// Gets the name of the area.
/// </summary>
public string AreaName { get; }
}
}

View File

@@ -65,7 +65,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
if (udi == null)
return null;
content = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(udi.Guid);
if (content != null)
if (content != null && content.ItemType == PublishedItemType.Content)
return content;
}
}

View File

@@ -98,7 +98,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
break;
}
if (multiNodeTreePickerItem != null)
if (multiNodeTreePickerItem != null && multiNodeTreePickerItem.ItemType != PublishedItemType.Element)
{
multiNodeTreePicker.Add(multiNodeTreePickerItem);
}

View File

@@ -65,7 +65,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
_publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(preview, dto.Udi.Guid) :
_publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(preview, dto.Udi.Guid);
if (content == null)
if (content == null || content.ItemType == PublishedItemType.Element)
{
continue;
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Services;
@@ -13,7 +12,7 @@ namespace Umbraco.Web.PropertyEditors
/// <remarks>
/// This pre-value editor is shared with editors like drop down, checkbox list, etc....
/// </remarks>
internal class ValueListConfigurationEditor : ConfigurationEditor<ValueListConfiguration>
public class ValueListConfigurationEditor : ConfigurationEditor<ValueListConfiguration>
{
public ValueListConfigurationEditor(ILocalizedTextService textService)
{

View File

@@ -1,26 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Routing;
namespace Umbraco.Web.Routing
{
//public class WebServicesRouteConstraint : IRouteConstraint
//{
// public bool Match(
// HttpContextBase httpContext,
// Route route,
// string parameterName,
// RouteValueDictionary values,
// RouteDirection routeDirection)
// {
// if (routeDirection == RouteDirection.UrlGeneration)
// {
// }
// return true;
// }
//}
}

View File

@@ -208,14 +208,10 @@ namespace Umbraco.Web.Runtime
.Add(composition.TypeLoader.GetTypes<IDashboard>());
// register back office trees
foreach (var treeControllerType in umbracoApiControllerTypes
.Where(x => typeof(TreeControllerBase).IsAssignableFrom(x)))
{
var attribute = treeControllerType.GetCustomAttribute<TreeAttribute>(false);
if (attribute == null) continue;
var tree = new Tree(attribute.SortOrder, attribute.ApplicationAlias, attribute.TreeAlias, attribute.TreeTitle, treeControllerType, attribute.IsSingleNodeTree);
composition.WithCollectionBuilder<TreeCollectionBuilder>().Trees.Add(tree);
}
// the collection builder only accepts types inheriting from TreeControllerBase
// and will filter out those that are not attributed with TreeAttribute
composition.WithCollectionBuilder<TreeCollectionBuilder>()
.AddTreeControllers(umbracoApiControllerTypes.Where(x => typeof(TreeControllerBase).IsAssignableFrom(x)));
}
}
}

View File

@@ -32,7 +32,7 @@ namespace Umbraco.Web.Search
var found = searchableTrees.FirstOrDefault(x => x.TreeAlias.InvariantEquals(appTree.TreeAlias));
if (found != null)
{
dictionary[found.TreeAlias] = new SearchableApplicationTree(appTree.ApplicationAlias, appTree.TreeAlias, found);
dictionary[found.TreeAlias] = new SearchableApplicationTree(appTree.SectionAlias, appTree.TreeAlias, found);
}
}
return dictionary;

View File

@@ -1,41 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentEditing;
using Umbraco.Web.Models.ContentEditing;
using System.Collections.Generic;
using Umbraco.Web.Trees;
namespace Umbraco.Web.Services
{
/// <summary>
/// Represents a service which manages section trees.
/// </summary>
public interface ITreeService
{
/// <summary>
/// Gets an ApplicationTree by it's tree alias.
/// Gets a tree.
/// </summary>
/// <param name="treeAlias">The tree alias.</param>
/// <returns>An ApplicationTree instance</returns>
Tree GetByAlias(string treeAlias);
/// <summary>
/// Gets all applicationTrees registered in umbraco from the umbracoAppTree table..
/// Gets all trees.
/// </summary>
/// <returns>Returns a ApplicationTree Array</returns>
IEnumerable<Tree> GetAll();
IEnumerable<Tree> GetAll(TreeUse use = TreeUse.Main);
/// <summary>
/// Gets the application tree for the applcation with the specified alias
/// Gets all trees for a section.
/// </summary>
/// <param name="sectionAlias">The application alias.</param>
/// <returns>Returns a ApplicationTree Array</returns>
IEnumerable<Tree> GetTrees(string sectionAlias);
IEnumerable<Tree> GetBySection(string sectionAlias, TreeUse use = TreeUse.Main);
/// <summary>
/// Gets the grouped application trees for the application with the specified alias
/// Gets all trees for a section, grouped.
/// </summary>
/// <param name="sectionAlias"></param>
/// <returns></returns>
IDictionary<string, IEnumerable<Tree>> GetGroupedTrees(string sectionAlias);
IDictionary<string, IEnumerable<Tree>> GetBySectionGrouped(string sectionAlias, TreeUse use = TreeUse.Main);
}
}

View File

@@ -6,65 +6,41 @@ using Umbraco.Web.Trees;
namespace Umbraco.Web.Services
{
/// <summary>
/// Implements <see cref="ITreeService"/>.
/// </summary>
internal class TreeService : ITreeService
{
private readonly TreeCollection _treeCollection;
private readonly Lazy<IReadOnlyCollection<IGrouping<string, string>>> _groupedTrees;
/// <summary>
/// Initializes a new instance of the <see cref="TreeService"/> class.
/// </summary>
/// <param name="treeCollection"></param>
public TreeService(TreeCollection treeCollection)
{
_treeCollection = treeCollection;
_groupedTrees = new Lazy<IReadOnlyCollection<IGrouping<string, string>>>(InitGroupedTrees);
}
/// <inheritdoc />
public Tree GetByAlias(string treeAlias) => _treeCollection.FirstOrDefault(t => t.TreeAlias == treeAlias);
public Tree GetByAlias(string treeAlias) => _treeCollection.FirstOrDefault(x => x.TreeAlias == treeAlias);
/// <inheritdoc />
public IEnumerable<Tree> GetAll() => _treeCollection;
public IEnumerable<Tree> GetAll(TreeUse use = TreeUse.Main)
// use HasFlagAny: if use is Main|Dialog, we want to return Main *and* Dialog trees
=> _treeCollection.Where(x => x.TreeUse.HasFlagAny(use));
/// <inheritdoc />
public IEnumerable<Tree> GetTrees(string sectionAlias)
=> GetAll().Where(x => x.ApplicationAlias.InvariantEquals(sectionAlias)).OrderBy(x => x.SortOrder).ToList();
public IEnumerable<Tree> GetBySection(string sectionAlias, TreeUse use = TreeUse.Main)
// use HasFlagAny: if use is Main|Dialog, we want to return Main *and* Dialog trees
=> _treeCollection.Where(x => x.SectionAlias.InvariantEquals(sectionAlias) && x.TreeUse.HasFlagAny(use)).OrderBy(x => x.SortOrder).ToList();
public IDictionary<string, IEnumerable<Tree>> GetGroupedTrees(string sectionAlias)
/// <inheritdoc />
public IDictionary<string, IEnumerable<Tree>> GetBySectionGrouped(string sectionAlias, TreeUse use = TreeUse.Main)
{
var result = new Dictionary<string, IEnumerable<Tree>>();
var foundTrees = GetTrees(sectionAlias).ToList();
foreach(var treeGroup in _groupedTrees.Value)
{
List<Tree> resultGroup = null;
foreach(var tree in foundTrees)
{
foreach(var treeAliasInGroup in treeGroup)
{
if (tree.TreeAlias != treeAliasInGroup) continue;
if (resultGroup == null) resultGroup = new List<Tree>();
resultGroup.Add(tree);
}
}
if (resultGroup != null)
result[treeGroup.Key ?? string.Empty] = resultGroup; //key cannot be null so make empty string
}
return result;
return GetBySection(sectionAlias, use).GroupBy(x => x.TreeGroup).ToDictionary(
x => x.Key ?? "",
x => (IEnumerable<Tree>) x.ToArray());
}
/// <summary>
/// Creates a group of all tree groups and their tree aliases
/// </summary>
/// <returns></returns>
/// <remarks>
/// Used to initialize the <see cref="_groupedTrees"/> field
/// </remarks>
private IReadOnlyCollection<IGrouping<string, string>> InitGroupedTrees()
{
var result = GetAll()
.Select(x => (treeAlias: x.TreeAlias, treeGroup: x.TreeControllerType.GetCustomAttribute<CoreTreeAttribute>(false)?.TreeGroup))
.GroupBy(x => x.treeGroup, x => x.treeAlias)
.ToList();
return result;
}
}
}

View File

@@ -14,11 +14,8 @@ using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentEditing;
using Umbraco.Core.Persistence;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.Mvc;
using Umbraco.Web.Services;
@@ -50,262 +47,235 @@ namespace Umbraco.Web.Trees
/// </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>
/// <param name="querystring"></param>
/// <param name="use">Tree use.</param>
/// <returns></returns>
[HttpQueryStringFilter("queryStrings")]
public async Task<TreeRootNode> GetApplicationTrees(string application, string tree, FormDataCollection queryStrings)
public async Task<TreeRootNode> GetApplicationTrees(string application, string tree, FormDataCollection querystring, TreeUse use = TreeUse.Main)
{
application = application.CleanForXss();
if (string.IsNullOrEmpty(application)) throw new HttpResponseException(HttpStatusCode.NotFound);
if (string.IsNullOrEmpty(application))
throw new HttpResponseException(HttpStatusCode.NotFound);
//find all tree definitions that have the current application alias
var groupedTrees = _treeService.GetGroupedTrees(application);
var groupedTrees = _treeService.GetBySectionGrouped(application, use);
var allTrees = groupedTrees.Values.SelectMany(x => x).ToList();
if (string.IsNullOrEmpty(tree) == false || allTrees.Count == 1)
if (allTrees.Count == 0)
throw new HttpResponseException(HttpStatusCode.NotFound);
// handle request for a specific tree / or when there is only one tree
if (!tree.IsNullOrWhiteSpace() || allTrees.Count == 1)
{
var apptree = !tree.IsNullOrWhiteSpace()
? allTrees.FirstOrDefault(x => x.TreeAlias == tree)
: allTrees.FirstOrDefault();
var t = tree.IsNullOrWhiteSpace()
? allTrees[0]
: allTrees.FirstOrDefault(x => x.TreeAlias == tree);
if (apptree == null) throw new HttpResponseException(HttpStatusCode.NotFound);
if (t == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
var result = await GetRootForSingleAppTree(
apptree,
Constants.System.Root.ToString(CultureInfo.InvariantCulture),
queryStrings,
application);
var treeRootNode = await GetTreeRootNode(t, Constants.System.Root, querystring);
if (treeRootNode != null)
return treeRootNode;
//this will be null if it cannot convert to a single root section
if (result != null)
{
return result;
}
throw new HttpResponseException(HttpStatusCode.NotFound);
}
//Don't apply fancy grouping logic futher down, if we only have one group of items
var hasGroups = groupedTrees.Count > 1;
if (!hasGroups)
// handle requests for all trees
// for only 1 group
if (groupedTrees.Count == 1)
{
var collection = new TreeNodeCollection();
foreach (var apptree in allTrees)
var nodes = new TreeNodeCollection();
foreach (var t in allTrees)
{
//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);
}
var node = await TryGetRootNode(t, querystring);
if (node != null)
nodes.Add(node);
}
if(collection.Count > 0)
{
var multiTree = TreeRootNode.CreateMultiTreeRoot(collection);
multiTree.Name = Services.TextService.Localize("sections/" + application);
var name = Services.TextService.Localize("sections/" + application);
return multiTree;
if (nodes.Count > 0)
{
var treeRootNode = TreeRootNode.CreateMultiTreeRoot(nodes);
treeRootNode.Name = name;
return treeRootNode;
}
//Otherwise its a application/section with no trees (aka a full screen app)
//For example we do not have a Forms tree defined in C# & can not attribute with [Tree(isSingleNodeTree:true0]
var rootId = Constants.System.Root.ToString(CultureInfo.InvariantCulture);
var section = Services.TextService.Localize("sections/" + application);
return TreeRootNode.CreateSingleTreeRoot(rootId, null, null, section, TreeNodeCollection.Empty, true);
// otherwise it's a section with no tree, aka a fullscreen section
// todo is this true? what if we just failed to TryGetRootNode on all of them?
return TreeRootNode.CreateSingleTreeRoot(Constants.System.Root.ToInvariantString(), null, null, name, TreeNodeCollection.Empty, true);
}
var rootNodeGroups = new List<TreeRootNode>();
//Group trees by [CoreTree] attribute with a TreeGroup property
foreach (var treeSectionGroup in groupedTrees)
// for many groups
var treeRootNodes = new List<TreeRootNode>();
foreach (var (groupName, trees) in groupedTrees)
{
var treeGroupName = treeSectionGroup.Key;
var groupNodeCollection = new TreeNodeCollection();
foreach (var appTree in treeSectionGroup.Value)
var nodes = new TreeNodeCollection();
foreach (var t in trees)
{
var rootNode = await GetRootForMultipleAppTree(appTree, queryStrings);
if (rootNode != null)
{
//Add to a new list/collection
groupNodeCollection.Add(rootNode);
}
var node = await TryGetRootNode(t, querystring);
if (node != null)
nodes.Add(node);
}
//If treeGroupName == null then its third party
if (treeGroupName.IsNullOrWhiteSpace())
{
//This is used for the localization key
//treeHeaders/thirdPartyGroup
treeGroupName = "thirdPartyGroup";
}
if (nodes.Count == 0)
continue;
if (groupNodeCollection.Count > 0)
{
var groupRoot = TreeRootNode.CreateGroupNode(groupNodeCollection, application);
groupRoot.Name = Services.TextService.Localize("treeHeaders/" + treeGroupName);
// no name => third party
// use localization key treeHeaders/thirdPartyGroup
// todo this is an odd convention
var name = groupName.IsNullOrWhiteSpace() ? "thirdPartyGroup" : groupName;
rootNodeGroups.Add(groupRoot);
}
var groupRootNode = TreeRootNode.CreateGroupNode(nodes, application);
groupRootNode.Name = Services.TextService.Localize("treeHeaders/" + name);
treeRootNodes.Add(groupRootNode);
}
return TreeRootNode.CreateGroupedMultiTreeRoot(new TreeNodeCollection(rootNodeGroups.OrderBy(x => x.Name)));
return TreeRootNode.CreateGroupedMultiTreeRoot(new TreeNodeCollection(treeRootNodes.OrderBy(x => x.Name)));
}
/// <summary>
/// Get the root node for an application with multiple trees
/// Tries to get the root node of a tree.
/// </summary>
/// <param name="tree"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
private async Task<TreeNode> GetRootForMultipleAppTree(Tree tree, FormDataCollection queryStrings)
/// <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)
{
if (tree == null) throw new ArgumentNullException(nameof(tree));
try
{
var byControllerAttempt = await TryGetRootNodeFromControllerTree(tree, queryStrings, ControllerContext);
if (byControllerAttempt.Success)
{
return byControllerAttempt.Result;
}
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.
// 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;
}
throw new ApplicationException("Could not get root node for tree type " + tree.TreeAlias);
}
/// <summary>
/// Get the root node for an application with one tree
/// Get the tree root node of a tree.
/// </summary>
/// <param name="tree"></param>
/// <param name="id"></param>
/// <param name="queryStrings"></param>
/// <param name="application"></param>
/// <returns></returns>
private async Task<TreeRootNode> GetRootForSingleAppTree(Tree tree, string id, FormDataCollection queryStrings, string application)
private async Task<TreeRootNode> GetTreeRootNode(Tree tree, int id, FormDataCollection querystring)
{
var rootId = Constants.System.Root.ToString(CultureInfo.InvariantCulture);
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)
{
//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);
}
var children = await GetChildren(tree, id, querystring);
var rootNode = await GetRootNode(tree, querystring);
var sectionRoot = TreeRootNode.CreateSingleTreeRoot(
rootId,
rootNode.Result.ChildNodesUrl,
rootNode.Result.MenuUrl,
rootNode.Result.Name,
byControllerAttempt.Result,
Constants.System.Root.ToInvariantString(),
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.Result.RoutePath;
sectionRoot.Path = rootNode.Result.Path;
// 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.Result.AdditionalData)
{
foreach (var d in rootNode.AdditionalData)
sectionRoot.AdditionalData[d.Key] = d.Value;
}
return sectionRoot;
return sectionRoot;
}
/// <summary>
/// Proxies a request to the destination tree controller to get it's root tree node
/// Gets the root node of a tree.
/// </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>
private async Task<Attempt<TreeNode>> TryGetRootNodeFromControllerTree(Tree appTree, FormDataCollection formCollection, HttpControllerContext controllerContext)
private async Task<TreeNode> GetRootNode(Tree tree, FormDataCollection querystring)
{
//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 (tree == null) throw new ArgumentNullException(nameof(tree));
//NOTE: This is all required in order to execute the auth-filters for the sub request, we
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;
}
/// <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));
// 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);
var controller = (TreeController) await GetApiControllerProxy(tree.TreeControllerType, "GetNodes", proxyQuerystring);
return controller.GetNodes(id.ToInvariantString(), querystring);
}
/// <summary>
/// Gets a proxy to a controller for a specified action.
/// </summary>
/// <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)
{
// 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()) }));
var context = ControllerContext;
//create a proxied controller context
var proxiedControllerContext = new HttpControllerContext(
controllerContext.Configuration,
proxiedRouteData,
new HttpRequestMessage(HttpMethod.Get, proxiedUrl))
// 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(controllerContext.ControllerDescriptor.Configuration, ControllerExtensions.GetControllerName(instance.GetType()), instance.GetType()),
RequestContext = controllerContext.RequestContext
ControllerDescriptor = new HttpControllerDescriptor(context.ControllerDescriptor.Configuration, ControllerExtensions.GetControllerName(controllerType), controllerType),
RequestContext = context.RequestContext,
Controller = controller
};
instance.ControllerContext = proxiedControllerContext;
instance.Request = controllerContext.Request;
instance.RequestContext.RouteData = proxiedRouteData;
// wire everything
controller.ControllerContext = proxyContext;
controller.Request = proxyContext.Request;
controller.RequestContext.RouteData = proxyRoute;
//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);
}
// auth
var authResult = await controller.ControllerContext.InvokeAuthorizationFiltersForRequest();
if (authResult != null)
throw new HttpResponseException(authResult);
//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);
return controller;
}
/// <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>
private Attempt<TreeNodeCollection> TryLoadFromControllerTree(Tree appTree, string id, FormDataCollection formCollection, HttpControllerContext controllerContext)
{
// 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));
}
}
}

View File

@@ -18,9 +18,9 @@ namespace Umbraco.Web.Trees
/// This authorizes based on access to the content section even though it exists in the settings
/// </remarks>
[UmbracoApplicationAuthorize(Constants.Applications.Content)]
[Tree(Constants.Applications.Settings, Constants.Trees.ContentBlueprints, null, sortOrder: 12)]
[Tree(Constants.Applications.Settings, Constants.Trees.ContentBlueprints, SortOrder = 12, TreeGroup = Constants.Trees.Groups.Settings)]
[PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Settings)]
[CoreTree]
public class ContentBlueprintTreeController : TreeController
{

View File

@@ -15,9 +15,9 @@ using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.DocumentTypes)]
[Tree(Constants.Applications.Settings, Constants.Trees.DocumentTypes, null, sortOrder: 0)]
[Tree(Constants.Applications.Settings, Constants.Trees.DocumentTypes, SortOrder = 0, TreeGroup = Constants.Trees.Groups.Settings)]
[Mvc.PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Settings)]
[CoreTree]
public class ContentTypeTreeController : TreeController, ISearchableTree
{
protected override TreeNode CreateRootNode(FormDataCollection queryStrings)

View File

@@ -3,19 +3,12 @@
namespace Umbraco.Web.Trees
{
/// <summary>
/// Indicates that a tree is a core tree and shouldn't be treated as a plugin tree
/// Indicates that a tree is a core tree and should not be treated as a plugin tree.
/// </summary>
/// <remarks>
/// This ensures that umbraco will look in the umbraco folders for views for this tree
/// This ensures that umbraco will look in the umbraco folders for views for this tree.
/// </remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
internal class CoreTreeAttribute : Attribute
{
public string TreeGroup { get; set; }
public CoreTreeAttribute()
{
}
}
{ }
}

View File

@@ -17,9 +17,9 @@ using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.DataTypes)]
[Tree(Constants.Applications.Settings, Constants.Trees.DataTypes, null, sortOrder:3)]
[Tree(Constants.Applications.Settings, Constants.Trees.DataTypes, SortOrder = 3, TreeGroup = Constants.Trees.Groups.Settings)]
[PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Settings)]
[CoreTree]
public class DataTypeTreeController : TreeController, ISearchableTree
{
protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)
@@ -121,7 +121,7 @@ namespace Umbraco.Web.Trees
});
if (container.HasChildren == false)
{
{
//can delete data type
menu.Items.Add<ActionDelete>(Services.TextService, opensDialog: true);
}

View File

@@ -17,8 +17,8 @@ namespace Umbraco.Web.Trees
// dictionary items in templates, even when we dont have authorization to manage the dictionary items
)]
[Mvc.PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Settings)]
[Tree(Constants.Applications.Translation, Constants.Trees.Dictionary, null)]
[CoreTree]
[Tree(Constants.Applications.Translation, Constants.Trees.Dictionary, TreeGroup = Constants.Trees.Groups.Settings)]
public class DictionaryTreeController : TreeController
{
protected override TreeNode CreateRootNode(FormDataCollection queryStrings)

View File

@@ -4,11 +4,11 @@ using Umbraco.Web.Models.Trees;
namespace Umbraco.Web.Trees
{
[CoreTree(TreeGroup = Constants.Trees.Groups.Templating)]
[Tree(Constants.Applications.Settings, "files", "Files", "icon-folder", "icon-folder", sortOrder: 13, initialize: false)]
[Tree(Constants.Applications.Settings, "files", TreeTitle = "Files", IconOpen = "icon-folder", IconClosed = "icon-folder", TreeUse = TreeUse.Dialog)]
[CoreTree]
public class FilesTreeController : FileSystemTreeController
{
protected override IFileSystem FileSystem => new PhysicalFileSystem("~/"); // TODO: inject
protected override IFileSystem FileSystem => new PhysicalFileSystem("~/");
private static readonly string[] ExtensionsStatic = { "*" };

View File

@@ -1,7 +1,7 @@
namespace Umbraco.Web.Trees
{
// TODO: we don't really use this, it is nice to have the treecontroller, attribute and ApplicationTree streamlined to implement this but it's not used
//leave as internal for now, maybe we'll use in the future, means we could pass around ITree
//leave as internal for now, maybe we'll use in the future, means we could pass around ITree
internal interface ITree
{
/// <summary>
@@ -13,7 +13,12 @@
/// <summary>
/// Gets the section alias.
/// </summary>
string ApplicationAlias { get; }
string SectionAlias { get; }
/// <summary>
/// Gets the tree group.
/// </summary>
string TreeGroup { get; }
/// <summary>
/// Gets the tree alias.
@@ -25,6 +30,11 @@
/// </summary>
string TreeTitle { get; }
/// <summary>
/// Gets the tree use.
/// </summary>
TreeUse TreeUse { get; }
/// <summary>
/// Flag to define if this tree is a single node tree (will never contain child nodes, full screen app)
/// </summary>

View File

@@ -7,9 +7,9 @@ using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.Languages)]
[Tree(Constants.Applications.Settings, Constants.Trees.Languages, null, sortOrder: 11)]
[Tree(Constants.Applications.Settings, Constants.Trees.Languages, SortOrder = 11, TreeGroup = Constants.Trees.Groups.Settings)]
[PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Settings)]
[CoreTree]
public class LanguageTreeController : TreeController
{
protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)

View File

@@ -7,9 +7,8 @@ using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.LogViewer)]
[Tree(Constants.Applications.Settings, Constants.Trees.LogViewer, null, sortOrder: 9)]
[Tree(Constants.Applications.Settings, Constants.Trees.LogViewer, SortOrder= 9, TreeGroup = Constants.Trees.Groups.Settings)]
[PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Settings)]
public class LogViewerTreeController : TreeController
{
protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)

View File

@@ -12,9 +12,9 @@ using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.Macros)]
[Tree(Constants.Applications.Settings, Constants.Trees.Macros, "Macros", sortOrder: 4)]
[Tree(Constants.Applications.Settings, Constants.Trees.Macros, TreeTitle = "Macros", SortOrder = 4, TreeGroup = Constants.Trees.Groups.Settings)]
[PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Settings)]
[CoreTree]
public class MacrosTreeController : TreeController
{
protected override TreeNode CreateRootNode(FormDataCollection queryStrings)

View File

@@ -16,9 +16,9 @@ using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.MediaTypes)]
[Tree(Constants.Applications.Settings, Constants.Trees.MediaTypes, null, sortOrder:1)]
[Tree(Constants.Applications.Settings, Constants.Trees.MediaTypes, SortOrder = 1, TreeGroup = Constants.Trees.Groups.Settings)]
[Mvc.PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Settings)]
[CoreTree]
public class MediaTypeTreeController : TreeController, ISearchableTree
{
protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)

View File

@@ -8,7 +8,7 @@ using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.MemberGroups)]
[Tree(Constants.Applications.Members, Constants.Trees.MemberGroups, null, sortOrder: 1)]
[Tree(Constants.Applications.Members, Constants.Trees.MemberGroups, SortOrder = 1)]
[Mvc.PluginController("UmbracoTrees")]
[CoreTree]
public class MemberGroupTreeController : MemberTypeAndGroupTreeControllerBase

View File

@@ -26,7 +26,7 @@ namespace Umbraco.Web.Trees
Constants.Applications.Content,
Constants.Applications.Media,
Constants.Applications.Members)]
[Tree(Constants.Applications.Members, Constants.Trees.Members, null, sortOrder: 0)]
[Tree(Constants.Applications.Members, Constants.Trees.Members, SortOrder = 0)]
[PluginController("UmbracoTrees")]
[CoreTree]
[SearchableTree("searchResultFormatter", "configureMemberResult")]

View File

@@ -7,9 +7,9 @@ using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.Trees
{
[CoreTree(TreeGroup = Constants.Trees.Groups.Settings)]
[CoreTree]
[UmbracoTreeAuthorize(Constants.Trees.MemberTypes)]
[Tree(Constants.Applications.Settings, Constants.Trees.MemberTypes, null, sortOrder: 2)]
[Tree(Constants.Applications.Settings, Constants.Trees.MemberTypes, SortOrder = 2, TreeGroup = Constants.Trees.Groups.Settings)]
public class MemberTypeTreeController : MemberTypeAndGroupTreeControllerBase
{
protected override TreeNode CreateRootNode(FormDataCollection queryStrings)

View File

@@ -8,7 +8,7 @@ using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.Packages)]
[Tree(Constants.Applications.Packages, Constants.Trees.Packages, null, sortOrder: 0, isSingleNodeTree: true)]
[Tree(Constants.Applications.Packages, Constants.Trees.Packages, SortOrder = 0, IsSingleNodeTree = true)]
[PluginController("UmbracoTrees")]
[CoreTree]
public class PackagesTreeController : TreeController

View File

@@ -10,10 +10,10 @@ namespace Umbraco.Web.Trees
/// <summary>
/// Tree for displaying partial view macros in the developer app
/// </summary>
[Tree(Constants.Applications.Settings, Constants.Trees.PartialViewMacros, null, sortOrder: 8)]
[Tree(Constants.Applications.Settings, Constants.Trees.PartialViewMacros, SortOrder = 8, TreeGroup = Constants.Trees.Groups.Templating)]
[UmbracoTreeAuthorize(Constants.Trees.PartialViewMacros)]
[PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Templating)]
[CoreTree]
public class PartialViewMacrosTreeController : PartialViewsTreeController
{
protected override IFileSystem FileSystem => Current.FileSystems.MacroPartialsFileSystem;

View File

@@ -10,10 +10,10 @@ namespace Umbraco.Web.Trees
/// <summary>
/// Tree for displaying partial views in the settings app
/// </summary>
[Tree(Constants.Applications.Settings, Constants.Trees.PartialViews, null, sortOrder: 7)]
[Tree(Constants.Applications.Settings, Constants.Trees.PartialViews, SortOrder = 7, TreeGroup = Constants.Trees.Groups.Templating)]
[UmbracoTreeAuthorize(Constants.Trees.PartialViews)]
[PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Templating)]
[CoreTree]
public class PartialViewsTreeController : FileSystemTreeController
{
protected override IFileSystem FileSystem => Current.FileSystems.PartialViewsFileSystem;

View File

@@ -9,9 +9,9 @@ using Umbraco.Web.Actions;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.RelationTypes)]
[Tree(Constants.Applications.Settings, Constants.Trees.RelationTypes, null, sortOrder: 5)]
[Tree(Constants.Applications.Settings, Constants.Trees.RelationTypes, SortOrder = 5, TreeGroup = Constants.Trees.Groups.Settings)]
[Mvc.PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Settings)]
[CoreTree]
public class RelationTypeTreeController : TreeController
{
protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings)

View File

@@ -5,8 +5,8 @@ using Umbraco.Web.Models.Trees;
namespace Umbraco.Web.Trees
{
[CoreTree(TreeGroup = Constants.Trees.Groups.Templating)]
[Tree(Constants.Applications.Settings, Constants.Trees.Scripts, "Scripts", "icon-folder", "icon-folder", sortOrder: 10)]
[CoreTree]
[Tree(Constants.Applications.Settings, Constants.Trees.Scripts, TreeTitle = "Scripts", IconOpen = "icon-folder", IconClosed = "icon-folder", SortOrder = 10, TreeGroup = Constants.Trees.Groups.Templating)]
public class ScriptsTreeController : FileSystemTreeController
{
protected override IFileSystem FileSystem => Current.FileSystems.ScriptsFileSystem; // TODO: inject

View File

@@ -4,8 +4,8 @@ using Umbraco.Web.Composing;
namespace Umbraco.Web.Trees
{
[CoreTree(TreeGroup = Constants.Trees.Groups.Templating)]
[Tree(Constants.Applications.Settings, Constants.Trees.Stylesheets, "Stylesheets", "icon-folder", "icon-folder", sortOrder: 9)]
[CoreTree]
[Tree(Constants.Applications.Settings, Constants.Trees.Stylesheets, TreeTitle = "Stylesheets", IconOpen = "icon-folder", IconClosed = "icon-folder", SortOrder = 9, TreeGroup = Constants.Trees.Groups.Templating)]
public class StylesheetsTreeController : FileSystemTreeController
{
protected override IFileSystem FileSystem => Current.FileSystems.StylesheetsFileSystem; // TODO: inject

View File

@@ -16,9 +16,9 @@ using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.Templates)]
[Tree(Constants.Applications.Settings, Constants.Trees.Templates, null, sortOrder:6)]
[Tree(Constants.Applications.Settings, Constants.Trees.Templates, SortOrder = 6, TreeGroup = Constants.Trees.Groups.Templating)]
[PluginController("UmbracoTrees")]
[CoreTree(TreeGroup = Constants.Trees.Groups.Templating)]
[CoreTree]
public class TemplatesTreeController : TreeController, ISearchableTree
{
protected override TreeNode CreateRootNode(FormDataCollection queryStrings)

View File

@@ -1,46 +1,48 @@
using System;
using System.Diagnostics;
using Umbraco.Core.Services;
using Umbraco.Web.Models.Trees;
namespace Umbraco.Web.Trees
{
[DebuggerDisplay("Tree - {TreeAlias} ({ApplicationAlias})")]
[DebuggerDisplay("Tree - {SectionAlias}/{TreeAlias}")]
public class Tree : ITree
{
public Tree(int sortOrder, string applicationAlias, string alias, string title, Type treeControllerType, bool isSingleNodeTree)
public Tree(int sortOrder, string applicationAlias, string group, string alias, string title, TreeUse use, Type treeControllerType, bool isSingleNodeTree)
{
SortOrder = sortOrder;
ApplicationAlias = applicationAlias;
SectionAlias = applicationAlias;
TreeGroup = group;
TreeAlias = alias;
TreeTitle = title;
TreeUse = use;
TreeControllerType = treeControllerType;
IsSingleNodeTree = isSingleNodeTree;
}
/// <inheritdoc />
/// <summary>
/// Gets or sets the sort order.
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// Gets the application alias.
/// </summary>
public string ApplicationAlias { get; set; }
/// <inheritdoc />
public string SectionAlias { get; set; }
/// <inheritdoc />
public string TreeGroup { get; }
/// <inheritdoc />
public string TreeAlias { get; }
/// <inheritdoc />
/// <summary>
/// Gets or sets the tree title (fallback if the tree alias isn't localized)
/// </summary>
/// <value>The title.</value>
public string TreeTitle { get; set; }
/// <inheritdoc />
public TreeUse TreeUse { get; set; }
/// <inheritdoc />
public bool IsSingleNodeTree { get; }
/// <summary>
/// Gets the tree controller type.
/// </summary>
public Type TreeControllerType { get; }
internal static string GetRootNodeDisplayName(ITree tree, ILocalizedTextService textService)
@@ -64,6 +66,5 @@ namespace Umbraco.Web.Trees
return label;
}
}
}

View File

@@ -1,10 +1,9 @@
using System;
using Umbraco.Web.Models.Trees;
namespace Umbraco.Web.Trees
{
/// <summary>
/// Identifies an application tree
/// Identifies a section tree.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class TreeAttribute : Attribute, ITree
@@ -12,54 +11,55 @@ namespace Umbraco.Web.Trees
/// <summary>
/// Initializes a new instance of the <see cref="TreeAttribute"/> class.
/// </summary>
/// <param name="appAlias">The app alias.</param>
/// <param name="treeAlias"></param>
public TreeAttribute(string appAlias,
string treeAlias) : this(appAlias, treeAlias, null)
public TreeAttribute(string sectionAlias, string treeAlias)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TreeAttribute"/> class.
/// </summary>
/// <param name="appAlias">The app alias.</param>
/// <param name="treeAlias"></param>
/// <param name="treeTitle"></param>
/// <param name="iconClosed">The icon closed.</param>
/// <param name="iconOpen">The icon open.</param>
/// <param name="initialize">if set to <c>true</c> [initialize].</param>
/// <param name="sortOrder">The sort order.</param>
/// <param name="isSingleNodeTree">Flag to define if this tree is a single node tree (will never contain child nodes, full screen app)</param>
public TreeAttribute(string appAlias,
string treeAlias,
string treeTitle,
string iconClosed = "icon-folder",
string iconOpen = "icon-folder-open",
bool initialize = true,
int sortOrder = 0,
bool isSingleNodeTree = false)
{
ApplicationAlias = appAlias;
SectionAlias = sectionAlias;
TreeAlias = treeAlias;
TreeTitle = treeTitle;
IconClosed = iconClosed;
IconOpen = iconOpen;
Initialize = initialize;
SortOrder = sortOrder;
IsSingleNodeTree = isSingleNodeTree;
}
public string ApplicationAlias { get; }
public string TreeAlias { get; }
public string TreeTitle { get; }
public string IconClosed { get; }
public string IconOpen { get; }
public bool Initialize { get; }
public int SortOrder { get; }
/// <summary>
/// Gets the section alias.
/// </summary>
public string SectionAlias { get; }
/// <summary>
/// Flag to define if this tree is a single node tree (will never contain child nodes, full screen app)
/// Gets the tree alias.
/// </summary>
public bool IsSingleNodeTree { get; }
public string TreeAlias { get; }
/// <summary>
/// Gets or sets the tree title.
/// </summary>
public string TreeTitle { get; set; }
/// <summary>
/// Gets or sets the group of the tree.
/// </summary>
public string TreeGroup { get; set; }
/// <summary>
/// Gets the usage of the tree.
/// </summary>
public TreeUse TreeUse { get; set; } = TreeUse.Main;
/// <summary>
/// Gets or sets the tree icon when closed.
/// </summary>
public string IconClosed { get; set; } = "icon-folder";
/// <summary>
/// Gets or sets the tree icon when open.
/// </summary>
public string IconOpen { get; set; } = "icon-folder-open";
/// <summary>
/// Gets or sets the tree sort order.
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the tree is a single-node tree (no child nodes, full screen app).
/// </summary>
public bool IsSingleNodeTree { get; set; }
}
}

View File

@@ -1,15 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentEditing;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.Trees
{
/// <summary>
/// Represents the collection of section trees.
/// </summary>
public class TreeCollection : BuilderCollectionBase<Tree>
{
public TreeCollection(IEnumerable<Tree> items)

View File

@@ -1,17 +1,43 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Umbraco.Core;
using Umbraco.Core.Composing;
namespace Umbraco.Web.Trees
{
/// <summary>
/// Builds a <see cref="TreeCollection"/>.
/// </summary>
public class TreeCollectionBuilder : ICollectionBuilder<TreeCollection, Tree>
{
/// <summary>
/// expose the list of trees which developers can manipulate before the collection is created
/// </summary>
public List<Tree> Trees { get; } = new List<Tree>();
private readonly List<Tree> _trees = new List<Tree>();
public TreeCollection CreateCollection(IFactory factory) => new TreeCollection(Trees);
public TreeCollection CreateCollection(IFactory factory) => new TreeCollection(_trees);
public void RegisterWith(IRegister register) => register.Register(CreateCollection, Lifetime.Singleton);
public void AddTreeController<TController>()
where TController : TreeControllerBase
=> AddTreeController(typeof(TController));
public void AddTreeController(Type controllerType)
{
if (!typeof(TreeControllerBase).IsAssignableFrom(controllerType))
throw new ArgumentException($"Type {controllerType} does not inherit from {typeof(TreeControllerBase).FullName}.");
// no all TreeControllerBase are meant to be used here,
// ignore those that don't have the attribute
var attribute = controllerType.GetCustomAttribute<TreeAttribute>(false);
if (attribute == null) return;
var tree = new Tree(attribute.SortOrder, attribute.SectionAlias, attribute.TreeGroup, attribute.TreeAlias, attribute.TreeTitle, attribute.TreeUse, controllerType, attribute.IsSingleNodeTree);
_trees.Add(tree);
}
public void AddTreeControllers(IEnumerable<Type> controllerTypes)
{
foreach (var controllerType in controllerTypes)
AddTreeController(controllerType);
}
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
@@ -15,53 +14,53 @@ namespace Umbraco.Web.Trees
/// </summary>
public abstract class TreeController : TreeControllerBase
{
private TreeAttribute _attribute;
private static readonly ConcurrentDictionary<Type, TreeAttribute> TreeAttributeCache = new ConcurrentDictionary<Type, TreeAttribute>();
protected TreeController(IGlobalSettings globalSettings, UmbracoContext umbracoContext, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState) : base(globalSettings, umbracoContext, sqlContext, services, appCaches, logger, runtimeState)
private readonly TreeAttribute _treeAttribute;
protected TreeController(IGlobalSettings globalSettings, UmbracoContext umbracoContext, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState)
: base(globalSettings, umbracoContext, sqlContext, services, appCaches, logger, runtimeState)
{
Initialize();
_treeAttribute = GetTreeAttribute();
}
protected TreeController()
{
Initialize();
_treeAttribute = GetTreeAttribute();
}
/// <inheritdoc />
public override string RootNodeDisplayName => Tree.GetRootNodeDisplayName(this, Services.TextService);
/// <inheritdoc />
public override string TreeAlias => _attribute.TreeAlias;
/// <inheritdoc />
public override string TreeTitle => _attribute.TreeTitle;
/// <inheritdoc />
public override string ApplicationAlias => _attribute.ApplicationAlias;
/// <inheritdoc />
public override int SortOrder => _attribute.SortOrder;
/// <inheritdoc />
public override bool IsSingleNodeTree => _attribute.IsSingleNodeTree;
public override string TreeGroup => _treeAttribute.TreeGroup;
private void Initialize()
{
_attribute = GetTreeAttribute();
}
/// <inheritdoc />
public override string TreeAlias => _treeAttribute.TreeAlias;
private static readonly ConcurrentDictionary<Type, TreeAttribute> TreeAttributeCache = new ConcurrentDictionary<Type, TreeAttribute>();
/// <inheritdoc />
public override string TreeTitle => _treeAttribute.TreeTitle;
/// <inheritdoc />
public override TreeUse TreeUse => _treeAttribute.TreeUse;
/// <inheritdoc />
public override string SectionAlias => _treeAttribute.SectionAlias;
/// <inheritdoc />
public override int SortOrder => _treeAttribute.SortOrder;
/// <inheritdoc />
public override bool IsSingleNodeTree => _treeAttribute.IsSingleNodeTree;
private TreeAttribute GetTreeAttribute()
{
return TreeAttributeCache.GetOrAdd(GetType(), type =>
{
//Locate the tree attribute
var treeAttributes = type
.GetCustomAttributes<TreeAttribute>(false)
.ToArray();
if (treeAttributes.Length == 0)
var treeAttribute = type.GetCustomAttribute<TreeAttribute>(false);
if (treeAttribute == null)
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];
return treeAttribute;
});
}
}

View File

@@ -2,7 +2,6 @@
using System.Globalization;
using System.Linq;
using System.Net.Http.Formatting;
using System.Web.Http.Routing;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
@@ -19,7 +18,7 @@ using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.Trees
{
/// <summary>
/// A base controller reference for non-attributed trees (un-registered).
/// A base controller reference for non-attributed trees (un-registered).
/// </summary>
/// <remarks>
/// Developers should generally inherit from TreeController.
@@ -28,13 +27,11 @@ namespace Umbraco.Web.Trees
public abstract class TreeControllerBase : UmbracoAuthorizedApiController, ITree
{
protected TreeControllerBase()
{
}
protected TreeControllerBase(IGlobalSettings globalSettings, UmbracoContext umbracoContext, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState) : base(globalSettings, umbracoContext, sqlContext, services, appCaches, logger, runtimeState)
{
}
{ }
protected TreeControllerBase(IGlobalSettings globalSettings, UmbracoContext umbracoContext, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState)
: base(globalSettings, umbracoContext, sqlContext, services, appCaches, logger, runtimeState)
{ }
/// <summary>
/// The method called to render the contents of the tree structure
@@ -62,14 +59,24 @@ namespace Umbraco.Web.Trees
/// </summary>
public abstract string RootNodeDisplayName { get; }
/// <inheritdoc />
public abstract string TreeGroup { get; }
/// <inheritdoc />
public abstract string TreeAlias { get; }
/// <inheritdoc />
public abstract string TreeTitle { get; }
/// <inheritdoc />
public abstract string ApplicationAlias { get; }
public abstract TreeUse TreeUse { get; }
/// <inheritdoc />
public abstract string SectionAlias { get; }
/// <inheritdoc />
public abstract int SortOrder { get; }
/// <inheritdoc />
public abstract bool IsSingleNodeTree { get; }
@@ -402,5 +409,4 @@ namespace Umbraco.Web.Trees
handler?.Invoke(instance, e);
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
namespace Umbraco.Web.Trees
{
/// <summary>
/// Defines tree uses.
/// </summary>
[Flags]
public enum TreeUse
{
/// <summary>
/// The tree is not used.
/// </summary>
None = 0,
/// <summary>
/// The tree is used as a main (section) tree.
/// </summary>
Main = 1,
/// <summary>
/// The tree is used as a dialog.
/// </summary>
Dialog = 2,
}
}

View File

@@ -7,7 +7,7 @@ using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.Users)]
[Tree(Constants.Applications.Users, Constants.Trees.Users, null, sortOrder: 0, isSingleNodeTree: true)]
[Tree(Constants.Applications.Users, Constants.Trees.Users, SortOrder = 0, IsSingleNodeTree = true)]
[PluginController("UmbracoTrees")]
[CoreTree]
public class UserTreeController : TreeController

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.Media;
using Umbraco.Web.Mvc;
using Umbraco.Web.Trees;
using Umbraco.Web.WebApi;
using Umbraco.Core.Composing;
@@ -10,29 +8,20 @@ using Umbraco.Core.Composing;
namespace Umbraco.Web
{
/// <summary>
/// Extension methods for the PluginTypemgr
/// Provides extension methods for the <see cref="TypeLoader"/> class.
/// </summary>
public static class TypeLoaderExtensions
{
/// <summary>
/// Returns all available TreeApiController's in application that are attribute with TreeAttribute
/// Gets all types implementing <see cref="SurfaceController"/>.
/// </summary>
/// <param name="mgr"></param>
/// <returns></returns>
internal static IEnumerable<Type> GetAttributedTreeControllers(this TypeLoader mgr)
{
return mgr.GetTypesWithAttribute<TreeController, TreeAttribute>();
}
internal static IEnumerable<Type> GetSurfaceControllers(this TypeLoader typeLoader)
=> typeLoader.GetTypes<SurfaceController>();
internal static IEnumerable<Type> GetSurfaceControllers(this TypeLoader mgr)
{
return mgr.GetTypes<SurfaceController>();
}
internal static IEnumerable<Type> GetUmbracoApiControllers(this TypeLoader mgr)
{
return mgr.GetTypes<UmbracoApiController>();
}
/// <summary>
/// Gets all types implementing <see cref="UmbracoApiController"/>.
/// </summary>
internal static IEnumerable<Type> GetUmbracoApiControllers(this TypeLoader typeLoader)
=> typeLoader.GetTypes<UmbracoApiController>();
}
}

View File

@@ -204,6 +204,7 @@
<Compile Include="Trees\SettingsBackOfficeSection.cs" />
<Compile Include="Trees\TranslationBackOfficeSection.cs" />
<Compile Include="Trees\TreeCollectionBuilder.cs" />
<Compile Include="Trees\TreeUse.cs" />
<Compile Include="Trees\UsersBackOfficeSection.cs" />
<Compile Include="Trees\Tree.cs" />
<Compile Include="Trees\ITree.cs" />
@@ -1127,7 +1128,6 @@
<Compile Include="Routing\EnsureRoutableOutcome.cs" />
<Compile Include="Routing\PublishedRouter.cs" />
<Compile Include="Routing\UrlProvider.cs" />
<Compile Include="Routing\WebServicesRouteConstraint.cs" />
<Compile Include="Search\ExamineSearcherModel.cs" />
<Compile Include="Search\ExamineComponent.cs" />
<Compile Include="Components\DatabaseServerRegistrarAndMessengerComponent.cs" />

View File

@@ -44,7 +44,7 @@ namespace Umbraco.Web.WebApi.Filters
var apps = _treeAliases.Select(x => Current.TreeService
.GetByAlias(x))
.WhereNotNull()
.Select(x => x.ApplicationAlias)
.Select(x => x.SectionAlias)
.Distinct()
.ToArray();

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Runtime.Remoting.Contexts;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
@@ -15,52 +13,42 @@ namespace Umbraco.Web.WebApi
internal static class HttpControllerContextExtensions
{
/// <summary>
/// This method will go an execute the authorization filters for the controller action, if any fail
/// it will return their response, otherwise we'll return null.
/// Invokes the authorization filters for the controller action.
/// </summary>
/// <param name="controllerContext"></param>
/// <returns>The response of the first filter returning a result, if any, otherwise null (authorized).</returns>
internal static async Task<HttpResponseMessage> InvokeAuthorizationFiltersForRequest(this HttpControllerContext controllerContext)
{
var controllerDescriptor = controllerContext.ControllerDescriptor;
var controllerServices = controllerDescriptor.Configuration.Services;
var actionDescriptor = controllerServices.GetActionSelector().SelectAction(controllerContext);
var actionContext = new HttpActionContext(controllerContext, actionDescriptor);
var filters = actionDescriptor.GetFilterPipeline();
var filterGrouping = new FilterGrouping(filters);
var actionFilters = filterGrouping.ActionFilters;
// Because the continuation gets built from the inside out we need to reverse the filter list
// so that least specific filters (Global) get run first and the most specific filters (Action) get run last.
var authorizationFilters = filterGrouping.AuthorizationFilters.Reverse().ToArray();
if (authorizationFilters.Any())
{
var cancelToken = new CancellationToken();
var filterResult = await FilterContinuation(actionContext, cancelToken, authorizationFilters, 0);
if (filterResult != null)
{
//this means that the authorization filter has returned a result - unauthorized so we cannot continue
return filterResult;
}
}
return null;
// because the continuation gets built from the inside out we need to reverse the filter list
// so that least specific filters (Global) get run first and the most specific filters (Action) get run last.
var authorizationFilters = filterGrouping.AuthorizationFilters.Reverse().ToList();
if (authorizationFilters.Count == 0)
return null;
// if the authorization filter returns a result, it means it failed to authorize
var actionContext = new HttpActionContext(controllerContext, actionDescriptor);
return await ExecuteAuthorizationFiltersAsync(actionContext, CancellationToken.None, authorizationFilters);
}
/// <summary>
/// This method is how you execute a chain of filters, it needs to recursively call in to itself as the continuation for the next filter in the chain
/// Executes a chain of filters.
/// </summary>
/// <param name="actionContext"></param>
/// <param name="token"></param>
/// <param name="filters"></param>
/// <param name="index"></param>
/// <returns></returns>
private static async Task<HttpResponseMessage> FilterContinuation(HttpActionContext actionContext, CancellationToken token, IList<IAuthorizationFilter> filters, int index)
/// <remarks>
/// Recursively calls in to itself as its continuation for the next filter in the chain.
/// </remarks>
private static async Task<HttpResponseMessage> ExecuteAuthorizationFiltersAsync(HttpActionContext actionContext, CancellationToken token, IList<IAuthorizationFilter> filters, int index = 0)
{
return await filters[index].ExecuteAuthorizationFilterAsync(actionContext, token,
() => (index + 1) == filters.Count
() => ++index == filters.Count
? Task.FromResult<HttpResponseMessage>(null)
: FilterContinuation(actionContext, token, filters, ++index));
: ExecuteAuthorizationFiltersAsync(actionContext, token, filters, index));
}
}
}