From a32d38f7eaf21f10325b806c86bc16e5297cb69e Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 16 Dec 2014 14:26:53 +1100 Subject: [PATCH 1/6] Fixes: U4-5992 Caching of XPathNodeIterator in umbraco.library is not thread safe and will cause media errors like valueDictionary exception --- .../XmlPublishedCache/PublishedMediaCache.cs | 8 ++ .../umbraco.presentation/library.cs | 103 ++++++++++-------- 2 files changed, 63 insertions(+), 48 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index 7488b097ac..2ec583597b 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -191,6 +191,10 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } + LogHelper.Debug( + "Could not retrieve media {0} from Examine index, reverting to looking up media via legacy library.GetMedia method", + () => id); + var media = global::umbraco.library.GetMedia(id, true); if (media != null && media.Current != null) { @@ -212,6 +216,10 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache return ConvertFromXPathNavigator(media.Current); } + LogHelper.Debug( + "Could not retrieve media {0} from Examine index or from legacy library.GetMedia method", + () => id); + return null; } diff --git a/src/Umbraco.Web/umbraco.presentation/library.cs b/src/Umbraco.Web/umbraco.presentation/library.cs index 3874ad7b3a..3fb2a5d9a8 100644 --- a/src/Umbraco.Web/umbraco.presentation/library.cs +++ b/src/Umbraco.Web/umbraco.presentation/library.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.IO; +using System.Linq; using System.Net; using System.Net.Mail; using System.Text; @@ -8,35 +9,31 @@ using System.Text.RegularExpressions; using System.Web; using System.Web.UI; using System.Xml; +using System.Xml.Linq; using System.Xml.XPath; +using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Web; using Umbraco.Web.Cache; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; using Umbraco.Web.Templates; using umbraco.BusinessLogic; using umbraco.cms.businesslogic; -using umbraco.cms.businesslogic.media; -using umbraco.cms.businesslogic.member; -using umbraco.cms.businesslogic.propertytype; -using umbraco.cms.businesslogic.relation; using umbraco.cms.businesslogic.web; using umbraco.cms.helpers; -using umbraco.presentation.cache; using umbraco.scripting; using umbraco.DataLayer; -using System.Web.Security; -using umbraco.cms.businesslogic.language; using Umbraco.Core.IO; -using System.Collections; -using System.Collections.Generic; -using umbraco.cms.businesslogic.cache; -using umbraco.NodeFactory; +using Language = umbraco.cms.businesslogic.language.Language; +using Media = umbraco.cms.businesslogic.media.Media; +using Member = umbraco.cms.businesslogic.member.Member; +using PropertyType = umbraco.cms.businesslogic.propertytype.PropertyType; +using Relation = umbraco.cms.businesslogic.relation.Relation; using UmbracoContext = umbraco.presentation.UmbracoContext; -using System.Linq; namespace umbraco { @@ -470,46 +467,48 @@ namespace umbraco { if (UmbracoSettings.UmbracoLibraryCacheDuration > 0) { - XPathNodeIterator retVal = ApplicationContext.Current.ApplicationCache.GetCacheItem( + var xml = ApplicationContext.Current.ApplicationCache.GetCacheItem( string.Format( "{0}_{1}_{2}", CacheKeys.MediaCacheKey, MediaId, Deep), TimeSpan.FromSeconds(UmbracoSettings.UmbracoLibraryCacheDuration), () => GetMediaDo(MediaId, Deep)); - if (retVal != null) - return retVal; + if (xml != null) + { + //returning the root element of the Media item fixes the problem + return xml.CreateNavigator().Select("/"); + } + } else { - return GetMediaDo(MediaId, Deep); + var xml = GetMediaDo(MediaId, Deep); + + //returning the root element of the Media item fixes the problem + return xml.CreateNavigator().Select("/"); } - } - catch + catch(Exception ex) { + LogHelper.Error("An error occurred looking up media", ex); } - XmlDocument xd = new XmlDocument(); + LogHelper.Debug("No media result for id {0}", () => MediaId); + + var xd = new XmlDocument(); xd.LoadXml(string.Format("No media is maching '{0}'", MediaId)); return xd.CreateNavigator().Select("/"); } - private static XPathNodeIterator GetMediaDo(int mediaId, bool deep) + private static XElement GetMediaDo(int mediaId, bool deep) { - var m = new Media(mediaId); - if (m.nodeObjectType == Media._objectType) - { - var mXml = new XmlDocument(); - var xml = m.ToXml(mXml, deep); - //This will be null if the media isn't public (meaning it is in the trash) - if (xml == null) return null; - //TODO: This is an aweful way of loading in XML - it is very slow. - mXml.LoadXml(xml.OuterXml); - var xp = mXml.CreateNavigator(); - var xpath = UmbracoSettings.UseLegacyXmlSchema ? "/node" : String.Format("/{0}", Casing.SafeAliasWithForcingCheck(m.ContentType.Alias)); - return xp.Select(xpath); - } - return null; + var media = ApplicationContext.Current.Services.MediaService.GetById(mediaId); + if (media == null) return null; + var serializer = new EntityXmlSerializer(); + var serialized = serializer.Serialize( + ApplicationContext.Current.Services.MediaService, ApplicationContext.Current.Services.DataTypeService, + media, deep); + return serialized; } /// @@ -525,35 +524,42 @@ namespace umbraco { if (UmbracoSettings.UmbracoLibraryCacheDuration > 0) { - var retVal = ApplicationContext.Current.ApplicationCache.GetCacheItem( + var xml = ApplicationContext.Current.ApplicationCache.GetCacheItem( string.Format( "{0}_{1}", CacheKeys.MemberLibraryCacheKey, MemberId), TimeSpan.FromSeconds(UmbracoSettings.UmbracoLibraryCacheDuration), () => GetMemberDo(MemberId)); - if (retVal != null) - return retVal.CreateNavigator().Select("/"); + if (xml != null) + { + return xml.CreateNavigator().Select("/"); + } } else { return GetMemberDo(MemberId).CreateNavigator().Select("/"); } - } - catch + catch (Exception ex) { + LogHelper.Error("An error occurred looking up member", ex); } - XmlDocument xd = new XmlDocument(); + + LogHelper.Debug("No member result for id {0}", () => MemberId); + + var xd = new XmlDocument(); xd.LoadXml(string.Format("No member is maching '{0}'", MemberId)); return xd.CreateNavigator().Select("/"); } - private static XmlDocument GetMemberDo(int MemberId) + private static XElement GetMemberDo(int MemberId) { - Member m = new Member(MemberId); - XmlDocument mXml = new XmlDocument(); - mXml.LoadXml(m.ToXml(mXml, false).OuterXml); - return mXml; + var member = ApplicationContext.Current.Services.MemberService.GetById(MemberId); + if (member == null) return null; + var serializer = new EntityXmlSerializer(); + var serialized = serializer.Serialize( + ApplicationContext.Current.Services.DataTypeService, member); + return serialized; } /// @@ -1352,8 +1358,9 @@ namespace umbraco nav.MoveToId(HttpContext.Current.Items["pageID"].ToString()); return nav.Select("."); } - catch + catch (Exception ex) { + LogHelper.Error("Could not retrieve current xml node", ex); } XmlDocument xd = new XmlDocument(); From e643f2a0ba1f1a3a8f9271d10fe731e9a2b8eae1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 18 Dec 2014 14:20:41 +1100 Subject: [PATCH 2/6] working on U4-6030 --- .../umbraco/Trees/TreeDefinitionCollection.cs | 128 +++++++++--------- 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/TreeDefinitionCollection.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/TreeDefinitionCollection.cs index f1c5d4004b..4b43065538 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/TreeDefinitionCollection.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/TreeDefinitionCollection.cs @@ -27,8 +27,9 @@ namespace umbraco.cms.presentation.Trees //create singleton private static readonly TreeDefinitionCollection instance = new TreeDefinitionCollection(); - private static readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(); - + private static readonly object Locker = new object(); + private static volatile bool _ensureTrees = false; + public static TreeDefinitionCollection Instance { get @@ -127,7 +128,12 @@ namespace umbraco.cms.presentation.Trees public void ReRegisterTrees() { - EnsureTreesRegistered(true); + //clears the trees/flag so that they are lazily refreshed on next access + lock (Locker) + { + this.Clear(); + _ensureTrees = false; + } } /// @@ -136,70 +142,68 @@ namespace umbraco.cms.presentation.Trees /// This will also store an instance of each tree object in the TreeDefinition class which should be /// used when referencing all tree classes. /// - private void EnsureTreesRegistered(bool clearFirst = false) + private void EnsureTreesRegistered() { - using (var l = new UpgradeableReadLock(Lock)) - { - if (clearFirst) - { - this.Clear(); - } + if (_ensureTrees == false) + { + lock (Locker) + { + if (_ensureTrees == false) + { - //if we already have tree, exit - if (this.Count > 0) - return; + var foundITrees = PluginManager.Current.ResolveTrees(); - l.UpgradeToWriteLock(); + var objTrees = ApplicationTree.getAll(); + var appTrees = new List(); + appTrees.AddRange(objTrees); + + var apps = Application.getAll(); + + foreach (var type in foundITrees) + { + + //find the Application tree's who's combination of assembly name and tree type is equal to + //the Type that was found's full name. + //Since a tree can exist in multiple applications we'll need to register them all. + + //The logic of this has changed in 6.0: http://issues.umbraco.org/issue/U4-1360 + // we will support the old legacy way but the normal way is to match on assembly qualified names + + var appTreesForType = appTrees.FindAll( + tree => + { + //match the type on assembly qualified name if the assembly attribute is empty or if the + // tree type contains a comma (meaning it is assembly qualified) + if (tree.AssemblyName.IsNullOrWhiteSpace() || tree.Type.Contains(",")) + { + return tree.GetRuntimeType() == type; + } + + //otherwise match using legacy match rules + return (string.Format("{0}.{1}", tree.AssemblyName, tree.Type).InvariantEquals(type.FullName)); + } + ); + + foreach (var appTree in appTreesForType) + { + //find the Application object whos name is the same as our appTree ApplicationAlias + var app = apps.Find( + a => (a.alias == appTree.ApplicationAlias) + ); + + var def = new TreeDefinition(type, appTree, app); + this.Add(def); + } + } + //sort our trees with the sort order definition + this.Sort((t1, t2) => t1.Tree.SortOrder.CompareTo(t2.Tree.SortOrder)); + + _ensureTrees = true; + } + } + } - var foundITrees = PluginManager.Current.ResolveTrees(); - - var objTrees = ApplicationTree.getAll(); - var appTrees = new List(); - appTrees.AddRange(objTrees); - - var apps = Application.getAll(); - - foreach (var type in foundITrees) - { - - //find the Application tree's who's combination of assembly name and tree type is equal to - //the Type that was found's full name. - //Since a tree can exist in multiple applications we'll need to register them all. - - //The logic of this has changed in 6.0: http://issues.umbraco.org/issue/U4-1360 - // we will support the old legacy way but the normal way is to match on assembly qualified names - - var appTreesForType = appTrees.FindAll( - tree => - { - //match the type on assembly qualified name if the assembly attribute is empty or if the - // tree type contains a comma (meaning it is assembly qualified) - if (tree.AssemblyName.IsNullOrWhiteSpace() || tree.Type.Contains(",")) - { - return tree.GetRuntimeType() == type; - } - - //otherwise match using legacy match rules - return (string.Format("{0}.{1}", tree.AssemblyName, tree.Type).InvariantEquals(type.FullName)); - } - ); - - foreach (var appTree in appTreesForType) - { - //find the Application object whos name is the same as our appTree ApplicationAlias - var app = apps.Find( - a => (a.alias == appTree.ApplicationAlias) - ); - - var def = new TreeDefinition(type, appTree, app); - this.Add(def); - } - } - //sort our trees with the sort order definition - this.Sort((t1, t2) => t1.Tree.SortOrder.CompareTo(t2.Tree.SortOrder)); - - } } } From aa439cacda01de5762df44109f4933d5ee5c0512 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 18 Dec 2014 14:58:49 +1100 Subject: [PATCH 3/6] working on U4-6030 --- src/umbraco.businesslogic/ApplicationTree.cs | 213 ++++++++++++++---- .../ApplicationTreeRegistrar.cs | 204 ++++++++++++----- 2 files changed, 307 insertions(+), 110 deletions(-) diff --git a/src/umbraco.businesslogic/ApplicationTree.cs b/src/umbraco.businesslogic/ApplicationTree.cs index f8e51546f7..44a57ecbd9 100644 --- a/src/umbraco.businesslogic/ApplicationTree.cs +++ b/src/umbraco.businesslogic/ApplicationTree.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.IO; using Umbraco.Core.Events; using Umbraco.Core.IO; +using Umbraco.Core.Logging; using umbraco.DataLayer; namespace umbraco.BusinessLogic @@ -24,6 +25,23 @@ namespace umbraco.BusinessLogic internal const string TreeConfigFileName = "trees.config"; private static string _treeConfig; private static readonly object Locker = new object(); + private static volatile bool _isInitialized = false; + private static IEnumerable _allAvailableTrees; + + /// + /// Initializes the service with any trees found in plugins + /// + /// + /// A collection of all available tree found in assemblies in the application + /// + /// + /// This will update the trees.config with the found tree plugins that are not currently listed in the file when the first + /// access is made to resolve the tree collection + /// + internal static void Intitialize(IEnumerable allAvailableTrees) + { + _allAvailableTrees = allAvailableTrees; + } /// /// gets/sets the trees.config file path @@ -45,58 +63,136 @@ namespace umbraco.BusinessLogic } /// - /// The cache storage for all application trees + /// The main entry point to get application trees /// - private static List AppTrees + /// + /// 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 static List GetAppTrees() { - get - { - return ApplicationContext.Current.ApplicationCache.GetCacheItem( - CacheKeys.ApplicationTreeCacheKey, - () => + return ApplicationContext.Current.ApplicationCache.GetCacheItem>( + CacheKeys.ApplicationTreeCacheKey, + () => + { + var list = ReadFromXmlAndSort(); + + //On first access we need to do some initialization + if (_isInitialized == false) + { + lock (Locker) { - var list = new List(); - - LoadXml(doc => + if (_isInitialized == false) { - foreach (var addElement in doc.Root.Elements("add").OrderBy(x => - { - var sortOrderAttr = x.Attribute("sortOrder"); - return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0; - })) + //now we can check the non-volatile flag + if (_allAvailableTrees != null) { + var hasChanges = false; - var applicationAlias = (string)addElement.Attribute("application"); - var type = (string)addElement.Attribute("type"); - var assembly = (string)addElement.Attribute("assembly"); - - //check if the tree definition (applicationAlias + type + assembly) is already in the list - - if (!list.Any(tree => tree.ApplicationAlias.InvariantEquals(applicationAlias) - && tree.Type.InvariantEquals(type) - && tree.AssemblyName.InvariantEquals(assembly))) + LoadXml(doc => { - list.Add(new ApplicationTree( - addElement.Attribute("silent") != null ? Convert.ToBoolean(addElement.Attribute("silent").Value) : false, - addElement.Attribute("initialize") != null ? Convert.ToBoolean(addElement.Attribute("initialize").Value) : true, - addElement.Attribute("sortOrder") != null ? Convert.ToByte(addElement.Attribute("sortOrder").Value) : (byte)0, - addElement.Attribute("application").Value, - addElement.Attribute("alias").Value, - addElement.Attribute("title").Value, - addElement.Attribute("iconClosed").Value, - addElement.Attribute("iconOpen").Value, - (string)addElement.Attribute("assembly"), //this could be empty: http://issues.umbraco.org/issue/U4-1360 - addElement.Attribute("type").Value, - addElement.Attribute("action") != null ? addElement.Attribute("action").Value : "")); + //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 + var unregistered = _allAvailableTrees + .Where(x => list.Any(l => l.Alias == x.Alias) == false) + .ToArray(); + + hasChanges = unregistered.Any(); + + if (hasChanges == false) return false; + + //add the unregistered ones to the list and re-save the file if any changes were found + var count = 0; + foreach (var tree in unregistered) + { + 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(); } - - } - }, false); - return list; - }); - } + _isInitialized = true; + } + } + } + + + return list; + + + }); + } + + private static 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 = System.Type.GetType(type); + if (clrType == null) + { + LogHelper.Warn(typeof(ApplicationTree), "The tree definition: " + addElement.ToString() + " 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("silent") != null && Convert.ToBoolean(addElement.Attribute("silent").Value), + addElement.Attribute("initialize") == null || Convert.ToBoolean(addElement.Attribute("initialize").Value), + addElement.Attribute("sortOrder") != null ? Convert.ToByte(addElement.Attribute("sortOrder").Value) : (byte)0, + addElement.Attribute("application").Value, + addElement.Attribute("alias").Value, + addElement.Attribute("title").Value, + addElement.Attribute("iconClosed").Value, + addElement.Attribute("iconOpen").Value, + (string)addElement.Attribute("assembly"), //this could be empty: http://issues.umbraco.org/issue/U4-1360 + addElement.Attribute("type").Value, + addElement.Attribute("action") != null ? addElement.Attribute("action").Value : "")); + } + } + + return false; + + }, false); + + return list; } /// @@ -256,6 +352,9 @@ namespace umbraco.BusinessLogic new XAttribute("type", type), new XAttribute("action", string.IsNullOrEmpty(action) ? "" : action))); } + + return true; + }, true); OnNew(new ApplicationTree(silent, initialize, sortOrder, applicationAlias, alias, title, iconClosed, iconOpened, assemblyName, type, action), new EventArgs()); @@ -287,6 +386,8 @@ namespace umbraco.BusinessLogic el.Add(new XAttribute("action", string.IsNullOrEmpty(this.Action) ? "" : this.Action)); } + return true; + }, true); OnUpdated(this, new EventArgs()); @@ -304,6 +405,9 @@ namespace umbraco.BusinessLogic { doc.Root.Elements("add").Where(x => x.Attribute("application") != null && x.Attribute("application").Value == this.ApplicationAlias && x.Attribute("alias") != null && x.Attribute("alias").Value == this.Alias).Remove(); + + return true; + }, true); OnDeleted(this, new EventArgs()); @@ -317,7 +421,7 @@ namespace umbraco.BusinessLogic /// An ApplicationTree instance public static ApplicationTree getByAlias(string treeAlias) { - return AppTrees.Find(t => (t.Alias == treeAlias)); + return GetAppTrees().Find(t => (t.Alias == treeAlias)); } @@ -327,7 +431,7 @@ namespace umbraco.BusinessLogic /// Returns a ApplicationTree Array public static ApplicationTree[] getAll() { - return AppTrees.OrderBy(x => x.SortOrder).ToArray(); + return GetAppTrees().OrderBy(x => x.SortOrder).ToArray(); } /// @@ -348,7 +452,7 @@ namespace umbraco.BusinessLogic /// Returns a ApplicationTree Array public static ApplicationTree[] getApplicationTree(string applicationAlias, bool onlyInitializedApplications) { - var list = AppTrees.FindAll( + var list = GetAppTrees().FindAll( t => { if (onlyInitializedApplications) @@ -360,21 +464,34 @@ namespace umbraco.BusinessLogic return list.OrderBy(x => x.SortOrder).ToArray(); } - internal static void LoadXml(Action callback, bool saveAfterCallback) + /// + /// 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 static void LoadXml(Func callback, bool saveAfterCallbackIfChanges) { lock (Locker) { var doc = File.Exists(TreeConfigFilePath) ? XDocument.Load(TreeConfigFilePath) : XDocument.Parse(""); + if (doc.Root != null) { - callback.Invoke(doc); + var hasChanges = callback.Invoke(doc); - if (saveAfterCallback) + 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 diff --git a/src/umbraco.businesslogic/ApplicationTreeRegistrar.cs b/src/umbraco.businesslogic/ApplicationTreeRegistrar.cs index e12eb2f21b..f9f83f3bbf 100644 --- a/src/umbraco.businesslogic/ApplicationTreeRegistrar.cs +++ b/src/umbraco.businesslogic/ApplicationTreeRegistrar.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Umbraco.Core; @@ -9,81 +11,159 @@ using umbraco.interfaces; namespace umbraco.BusinessLogic { - public class ApplicationTreeRegistrar : IApplicationStartupHandler + public class ApplicationTreeRegistrar : ApplicationEventHandler { - public ApplicationTreeRegistrar() + protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - //don't do anything if the application or database is not configured! - if (ApplicationContext.Current == null - || !ApplicationContext.Current.IsConfigured - || !ApplicationContext.Current.DatabaseContext.IsDatabaseConfigured) - return; + //Call initialize on the tree service with the lazy enumerable class below, when the tree service needs to resolve, + // it will lazily do the scanning and comparing so it's not actually done on app start. + ApplicationTree.Intitialize(new LazyEnumerableTrees()); + } - // Load all Trees by attribute and add them to the XML config - var types = PluginManager.Current.ResolveAttributedTrees(); + //public ApplicationTreeRegistrar() + //{ + // //don't do anything if the application or database is not configured! + // if (ApplicationContext.Current == null + // || !ApplicationContext.Current.IsConfigured + // || !ApplicationContext.Current.DatabaseContext.IsDatabaseConfigured) + // return; - var items = types - .Select(x => - new Tuple(x, x.GetCustomAttributes(false).Single())) - .Where(x => ApplicationTree.getByAlias(x.Item2.Alias) == null); + // // Load all Trees by attribute and add them to the XML config + // var types = PluginManager.Current.ResolveAttributedTrees(); - var allAliases = ApplicationTree.getAll().Select(x => x.Alias).Concat(items.Select(x => x.Item2.Alias)); - var inString = "'" + string.Join("','", allAliases) + "'"; + // var items = types + // .Select(x => + // new Tuple(x, x.GetCustomAttributes(false).Single())) + // .Where(x => ApplicationTree.getByAlias(x.Item2.Alias) == null); - ApplicationTree.LoadXml(doc => + // var allAliases = ApplicationTree.getAll().Select(x => x.Alias).Concat(items.Select(x => x.Item2.Alias)); + // var inString = "'" + string.Join("','", allAliases) + "'"; + + // ApplicationTree.LoadXml(doc => + // { + // foreach (var tuple in items) + // { + // var type = tuple.Item1; + // var attr = tuple.Item2; + + // //Add the new tree that doesn't exist in the config that was found by type finding + + // doc.Root.Add(new XElement("add", + // new XAttribute("silent", attr.Silent), + // new XAttribute("initialize", attr.Initialize), + // new XAttribute("sortOrder", attr.SortOrder), + // new XAttribute("alias", attr.Alias), + // new XAttribute("application", attr.ApplicationAlias), + // new XAttribute("title", attr.Title), + // new XAttribute("iconClosed", attr.IconClosed), + // new XAttribute("iconOpen", attr.IconOpen), + // // don't add the assembly, we don't need this: + // // http://issues.umbraco.org/issue/U4-1360 + // //new XAttribute("assembly", assemblyName), + // //new XAttribute("type", typeName), + // // instead, store the assembly type name + // new XAttribute("type", type.GetFullNameWithAssembly()), + // new XAttribute("action", attr.Action))); + // } + + // //add any trees that were found in the database that don't exist in the config + + // var db = ApplicationContext.Current.DatabaseContext.Database; + // var exist = db.TableExist("umbracoAppTree"); + // if (exist) + // { + // var appTrees = db.Fetch("WHERE treeAlias NOT IN (" + inString + ")"); + // foreach (var appTree in appTrees) + // { + // var action = appTree.Action; + + // doc.Root.Add(new XElement("add", + // new XAttribute("silent", appTree.Silent), + // new XAttribute("initialize", appTree.Initialize), + // new XAttribute("sortOrder", appTree.SortOrder), + // new XAttribute("alias", appTree.Alias), + // new XAttribute("application", appTree.AppAlias), + // new XAttribute("title", appTree.Title), + // new XAttribute("iconClosed", appTree.IconClosed), + // new XAttribute("iconOpen", appTree.IconOpen), + // new XAttribute("assembly", appTree.HandlerAssembly), + // new XAttribute("type", appTree.HandlerType), + // new XAttribute("action", string.IsNullOrEmpty(action) ? "" : action))); + // } + // } + + // }, true); + //} + + /// + /// 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() { - foreach (var tuple in items) + _lazyTrees = new Lazy>(() => { - var type = tuple.Item1; - var attr = tuple.Item2; - - //Add the new tree that doesn't exist in the config that was found by type finding + var added = new List(); - doc.Root.Add(new XElement("add", - new XAttribute("silent", attr.Silent), - new XAttribute("initialize", attr.Initialize), - new XAttribute("sortOrder", attr.SortOrder), - new XAttribute("alias", attr.Alias), - new XAttribute("application", attr.ApplicationAlias), - new XAttribute("title", attr.Title), - new XAttribute("iconClosed", attr.IconClosed), - new XAttribute("iconOpen", attr.IconOpen), - // don't add the assembly, we don't need this: - // http://issues.umbraco.org/issue/U4-1360 - //new XAttribute("assembly", assemblyName), - //new XAttribute("type", typeName), - // instead, store the assembly type name - new XAttribute("type", type.GetFullNameWithAssembly()), - new XAttribute("action", attr.Action))); - } + // Load all Controller Trees by attribute + var types = PluginManager.Current.ResolveAttributedTrees(); + //convert them to ApplicationTree instances + var items = types + .Select(x => + new Tuple(x, x.GetCustomAttributes(false).Single())) + .Select(x => new ApplicationTree( + x.Item2.Silent, x.Item2.Initialize, (byte) x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, + "", + x.Item1.GetFullNameWithAssembly(), + x.Item2.Action)) + .ToArray(); - //add any trees that were found in the database that don't exist in the config + added.AddRange(items.Select(x => x.Alias)); - var db = ApplicationContext.Current.DatabaseContext.Database; - var exist = db.TableExist("umbracoAppTree"); - if (exist) - { - var appTrees = db.Fetch("WHERE treeAlias NOT IN (" + inString + ")"); - foreach (var appTree in appTrees) - { - var action = appTree.Action; + //find the legacy trees + var legacyTreeTypes = PluginManager.Current.ResolveAttributedTrees(); + //convert them to ApplicationTree instances + var legacyItems = legacyTreeTypes + .Select(x => + new Tuple( + x, + x.GetCustomAttributes(false).SingleOrDefault())) + .Where(x => x.Item2 != null) + //make sure the legacy tree isn't added on top of the controller tree! + .Where(x => added.InvariantContains(x.Item2.Alias) == false) + .Select(x => new ApplicationTree(x.Item2.Silent, x.Item2.Initialize, (byte) x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, + "", + x.Item1.GetFullNameWithAssembly(), + x.Item2.Action)); - doc.Root.Add(new XElement("add", - new XAttribute("silent", appTree.Silent), - new XAttribute("initialize", appTree.Initialize), - new XAttribute("sortOrder", appTree.SortOrder), - new XAttribute("alias", appTree.Alias), - new XAttribute("application", appTree.AppAlias), - new XAttribute("title", appTree.Title), - new XAttribute("iconClosed", appTree.IconClosed), - new XAttribute("iconOpen", appTree.IconOpen), - new XAttribute("assembly", appTree.HandlerAssembly), - new XAttribute("type", appTree.HandlerType), - new XAttribute("action", string.IsNullOrEmpty(action) ? "" : action))); - } - } + return items.Concat(legacyItems).ToArray(); + }); + } - }, true); + 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(); + } } } } \ No newline at end of file From 3c01dcb30c2911342b4706bfacd271dd1247e240 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 18 Dec 2014 15:16:29 +1100 Subject: [PATCH 4/6] Fixes: U4-6030 --- src/umbraco.businesslogic/Application.cs | 157 +++++++++++++----- .../ApplicationRegistrar.cs | 105 ++++++------ .../ApplicationTreeRegistrar.cs | 75 --------- 3 files changed, 171 insertions(+), 166 deletions(-) diff --git a/src/umbraco.businesslogic/Application.cs b/src/umbraco.businesslogic/Application.cs index efa7131196..cd4633a23d 100644 --- a/src/umbraco.businesslogic/Application.cs +++ b/src/umbraco.businesslogic/Application.cs @@ -26,6 +26,22 @@ namespace umbraco.BusinessLogic internal const string AppConfigFileName = "applications.config"; private static string _appConfig; private static readonly object Locker = new object(); + private static IEnumerable _allAvailableSections; + private static volatile bool _isInitialized = false; + + /// + /// Initializes the service with all available application plugins + /// + /// + /// All application plugins found in assemblies + /// + /// + /// This is used to populate the app.config file with any applications declared in plugins that don't exist in the file + /// + internal static void Initialize(IEnumerable allAvailableSections) + { + _allAvailableSections = allAvailableSections; + } /// /// gets/sets the application.config file path @@ -49,52 +65,75 @@ namespace umbraco.BusinessLogic /// /// The cache storage for all applications /// - internal static List Apps + public static List GetSections() { - get - { - return ApplicationContext.Current.ApplicationCache.GetCacheItem( - CacheKeys.ApplicationsCacheKey, - () => + return ApplicationContext.Current.ApplicationCache.GetCacheItem>( + CacheKeys.ApplicationsCacheKey, + () => + { + ////used for unit tests + //if (_testApps != null) + // return _testApps; + + var list = ReadFromXmlAndSort(); + + //On first access we need to do some initialization + if (_isInitialized == false) + { + lock (Locker) { - ////used for unit tests - //if (_testApps != null) - // return _testApps; - - var tmp = new List(); - - try + if (_isInitialized == false) { - LoadXml(doc => + //now we can check the non-volatile flag + if (_allAvailableSections != null) + { + var hasChanges = false; + + 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; - })) + //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 apps, etc... this is because this ienumerable is lazy + // based on the ApplicationRegistrar - 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 + var unregistered = _allAvailableSections + .Where(x => list.Any(l => l.alias == x.alias) == false) + .ToArray(); + + hasChanges = unregistered.Any(); + + var count = 0; + foreach (var attr in unregistered) { - var sortOrderAttr = addElement.Attribute("sortOrder"); - tmp.Add(new Application(addElement.Attribute("name").Value, - addElement.Attribute("alias").Value, - addElement.Attribute("icon").Value, - sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0)); + doc.Root.Add(new XElement("add", + new XAttribute("alias", attr.alias), + new XAttribute("name", attr.name), + new XAttribute("icon", attr.icon), + new XAttribute("sortOrder", attr.sortOrder))); + count++; } - }, false); - return tmp; - } - catch - { - //this is a bit of a hack that just ensures the application doesn't crash when the - //installer is run and there is no database or connection string defined. - //the reason this method may get called during the installation is that the - //SqlHelper of this class is shared amongst everything "Application" wide. + //don't save if there's no changes + return count > 0; + }, true); - //TODO: Perhaps we should log something here?? - return null; + if (hasChanges) + { + //If there were changes, we need to re-read the structures from the XML + list = ReadFromXmlAndSort(); + } + } + + _isInitialized = true; } - }); - } + } + } + + return list; + + }); } /// @@ -200,7 +239,7 @@ namespace umbraco.BusinessLogic [MethodImpl(MethodImplOptions.Synchronized)] public static void MakeNew(string name, string alias, string icon) { - MakeNew(name, alias, icon, Apps.Max(x => x.sortOrder) + 1); + MakeNew(name, alias, icon, GetSections().Max(x => x.sortOrder) + 1); } /// @@ -224,6 +263,9 @@ namespace umbraco.BusinessLogic new XAttribute("name", name), new XAttribute("icon", icon), new XAttribute("sortOrder", sortOrder))); + + return true; + }, true); //raise event @@ -238,7 +280,7 @@ namespace umbraco.BusinessLogic /// public static Application getByAlias(string appAlias) { - return Apps.Find(t => t.alias == appAlias); + return GetSections().Find(t => t.alias == appAlias); } /// @@ -259,6 +301,9 @@ namespace umbraco.BusinessLogic LoadXml(doc => { doc.Root.Elements("add").Where(x => x.Attribute("alias") != null && x.Attribute("alias").Value == this.alias).Remove(); + + return true; + }, true); //raise event @@ -271,7 +316,7 @@ namespace umbraco.BusinessLogic /// Returns a Application Array public static List getAll() { - return Apps; + return GetSections(); } /// @@ -282,8 +327,32 @@ namespace umbraco.BusinessLogic { ApplicationStartupHandler.RegisterHandlers(); } - - internal static void LoadXml(Action callback, bool saveAfterCallback) + + private static List ReadFromXmlAndSort() + { + var tmp = new List(); + + 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 sortOrderAttr = addElement.Attribute("sortOrder"); + tmp.Add(new Application(addElement.Attribute("name").Value, + addElement.Attribute("alias").Value, + addElement.Attribute("icon").Value, + sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0)); + } + return false; + }, false); + + return tmp; + } + + internal static void LoadXml(Func callback, bool saveAfterCallbackIfChanged) { lock (Locker) { @@ -293,9 +362,9 @@ namespace umbraco.BusinessLogic if (doc.Root != null) { - callback.Invoke(doc); + var changed = callback.Invoke(doc); - if (saveAfterCallback) + if (saveAfterCallbackIfChanged && changed) { //ensure the folder is created! Directory.CreateDirectory(Path.GetDirectoryName(AppConfigFilePath)); diff --git a/src/umbraco.businesslogic/ApplicationRegistrar.cs b/src/umbraco.businesslogic/ApplicationRegistrar.cs index 8eb695eebb..6b3c73d8bd 100644 --- a/src/umbraco.businesslogic/ApplicationRegistrar.cs +++ b/src/umbraco.businesslogic/ApplicationRegistrar.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Configuration; using System.Data.SqlClient; using System.Linq; @@ -12,8 +14,21 @@ using umbraco.interfaces; namespace umbraco.BusinessLogic { - public class ApplicationRegistrar : IApplicationStartupHandler + /// + /// A startup handler for putting the app config in the config file based on attributes found + /// + /// /// + /// TODO: This is really not a very ideal process but the code is found here because tree plugins are in the Web project or the legacy business logic project. + /// Moving forward we can put the base tree plugin classes in the core and then this can all just be taken care of normally within the service. + /// + public class ApplicationRegistrar : ApplicationEventHandler { + protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + //initialize the section service with a lazy collection of found app plugins + Application.Initialize(new LazyEnumerableSections()); + } + private ISqlHelper _sqlHelper; protected ISqlHelper SqlHelper { @@ -32,55 +47,51 @@ namespace umbraco.BusinessLogic } } - public ApplicationRegistrar() + /// + /// This class is here so that we can provide lazy access to tree scanning for when it is needed + /// + private class LazyEnumerableSections : IEnumerable { - - //don't do anything if the application is not configured! - if (!ApplicationContext.Current.IsConfigured) - return; - - // Load all Applications by attribute and add them to the XML config - var types = PluginManager.Current.ResolveApplications(); - - //since applications don't populate their metadata from the attribute and because it is an interface, - //we need to interrogate the attributes for the data. Would be better to have a base class that contains - //metadata populated by the attribute. Oh well i guess. - var attrs = types.Select(x => x.GetCustomAttributes(false).Single()) - .Where(x => Application.getByAlias(x.Alias) == null); - - var allAliases = Application.getAll().Select(x => x.alias).Concat(attrs.Select(x => x.Alias)); - var inString = "'" + string.Join("','", allAliases) + "'"; - - Application.LoadXml(doc => + public LazyEnumerableSections() + { + _lazySections = new Lazy>(() => { - foreach (var attr in attrs) - { - doc.Root.Add(new XElement("add", - new XAttribute("alias", attr.Alias), - new XAttribute("name", attr.Name), - new XAttribute("icon", attr.Icon), - new XAttribute("sortOrder", attr.SortOrder))); - } - - var db = ApplicationContext.Current.DatabaseContext.Database; - var exist = db.TableExist("umbracoApp"); - if (exist) - { - var dbApps = SqlHelper.ExecuteReader("SELECT * FROM umbracoApp WHERE appAlias NOT IN (" + inString + ")"); - while (dbApps.Read()) - { - doc.Root.Add(new XElement("add", - new XAttribute("alias", dbApps.GetString("appAlias")), - new XAttribute("name", dbApps.GetString("appName")), - new XAttribute("icon", dbApps.GetString("appIcon")), - new XAttribute("sortOrder", dbApps.GetByte("sortOrder")))); - } - } + // Load all Applications by attribute and add them to the XML config + var types = PluginManager.Current.ResolveApplications(); - }, true); - - //TODO Shouldn't this be enabled and then delete the whole table? - //SqlHelper.ExecuteNonQuery("DELETE FROM umbracoApp"); + //since applications don't populate their metadata from the attribute and because it is an interface, + //we need to interrogate the attributes for the data. Would be better to have a base class that contains + //metadata populated by the attribute. Oh well i guess. + var attrs = types.Select(x => x.GetCustomAttributes(false).Single()); + return attrs.Select(x => new Application(x.Name, x.Alias, x.Icon, x.SortOrder)).ToArray(); + }); + } + + private readonly Lazy> _lazySections; + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return _lazySections.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(); + } } + + } } \ No newline at end of file diff --git a/src/umbraco.businesslogic/ApplicationTreeRegistrar.cs b/src/umbraco.businesslogic/ApplicationTreeRegistrar.cs index f9f83f3bbf..4ce1fa8a73 100644 --- a/src/umbraco.businesslogic/ApplicationTreeRegistrar.cs +++ b/src/umbraco.businesslogic/ApplicationTreeRegistrar.cs @@ -20,81 +20,6 @@ namespace umbraco.BusinessLogic ApplicationTree.Intitialize(new LazyEnumerableTrees()); } - //public ApplicationTreeRegistrar() - //{ - // //don't do anything if the application or database is not configured! - // if (ApplicationContext.Current == null - // || !ApplicationContext.Current.IsConfigured - // || !ApplicationContext.Current.DatabaseContext.IsDatabaseConfigured) - // return; - - // // Load all Trees by attribute and add them to the XML config - // var types = PluginManager.Current.ResolveAttributedTrees(); - - // var items = types - // .Select(x => - // new Tuple(x, x.GetCustomAttributes(false).Single())) - // .Where(x => ApplicationTree.getByAlias(x.Item2.Alias) == null); - - // var allAliases = ApplicationTree.getAll().Select(x => x.Alias).Concat(items.Select(x => x.Item2.Alias)); - // var inString = "'" + string.Join("','", allAliases) + "'"; - - // ApplicationTree.LoadXml(doc => - // { - // foreach (var tuple in items) - // { - // var type = tuple.Item1; - // var attr = tuple.Item2; - - // //Add the new tree that doesn't exist in the config that was found by type finding - - // doc.Root.Add(new XElement("add", - // new XAttribute("silent", attr.Silent), - // new XAttribute("initialize", attr.Initialize), - // new XAttribute("sortOrder", attr.SortOrder), - // new XAttribute("alias", attr.Alias), - // new XAttribute("application", attr.ApplicationAlias), - // new XAttribute("title", attr.Title), - // new XAttribute("iconClosed", attr.IconClosed), - // new XAttribute("iconOpen", attr.IconOpen), - // // don't add the assembly, we don't need this: - // // http://issues.umbraco.org/issue/U4-1360 - // //new XAttribute("assembly", assemblyName), - // //new XAttribute("type", typeName), - // // instead, store the assembly type name - // new XAttribute("type", type.GetFullNameWithAssembly()), - // new XAttribute("action", attr.Action))); - // } - - // //add any trees that were found in the database that don't exist in the config - - // var db = ApplicationContext.Current.DatabaseContext.Database; - // var exist = db.TableExist("umbracoAppTree"); - // if (exist) - // { - // var appTrees = db.Fetch("WHERE treeAlias NOT IN (" + inString + ")"); - // foreach (var appTree in appTrees) - // { - // var action = appTree.Action; - - // doc.Root.Add(new XElement("add", - // new XAttribute("silent", appTree.Silent), - // new XAttribute("initialize", appTree.Initialize), - // new XAttribute("sortOrder", appTree.SortOrder), - // new XAttribute("alias", appTree.Alias), - // new XAttribute("application", appTree.AppAlias), - // new XAttribute("title", appTree.Title), - // new XAttribute("iconClosed", appTree.IconClosed), - // new XAttribute("iconOpen", appTree.IconOpen), - // new XAttribute("assembly", appTree.HandlerAssembly), - // new XAttribute("type", appTree.HandlerType), - // new XAttribute("action", string.IsNullOrEmpty(action) ? "" : action))); - // } - // } - - // }, true); - //} - /// /// This class is here so that we can provide lazy access to tree scanning for when it is needed /// From 662ed03f0a711e23a0ab368ddf361039d5862222 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Tue, 23 Dec 2014 16:23:47 +0000 Subject: [PATCH 5/6] Fixed: U4-6051 HasAccces method tests the roles of the wrong user --- src/umbraco.cms/businesslogic/web/Access.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/umbraco.cms/businesslogic/web/Access.cs b/src/umbraco.cms/businesslogic/web/Access.cs index c7054dd923..65c055e0db 100644 --- a/src/umbraco.cms/businesslogic/web/Access.cs +++ b/src/umbraco.cms/businesslogic/web/Access.cs @@ -511,7 +511,7 @@ namespace umbraco.cms.businesslogic.web if (member != null) { - foreach (string role in Roles.GetRolesForUser()) + foreach (string role in Roles.GetRolesForUser(member.UserName)) { if (currentNode.SelectSingleNode("./group [@id='" + role + "']") != null) { From d4d77611ebadc9abea217fb37cf279444d192bcf Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Tue, 23 Dec 2014 16:27:51 +0000 Subject: [PATCH 6/6] Fixed: U4-5193 HasAccess override method name error --- src/umbraco.cms/businesslogic/web/Access.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/umbraco.cms/businesslogic/web/Access.cs b/src/umbraco.cms/businesslogic/web/Access.cs index 65c055e0db..26ef66483e 100644 --- a/src/umbraco.cms/businesslogic/web/Access.cs +++ b/src/umbraco.cms/businesslogic/web/Access.cs @@ -494,7 +494,14 @@ namespace umbraco.cms.businesslogic.web return hasAccess; } + [Obsolete("This method has been replaced because of a spelling mistake. Use the HasAccess method instead.", false)] public static bool HasAccces(int documentId, object memberId) + { + // Call the correctly named version of this method + return HasAccess(documentId, memberId); + } + + public static bool HasAccess(int documentId, object memberId) { bool hasAccess = false; var node = new CMSNode(documentId);