using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Composing; using Umbraco.Core.Services; using Umbraco.Web.Trees; namespace Umbraco.Web.Services { internal class ApplicationTreeService : IApplicationTreeService { private readonly ILogger _logger; private readonly CacheHelper _cache; private Lazy> _allAvailableTrees; internal const string TreeConfigFileName = "trees.config"; private static string _treeConfig; private static readonly object Locker = new object(); public ApplicationTreeService(ILogger logger, CacheHelper cache) { _logger = logger; _cache = cache; } /// /// gets/sets the trees.config file path /// /// /// The setter is generally only going to be used in unit tests, otherwise it will attempt to resolve it using the IOHelper.MapPath /// internal static string TreeConfigFilePath { get { if (string.IsNullOrWhiteSpace(_treeConfig)) { _treeConfig = IOHelper.MapPath(SystemDirectories.Config + "/" + TreeConfigFileName); } return _treeConfig; } set { _treeConfig = value; } } /// /// The main entry point to get application trees /// /// /// This lazily on first access will scan for plugin trees and ensure the trees.config is up-to-date with the plugins. If plugins /// haven't changed on disk then the file will not be saved. The trees are all then loaded from this config file into cache and returned. /// private List GetAppTrees() { return _cache.RuntimeCache.GetCacheItem>( CacheKeys.ApplicationTreeCacheKey, () => { var list = ReadFromXmlAndSort(); //now we can check the non-volatile flag if (_allAvailableTrees != null) { var hasChanges = false; LoadXml(doc => { //Now, load in the xml structure and update it with anything that is not declared there and save the file. //NOTE: On the first iteration here, it will lazily scan all trees, etc... this is because this ienumerable is lazy // based on the ApplicationTreeRegistrar - and as noted there this is not an ideal way to do things but were stuck like this // currently because of the legacy assemblies and types not in the Core. //Get all the trees not registered in the config (those not matching by alias casing will be detected as "unregistered") var unregistered = _allAvailableTrees.Value .Where(x => list.Any(l => l.Alias == x.Alias) == false) .ToArray(); hasChanges = unregistered.Any(); if (hasChanges == false) return false; //add or edit the unregistered ones and re-save the file if any changes were found var count = 0; foreach (var tree in unregistered) { var existingElement = doc.Root.Elements("add").SingleOrDefault(x => string.Equals(x.Attribute("alias").Value, tree.Alias, StringComparison.InvariantCultureIgnoreCase) && string.Equals(x.Attribute("application").Value, tree.ApplicationAlias, StringComparison.InvariantCultureIgnoreCase)); if (existingElement != null) { existingElement.SetAttributeValue("alias", tree.Alias); } else { if (tree.Title.IsNullOrWhiteSpace()) { doc.Root.Add(new XElement("add", new XAttribute("initialize", tree.Initialize), new XAttribute("sortOrder", tree.SortOrder), new XAttribute("alias", tree.Alias), new XAttribute("application", tree.ApplicationAlias), new XAttribute("iconClosed", tree.IconClosed), new XAttribute("iconOpen", tree.IconOpened), new XAttribute("type", tree.Type))); } else { doc.Root.Add(new XElement("add", new XAttribute("initialize", tree.Initialize), new XAttribute("sortOrder", tree.SortOrder), new XAttribute("alias", tree.Alias), new XAttribute("application", tree.ApplicationAlias), new XAttribute("title", tree.Title), new XAttribute("iconClosed", tree.IconClosed), new XAttribute("iconOpen", tree.IconOpened), new XAttribute("type", tree.Type))); } } count++; } //don't save if there's no changes return count > 0; }, true); if (hasChanges) { //If there were changes, we need to re-read the structures from the XML list = ReadFromXmlAndSort(); } } return list; }, new TimeSpan(0, 10, 0)); } /// /// Creates a new application tree. /// /// if set to true [initialize]. /// The sort order. /// The application alias. /// The alias. /// The title. /// The icon closed. /// The icon opened. /// The type. public void MakeNew(bool initialize, int sortOrder, string applicationAlias, string alias, string title, string iconClosed, string iconOpened, string type) { LoadXml(doc => { var el = doc.Root.Elements("add").SingleOrDefault(x => x.Attribute("alias").Value == alias && x.Attribute("application").Value == applicationAlias); if (el == null) { doc.Root.Add(new XElement("add", new XAttribute("initialize", initialize), new XAttribute("sortOrder", sortOrder), new XAttribute("alias", alias), new XAttribute("application", applicationAlias), new XAttribute("title", title), new XAttribute("iconClosed", iconClosed), new XAttribute("iconOpen", iconOpened), new XAttribute("type", type))); } return true; }, true); OnNew(new ApplicationTree(initialize, sortOrder, applicationAlias, alias, title, iconClosed, iconOpened, type), new EventArgs()); } /// /// Saves this instance. /// public void SaveTree(ApplicationTree tree) { LoadXml(doc => { var el = doc.Root.Elements("add").SingleOrDefault(x => x.Attribute("alias").Value == tree.Alias && x.Attribute("application").Value == tree.ApplicationAlias); if (el != null) { el.RemoveAttributes(); el.Add(new XAttribute("initialize", tree.Initialize)); el.Add(new XAttribute("sortOrder", tree.SortOrder)); el.Add(new XAttribute("alias", tree.Alias)); el.Add(new XAttribute("application", tree.ApplicationAlias)); el.Add(new XAttribute("title", tree.Title)); el.Add(new XAttribute("iconClosed", tree.IconClosed)); el.Add(new XAttribute("iconOpen", tree.IconOpened)); el.Add(new XAttribute("type", tree.Type)); } return true; }, true); OnUpdated(tree, new EventArgs()); } /// /// Deletes this instance. /// public void DeleteTree(ApplicationTree tree) { LoadXml(doc => { doc.Root.Elements("add") .Where(x => x.Attribute("application") != null && x.Attribute("application").Value == tree.ApplicationAlias && x.Attribute("alias") != null && x.Attribute("alias").Value == tree.Alias).Remove(); return true; }, true); OnDeleted(tree, new EventArgs()); } /// /// Gets an ApplicationTree by it's tree alias. /// /// The tree alias. /// An ApplicationTree instance public ApplicationTree GetByAlias(string treeAlias) { return GetAppTrees().Find(t => (t.Alias == treeAlias)); } /// /// Gets all applicationTrees registered in umbraco from the umbracoAppTree table.. /// /// Returns a ApplicationTree Array public IEnumerable GetAll() { return GetAppTrees().OrderBy(x => x.SortOrder); } /// /// Gets the application tree for the applcation with the specified alias /// /// The application alias. /// Returns a ApplicationTree Array public IEnumerable GetApplicationTrees(string applicationAlias) { return GetApplicationTrees(applicationAlias, false); } /// /// Gets the application tree for the applcation with the specified alias /// /// The application alias. /// /// Returns a ApplicationTree Array public IEnumerable GetApplicationTrees(string applicationAlias, bool onlyInitialized) { var list = GetAppTrees().FindAll( t => { if (onlyInitialized) return (t.ApplicationAlias == applicationAlias && t.Initialize); return (t.ApplicationAlias == applicationAlias); } ); return list.OrderBy(x => x.SortOrder).ToArray(); } /// /// Loads in the xml structure from disk if one is found, otherwise loads in an empty xml structure, calls the /// callback with the xml document and saves the structure back to disk if saveAfterCallback is true. /// /// /// internal void LoadXml(Func callback, bool saveAfterCallbackIfChanges) { lock (Locker) { var doc = System.IO.File.Exists(TreeConfigFilePath) ? XDocument.Load(TreeConfigFilePath) : XDocument.Parse(""); if (doc.Root != null) { var hasChanges = callback.Invoke(doc); if (saveAfterCallbackIfChanges && hasChanges //Don't save it if it is empty, in some very rare cases if the app domain get's killed in the middle of this process // in some insane way the file saved will be empty. I'm pretty sure it's not actually anything to do with the xml doc and // more about the IO trying to save the XML doc, but it doesn't hurt to check. && doc.Root != null && doc.Root.Elements().Any()) { //ensures the folder exists Directory.CreateDirectory(Path.GetDirectoryName(TreeConfigFilePath)); //saves it doc.Save(TreeConfigFilePath); //remove the cache now that it has changed SD: I'm leaving this here even though it // is taken care of by events as well, I think unit tests may rely on it being cleared here. _cache.RuntimeCache.ClearCacheItem(CacheKeys.ApplicationTreeCacheKey); } } } } private List ReadFromXmlAndSort() { var list = new List(); //read in the xml file containing trees and convert them all to ApplicationTree instances LoadXml(doc => { foreach (var addElement in doc.Root.Elements("add").OrderBy(x => { var sortOrderAttr = x.Attribute("sortOrder"); return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0; })) { var applicationAlias = (string)addElement.Attribute("application"); var type = (string)addElement.Attribute("type"); var assembly = (string)addElement.Attribute("assembly"); var clrType = Type.GetType(type); if (clrType == null) { _logger.Warn(() => $"The tree definition: {addElement} could not be resolved to a .Net object type"); continue; } //check if the tree definition (applicationAlias + type + assembly) is already in the list if (list.Any(tree => tree.ApplicationAlias.InvariantEquals(applicationAlias) && tree.GetRuntimeType() == clrType) == false) { list.Add(new ApplicationTree( addElement.Attribute("initialize") == null || Convert.ToBoolean(addElement.Attribute("initialize").Value), addElement.Attribute("sortOrder") != null ? Convert.ToByte(addElement.Attribute("sortOrder").Value) : (byte)0, (string)addElement.Attribute("application"), (string)addElement.Attribute("alias"), (string)addElement.Attribute("title"), (string)addElement.Attribute("iconClosed"), (string)addElement.Attribute("iconOpen"), (string)addElement.Attribute("type"))); } } return false; }, false); return list; } internal static event TypedEventHandler Deleted; private static void OnDeleted(ApplicationTree app, EventArgs args) { if (Deleted != null) { Deleted(app, args); } } internal static event TypedEventHandler New; private static void OnNew(ApplicationTree app, EventArgs args) { if (New != null) { New(app, args); } } internal static event TypedEventHandler Updated; private static void OnUpdated(ApplicationTree app, EventArgs args) { if (Updated != null) { Updated(app, args); } } /// /// This class is here so that we can provide lazy access to tree scanning for when it is needed /// private class LazyEnumerableTrees : IEnumerable { public LazyEnumerableTrees() { _lazyTrees = new Lazy>(() => { var added = new List(); // Load all Controller Trees by attribute var types = Current.TypeLoader.GetTypesWithAttribute(); // fixme inject //convert them to ApplicationTree instances var items = types .Select(x => new Tuple(x, x.GetCustomAttributes(false).Single())) .Select(x => new ApplicationTree(x.Item2.Initialize, x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, x.Item1.GetFullNameWithAssembly())) .ToArray(); added.AddRange(items.Select(x => x.Alias)); return items.ToArray(); }); } private readonly Lazy> _lazyTrees; /// /// Returns an enumerator that iterates through the collection. /// /// /// A that can be used to iterate through the collection. /// public IEnumerator GetEnumerator() { return _lazyTrees.Value.GetEnumerator(); } /// /// Returns an enumerator that iterates through a collection. /// /// /// An object that can be used to iterate through the collection. /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } } }