U4-10409 - udi parsing and assembly scanning

This commit is contained in:
Stephan
2017-09-13 11:50:37 +02:00
parent add0685d3f
commit ef91e152b4
8 changed files with 359 additions and 104 deletions

View File

@@ -0,0 +1,26 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Umbraco.Core.Serialization
{
public class KnownTypeUdiJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Udi).IsAssignableFrom(objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jo = JToken.ReadFrom(reader);
var val = jo.ToObject<string>();
return val == null ? null : Udi.Parse(val, true);
}
}
}

View File

@@ -4,12 +4,11 @@ using Newtonsoft.Json.Linq;
namespace Umbraco.Core.Serialization
{
public class UdiJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Udi).IsAssignableFrom(objectType);
return typeof (Udi).IsAssignableFrom(objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)

View File

@@ -22,7 +22,7 @@ namespace Umbraco.Core
private static volatile bool _scanned;
private static readonly object ScanLocker = new object();
private static readonly Lazy<ConcurrentDictionary<string, UdiType>> KnownUdiTypes;
private static ConcurrentDictionary<string, UdiType> _udiTypes;
private static readonly ConcurrentDictionary<string, Udi> RootUdis = new ConcurrentDictionary<string, Udi>();
internal readonly Uri UriValue; // internal for UdiRange
@@ -50,8 +50,15 @@ namespace Umbraco.Core
static Udi()
{
// initialize with known (built-in) Udi types
// for non-known Udi types we'll try to parse a GUID and if that doesn't work, we'll decide that it's a string
KnownUdiTypes = new Lazy<ConcurrentDictionary<string, UdiType>>(() => new ConcurrentDictionary<string, UdiType>(Constants.UdiEntityType.GetTypes()));
// we will add scanned types later on
_udiTypes = new ConcurrentDictionary<string, UdiType>(Constants.UdiEntityType.GetTypes());
}
// for tests, totally unsafe
internal static void ResetUdiTypes()
{
_udiTypes = new ConcurrentDictionary<string, UdiType>(Constants.UdiEntityType.GetTypes());
_scanned = false;
}
/// <summary>
@@ -79,46 +86,65 @@ namespace Umbraco.Core
public static Udi Parse(string s)
{
Udi udi;
ParseInternal(s, false, out udi);
ParseInternal(s, false, false, out udi);
return udi;
}
/// <summary>
/// Converts the string representation of an entity identifier into the equivalent Udi instance.
/// </summary>
/// <param name="s">The string to convert.</param>
/// <param name="knownTypes">A value indicating whether to only deal with known types.</param>
/// <returns>An Udi instance that contains the value that was parsed.</returns>
/// <remarks>
/// <para>If <paramref name="knownTypes"/> is <c>true</c>, and the string could not be parsed because
/// the entity type was not known, the method succeeds but sets <c>udi</c>to an
/// <see cref="UnknownTypeUdi"/> value.</para>
/// <para>If <paramref name="knownTypes"/> is <c>true</c>, assemblies are not scanned for types,
/// and therefore only builtin types may be known. Unless scanning already took place.</para>
/// </remarks>
public static Udi Parse(string s, bool knownTypes)
{
Udi udi;
ParseInternal(s, false, knownTypes, out udi);
return udi;
}
/// <summary>
/// Converts the string representation of an entity identifier into the equivalent Udi instance.
/// </summary>
/// <param name="s">The string to convert.</param>
/// <param name="udi">An Udi instance that contains the value that was parsed.</param>
/// <returns>A boolean value indicating whether the string could be parsed.</returns>
public static bool TryParse(string s, out Udi udi)
{
return ParseInternal(s, true, out udi);
return ParseInternal(s, true, false, out udi);
}
private static UdiType GetUdiType(Uri uri, out string path)
/// <summary>
/// Converts the string representation of an entity identifier into the equivalent Udi instance.
/// </summary>
/// <param name="s">The string to convert.</param>
/// <param name="knownTypes">A value indicating whether to only deal with known types.</param>
/// <param name="udi">An Udi instance that contains the value that was parsed.</param>
/// <returns>A boolean value indicating whether the string could be parsed.</returns>
/// <remarks>
/// <para>If <paramref name="knownTypes"/> is <c>true</c>, and the string could not be parsed because
/// the entity type was not known, the method returns <c>false</c> but still sets <c>udi</c>
/// to an <see cref="UnknownTypeUdi"/> value.</para>
/// <para>If <paramref name="knownTypes"/> is <c>true</c>, assemblies are not scanned for types,
/// and therefore only builtin types may be known. Unless scanning already took place.</para>
/// </remarks>
public static bool TryParse(string s, bool knownTypes, out Udi udi)
{
path = uri.AbsolutePath.TrimStart('/');
UdiType udiType;
if (KnownUdiTypes.Value.TryGetValue(uri.Host, out udiType))
{
return udiType;
}
// if it's empty and it's not in our known list then we don't know
if (path.IsNullOrWhiteSpace())
return UdiType.Unknown;
// try to parse into a Guid
// (note: root udis use Guid.Empty so this is ok)
Guid guidId;
if (Guid.TryParse(path, out guidId))
{
//add it to our known list
KnownUdiTypes.Value.TryAdd(uri.Host, UdiType.GuidUdi);
return UdiType.GuidUdi;
}
// add it to our known list - if it's not a GUID then it must a string
KnownUdiTypes.Value.TryAdd(uri.Host, UdiType.StringUdi);
return UdiType.StringUdi;
return ParseInternal(s, true, knownTypes, out udi);
}
private static bool ParseInternal(string s, bool tryParse, out Udi udi)
private static bool ParseInternal(string s, bool tryParse, bool knownTypes, out Udi udi)
{
if (knownTypes == false)
EnsureScanForUdiTypes();
udi = null;
Uri uri;
@@ -129,24 +155,30 @@ namespace Umbraco.Core
throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s));
}
// if it's a known entity type, GetUdiType will return it
// else it will try to guess based on the path, and register the type as known
string path;
var udiType = GetUdiType(uri, out path);
if (path.IsNullOrWhiteSpace())
var entityType = uri.Host;
UdiType udiType;
if (_udiTypes.TryGetValue(entityType, out udiType) == false)
{
// path is empty which indicates we need to return the root udi
udi = GetRootUdi(uri.Host);
return true;
if (knownTypes)
{
// not knowing the type is not an error
// just return the unknown type udi
udi = UnknownTypeUdi.Instance;
return false;
}
if (tryParse) return false;
throw new FormatException(string.Format("Unknown entity type \"{0}\".", entityType));
}
// if the path is not empty, type should not be unknown
if (udiType == UdiType.Unknown)
throw new FormatException(string.Format("Could not determine the Udi type for string \"{0}\".", s));
var path = uri.AbsolutePath.TrimStart('/');
if (udiType == UdiType.GuidUdi)
{
if (path == string.Empty)
{
udi = GetRootUdi(uri.Host);
return true;
}
Guid guid;
if (Guid.TryParse(path, out guid) == false)
{
@@ -156,23 +188,25 @@ namespace Umbraco.Core
udi = new GuidUdi(uri.Host, guid);
return true;
}
if (udiType == UdiType.StringUdi)
{
udi = new StringUdi(uri.Host, Uri.UnescapeDataString(path));
udi = path == string.Empty ? GetRootUdi(uri.Host) : new StringUdi(uri.Host, Uri.UnescapeDataString(path));
return true;
}
if (tryParse) return false;
throw new InvalidOperationException("Internal error.");
throw new InvalidOperationException(string.Format("Invalid udi type \"{0}\".", udiType));
}
private static Udi GetRootUdi(string entityType)
{
ScanAllUdiTypes();
EnsureScanForUdiTypes();
return RootUdis.GetOrAdd(entityType, x =>
{
UdiType udiType;
if (KnownUdiTypes.Value.TryGetValue(x, out udiType) == false)
if (_udiTypes.TryGetValue(x, out udiType) == false)
throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType));
return udiType == UdiType.StringUdi
? (Udi)new StringUdi(entityType, string.Empty)
@@ -186,7 +220,7 @@ namespace Umbraco.Core
/// <remarks>
/// This is only required when needing to resolve root udis
/// </remarks>
private static void ScanAllUdiTypes()
private static void EnsureScanForUdiTypes()
{
if (_scanned) return;
@@ -212,11 +246,9 @@ namespace Umbraco.Core
}
}
//merge these into the known list
// merge these into the known list
foreach (var item in result)
{
KnownUdiTypes.Value.TryAdd(item.Key, item.Value);
}
_udiTypes.TryAdd(item.Key, item.Value);
_scanned = true;
}
@@ -241,11 +273,13 @@ namespace Umbraco.Core
public static Udi Create(string entityType, string id)
{
UdiType udiType;
if (KnownUdiTypes.Value.TryGetValue(entityType, out udiType) && udiType != UdiType.StringUdi)
throw new InvalidOperationException(string.Format("Entity type \"{0}\" is not a StringUdi.", entityType));
if (_udiTypes.TryGetValue(entityType, out udiType) == false)
throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType");
if (string.IsNullOrWhiteSpace(id))
throw new ArgumentException("Value cannot be null or whitespace.", "id");
if (udiType != UdiType.StringUdi)
throw new InvalidOperationException(string.Format("Entity type \"{0}\" does not have string udis.", entityType));
return new StringUdi(entityType, id);
}
@@ -259,11 +293,14 @@ namespace Umbraco.Core
public static Udi Create(string entityType, Guid id)
{
UdiType udiType;
if (KnownUdiTypes.Value.TryGetValue(entityType, out udiType) && udiType != UdiType.GuidUdi)
throw new InvalidOperationException(string.Format("Entity type \"{0}\" is not a GuidUdi.", entityType));
if (_udiTypes.TryGetValue(entityType, out udiType) == false)
throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType");
if (udiType != UdiType.GuidUdi)
throw new InvalidOperationException(string.Format("Entity type \"{0}\" does not have guid udis.", entityType));
if (id == default(Guid))
throw new ArgumentException("Cannot be an empty guid.", "id");
return new GuidUdi(entityType, id);
}
@@ -273,13 +310,14 @@ namespace Umbraco.Core
// else fallback to parsing the string (and guess the type)
UdiType udiType;
if (KnownUdiTypes.Value.TryGetValue(uri.Host, out udiType) == false)
return Parse(uri.ToString());
if (_udiTypes.TryGetValue(uri.Host, out udiType) == false)
throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", uri.Host), "uri");
if (udiType == UdiType.GuidUdi)
return new GuidUdi(uri);
if (udiType == UdiType.GuidUdi)
return new StringUdi(uri);
throw new ArgumentException(string.Format("Uri \"{0}\" is not a valid udi.", uri));
}
@@ -328,6 +366,19 @@ namespace Umbraco.Core
{
return (udi1 == udi2) == false;
}
}
private class UnknownTypeUdi : Udi
{
private UnknownTypeUdi()
: base("unknown", "umb://unknown/")
{ }
public static readonly UnknownTypeUdi Instance = new UnknownTypeUdi();
public override bool IsRoot
{
get { return false; }
}
}
}
}

View File

@@ -644,6 +644,7 @@
<Compile Include="Security\EmailService.cs" />
<Compile Include="Security\OwinExtensions.cs" />
<Compile Include="SemVersionExtensions.cs" />
<Compile Include="Serialization\KnownTypeUdiJsonConverter.cs" />
<Compile Include="Serialization\NoTypeConverterJsonConverter.cs" />
<Compile Include="Serialization\StreamResultExtensions.cs" />
<Compile Include="Serialization\UdiJsonConverter.cs" />

View File

@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Deploy;
using Umbraco.Core.Serialization;
namespace Umbraco.Tests
@@ -180,16 +182,6 @@ namespace Umbraco.Tests
Assert.IsInstanceOf<GuidUdi>(udi);
}
[Test]
public void NotKnownTypeTest()
{
var udi1 = Udi.Parse("umb://not-known-1/DA845952BE474EE9BD6F6194272AC750");
Assert.IsInstanceOf<GuidUdi>(udi1);
var udi2 = Udi.Parse("umb://not-known-2/this-is-not-a-guid");
Assert.IsInstanceOf<StringUdi>(udi2);
}
[Test]
public void RangeTest()
{
@@ -265,5 +257,86 @@ namespace Umbraco.Tests
Assert.AreEqual(0, types.Count, "Error in class Constants.UdiEntityType, GetTypes declares types that don't exist ({0}).", string.Join(",", types.Keys.Select(x => "\"" + x + "\"")));
}
[Test]
public void KnownTypes()
{
Udi udi;
// cannot parse an unknown type, udi is null
// this will scan
Assert.IsFalse(Udi.TryParse("umb://whatever/1234", out udi));
Assert.IsNull(udi);
Udi.ResetUdiTypes();
// unless we want to know
Assert.IsFalse(Udi.TryParse("umb://whatever/1234", true, out udi));
Assert.AreEqual(Constants.UdiEntityType.Unknown, udi.EntityType);
Assert.AreEqual("Umbraco.Core.Udi+UnknownTypeUdi", udi.GetType().FullName);
Udi.ResetUdiTypes();
// not known
Assert.IsFalse(Udi.TryParse("umb://foo/A87F65C8D6B94E868F6949BA92C93045", true, out udi));
Assert.AreEqual(Constants.UdiEntityType.Unknown, udi.EntityType);
Assert.AreEqual("Umbraco.Core.Udi+UnknownTypeUdi", udi.GetType().FullName);
// scanned
Assert.IsTrue(Udi.TryParse("umb://foo/A87F65C8D6B94E868F6949BA92C93045", out udi));
Assert.IsInstanceOf<GuidUdi>(udi);
// known
Assert.IsTrue(Udi.TryParse("umb://foo/A87F65C8D6B94E868F6949BA92C93045", true, out udi));
Assert.IsInstanceOf<GuidUdi>(udi);
// can get method for Deploy compatibility
var method = typeof (Udi).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof (string), typeof (bool) }, null);
Assert.IsNotNull(method);
}
[UdiDefinition("foo", UdiType.GuidUdi)]
public class FooConnector : IServiceConnector
{
public IArtifact GetArtifact(Udi udi)
{
throw new NotImplementedException();
}
public IArtifact GetArtifact(object entity)
{
throw new NotImplementedException();
}
public ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context)
{
throw new NotImplementedException();
}
public void Process(ArtifactDeployState dart, IDeployContext context, int pass)
{
throw new NotImplementedException();
}
public void Explode(UdiRange range, List<Udi> udis)
{
throw new NotImplementedException();
}
public NamedUdiRange GetRange(Udi udi, string selector)
{
throw new NotImplementedException();
}
public NamedUdiRange GetRange(string entityType, string sid, string selector)
{
throw new NotImplementedException();
}
public bool Compare(IArtifact art1, IArtifact art2, ICollection<Difference> differences = null)
{
throw new NotImplementedException();
}
}
}
}

View File

@@ -119,4 +119,14 @@
</control>
</tab>
</section>
<section alias="Deploy">
<areas>
<area>content</area>
</areas>
<tab caption="Deploy">
<control>
/App_Plugins/Deploy/views/dashboards/dashboard.html
</control>
</tab>
</section>
</dashBoard>

View File

@@ -1,7 +1,7 @@
<%@ Page Language="C#" AutoEventWireup="True" Inherits="Umbraco.Web.UI.Config.Splashes.NoNodes" CodeBehind="NoNodes.aspx.cs" %>
<%@ Import Namespace="Umbraco.Core.Configuration" %>
<%@ Page Language="C#" AutoEventWireup="true"%>
<%@ Import Namespace="Umbraco.Core.IO" %>
<%@ Import Namespace="Umbraco.Deploy.UI" %>
<%@ Import Namespace="Umbraco.Web" %>
<!doctype html>
<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en"> <![endif]-->
<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en"> <![endif]-->
@@ -11,51 +11,145 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title></title>
<meta name="description" content="">
<meta name="author" content="">
<link rel="stylesheet" href="../../Umbraco/assets/css/nonodes.style.min.css" />
<link href='//fonts.googleapis.com/css?family=Open+Sans:300,400,700,600' rel='stylesheet' type='text/css'>
<link href='//fonts.googleapis.com/css?family=Asap:400,700,400italic,700italic' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="<%=IOHelper.ResolveUrl(SystemDirectories.Umbraco)%>/assets/css/nonodes.style.min.css" />
<link rel="stylesheet" href="<%=IOHelper.ResolveUrl(SystemDirectories.AppPlugins)%>/deploy/assets/css/deploy.css" />
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/modernizr/2.7.1/modernizr.min.js"></script>
<![endif]-->
</head>
<body>
<% if(HttpContext.Current.Request.IsLocal == false){ %>
<section>
<article>
<div>
<div class="logo"></div>
<article>
<div>
<div class="logo"></div>
<h1>Welcome to your Umbraco installation</h1>
<h3>You're seeing this wonderful page because your website doesn't contain any published content yet.</h3>
<h1>Welcome to your Umbraco installation</h1>
<h3>You're seeing the wonderful page because your website doesn't contain any published content yet.</h3>
<div class="cta">
<a href="<%= IOHelper.ResolveUrl(SystemDirectories.Umbraco) %>" class="button">Open Umbraco</a>
</div>
<div class="cta">
<a href="<%= IOHelper.ResolveUrl(SystemDirectories.Umbraco) %>" class="button">Open Umbraco</a>
</div>
<div class="row">
<div class="col">
<h2>Easy start with Umbraco.tv</h2>
<p>We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.</p>
<a href="http://umbraco.tv?ref=tvFromInstaller" target="_blank">Umbraco.tv &rarr;</a>
</div>
<div class="row">
<div class="col">
<h2>Easy start with Umbraco.tv</h2>
<p>We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.</p>
<a href="http://umbraco.tv?ref=tvFromInstaller" target="_blank">Umbraco.tv &rarr;</a>
</div>
<div class="col">
<h2>Be a part of the community</h2>
<p>The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we're sure that you can get your answers from the community.</p>
<a href="http://our.umbraco.org?ref=ourFromInstaller" target="_blank">our.Umbraco &rarr;</a>
</div>
</div>
<div class="col">
<h2>Be a part of the community</h2>
<p>The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, were sure that you can get your answers from the community.</p>
<a href="http://our.umbraco.org?ref=ourFromInstaller" target="_blank">our.Umbraco &rarr;</a>
</div>
</div>
</div>
</article>
</div>
</article>
</section>
<% }else{ %>
<section ng-controller="Umbraco.NoNodes.Controller as vm">
<article class="ud-nonodes">
<div>
<div class="logo"></div>
<div>
<div ng-if="vm.restore.status === ''">
<div ng-if="!vm.requiresInitialization">
<h1>Initializing...</h1>
<small>Please wait while your site is loaded</small>
</div>
<p ng-show="vm.restore.restoreProgress">{{ vm.restore.restoreProgress }}% restored</p>
<small ng-show="vm.restore.currentActivity">{{ vm.restore.currentActivity }}</small>
<div ng-if="vm.requiresInitialization">
<h1>Initialize your site...</h1>
<small>Press the button below to get started</small>
<div class="cta">
<button class="button" ng-click="vm.restoreSchema()">Go!</button>
</div>
</div>
</div>
<div ng-if="vm.restore.status === 'ready'">
<h1>Restore from Umbraco Cloud</h1>
<div class="cta">
<button class="button" ng-click="vm.restoreData()">Restore</button>
<small><span>or</span> <a ng-href="<%= IOHelper.ResolveUrl(SystemDirectories.Umbraco) %>">Skip restore and open Umbraco</a></small>
</div>
</div>
<div ng-if="vm.restore.status === 'inProgress'">
<h1>Restoring your website...</h1>
<p>{{ vm.restore.restoreProgress }}% restored</p>
<small>{{ vm.restore.currentActivity }}</small>
</div>
<div ng-if="vm.restore.status === 'completed'">
<h1>Ready to rock n' roll!</h1>
<p>Everything has been restored and is ready for use, click below to open Umbraco</p>
<div class="cta">
<a href="<%= IOHelper.ResolveUrl(SystemDirectories.Umbraco) %>" class="button">Open Umbraco</a>
</div>
</div>
<ud-error
ng-if="vm.restore.error.hasError"
comment="vm.restore.error.comment"
log="vm.restore.error.log"
exception="vm.restore.error.exception"
status="vm.restore.status"
class="ud-restore-error"
no-nodes="true">
</ud-error>
<%--<div ng-if="vm.restore.error.hasError" class="json">
<h1 style="margin-top: 0;">An error occurred: </h1>
<h2 ng-if="vm.restore.error.exceptionMessage">{{ vm.restore.error.exceptionMessage }}</h2>
<a href="#" ng-click="vm.showLog()" ng-hide="vm.logIsvisible">Show details</a>
<pre ng-if="vm.logIsvisible === true">{{ vm.restore.error.log }}</pre>
</div>--%>
</div>
</div>
</article>
</section>
<script src="https://code.jquery.com/jquery-latest.min.js"></script>
<%= NoNodesHelper.ServerVariables(HttpContext.Current.Request.RequestContext, UmbracoContext.Current) %>
<script type="text/javascript" src="<%= IOHelper.ResolveUrl(SystemDirectories.Umbraco) %>/lib/jquery/jquery.min.js"></script>
<script type="text/javascript" src="<%= IOHelper.ResolveUrl(SystemDirectories.Umbraco) %>/lib/angular/1.1.5/angular.min.js"></script>
<script type="text/javascript" src="<%= IOHelper.ResolveUrl(SystemDirectories.AppPlugins) %>/deploy/lib/signalr/jquery.signalR.min.js"></script>
<script type="text/javascript" src="<%= IOHelper.ResolveUrl(SystemDirectories.Umbraco) %>/backoffice/signalr/hubs"></script>
<script type="text/javascript" src="<%= IOHelper.ResolveUrl(SystemDirectories.AppPlugins) %>/deploy/js/nonodes.modules.js"></script>
<script type="text/javascript" src="<%= IOHelper.ResolveUrl(SystemDirectories.AppPlugins) %>/deploy/js/deploy.services.js"></script>
<script type="text/javascript" src="<%= IOHelper.ResolveUrl(SystemDirectories.AppPlugins) %>/deploy/js/deploy.components.js"></script>
<script type="text/javascript" src="<%= IOHelper.ResolveUrl(SystemDirectories.AppPlugins) %>/deploy/js/nonodes.bootstrap.js"></script>
<script type="text/javascript">
angular.bootstrap(document, ['umbraco.nonodes']);
</script>
<% } %>
</body>
</html>
</html>

View File

@@ -41,4 +41,5 @@
<add initialize="true" sortOrder="0" alias="form" application="forms" title="Forms" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.FormTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="3" alias="prevaluesource" application="forms" title="Prevalue sources" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.PreValueSourceTreeController, Umbraco.Forms.Web" />
<add initialize="true" sortOrder="3" alias="formsecurity" application="users" title="Forms Security" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.FormSecurityTreeController, Umbraco.Forms.Web" />
<add initialize="false" sortOrder="0" alias="emailTemplates" application="forms" title="Email Templates" iconClosed="icon-folder" iconOpen="icon-folder-open" type="Umbraco.Forms.Web.Trees.EmailTemplateTreeController, Umbraco.Forms.Web" />
</trees>