From ad8926668e1b470795246f27dede911cbe6d82f5 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 12 May 2015 12:39:46 +1000 Subject: [PATCH] Fixes: U4-6588 UmbracoSettings Error404 support Guids and Xpath - To achieve this, had to create an Id -> Key and Key -> Id method on EntityService including a cache for it which means updating all relavent cache refreshers to clear this cache when things are removed. Moved the logic for parsing an Umbraco XPath query (with tokens) to a stand-alone testable class (though haven't written tests), move the logic for looking up a not found page to the NotFoundHandlerHelper (instead of legacy 'library'). Fixes the $root query, since I don't think that ever worked. I've tested $root now for both MNTP and for the not found handler and it works. --- src/Umbraco.Core/Cache/CacheKeys.cs | 3 + .../InnerTextConfigurationElement.cs | 2 +- .../ContentErrorPageElement.cs | 41 ++- .../UmbracoSettings/IContentErrorPage.cs | 8 +- src/Umbraco.Core/Services/EntityService.cs | 82 +++++- src/Umbraco.Core/Services/IEntityService.cs | 15 ++ src/Umbraco.Core/Services/ServiceContext.cs | 3 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Xml/UmbracoXPathPathSyntaxParser.cs | 117 +++++++++ .../UmbracoSettings/ContentElementTests.cs | 10 +- .../UmbracoSettings/umbracoSettings.config | 4 +- .../Cache/ContentTypeCacheRefresher.cs | 7 +- .../Cache/DataTypeCacheRefresher.cs | 4 +- src/Umbraco.Web/Cache/MediaCacheRefresher.cs | 2 + src/Umbraco.Web/Cache/MemberCacheRefresher.cs | 2 + .../Cache/TemplateCacheRefresher.cs | 3 + src/Umbraco.Web/Editors/EntityController.cs | 80 +----- .../Routing/ContentFinderByLegacy404.cs | 22 +- .../Routing/NotFoundHandlerHelper.cs | 244 +++++++++++++----- .../umbraco.presentation/NotFoundHandlers.cs | 21 +- .../umbraco.presentation/library.cs | 47 +--- 21 files changed, 517 insertions(+), 201 deletions(-) create mode 100644 src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index c1f82b0d89..58e565d75e 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -54,5 +54,8 @@ namespace Umbraco.Core.Cache public const string DataTypeCacheKey = "UmbracoDataTypeDefinition"; public const string DataTypePreValuesCacheKey = "UmbracoPreVal"; + + public const string IdToKeyCacheKey = "UI2K"; + public const string KeyToIdCacheKey = "UK2I"; } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/InnerTextConfigurationElement.cs b/src/Umbraco.Core/Configuration/InnerTextConfigurationElement.cs index 966bbc6a11..fdbbc7889e 100644 --- a/src/Umbraco.Core/Configuration/InnerTextConfigurationElement.cs +++ b/src/Umbraco.Core/Configuration/InnerTextConfigurationElement.cs @@ -34,7 +34,7 @@ namespace Umbraco.Core.Configuration { get { - var converted = ObjectExtensions.TryConvertTo(RawValue); + var converted = RawValue.TryConvertTo(); if (converted.Success == false) throw new InvalidCastException("Could not convert value " + RawValue + " to type " + typeof(T)); return converted.Result; diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentErrorPageElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentErrorPageElement.cs index e0056ac958..eed15b9a0e 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentErrorPageElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentErrorPageElement.cs @@ -1,8 +1,9 @@ -using System.Xml.Linq; +using System; +using System.Xml.Linq; namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class ContentErrorPageElement : InnerTextConfigurationElement, IContentErrorPage + internal class ContentErrorPageElement : InnerTextConfigurationElement, IContentErrorPage { public ContentErrorPageElement(XElement rawXml) : base(rawXml) @@ -14,7 +15,43 @@ namespace Umbraco.Core.Configuration.UmbracoSettings } + public bool HasContentId + { + get { return ContentId != int.MinValue; } + } + + public bool HasContentKey + { + get { return ContentKey != Guid.Empty; } + } + public int ContentId + { + get + { + int parsed; + if (int.TryParse(Value, out parsed)) + { + return parsed; + } + return int.MinValue; + } + } + + public Guid ContentKey + { + get + { + Guid parsed; + if (Guid.TryParse(Value, out parsed)) + { + return parsed; + } + return Guid.Empty; + } + } + + public string ContentXPath { get { return Value; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentErrorPage.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentErrorPage.cs index be342b1fb6..5f10e542bb 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentErrorPage.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentErrorPage.cs @@ -1,8 +1,14 @@ -namespace Umbraco.Core.Configuration.UmbracoSettings +using System; + +namespace Umbraco.Core.Configuration.UmbracoSettings { public interface IContentErrorPage { int ContentId { get; } + Guid ContentKey { get; } + string ContentXPath { get; } + bool HasContentId { get; } + bool HasContentKey { get; } string Culture { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 0b3e4a924f..8d99845c60 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Umbraco.Core.Cache; using Umbraco.Core.CodeAnnotations; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -19,12 +20,13 @@ namespace Umbraco.Core.Services private readonly IMediaService _mediaService; private readonly IMemberService _memberService; private readonly IMemberTypeService _memberTypeService; + private readonly IRuntimeCacheProvider _runtimeCache; private readonly IDataTypeService _dataTypeService; private readonly Dictionary>> _supportedObjectTypes; public EntityService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, IContentService contentService, IContentTypeService contentTypeService, IMediaService mediaService, IDataTypeService dataTypeService, - IMemberService memberService, IMemberTypeService memberTypeService) + IMemberService memberService, IMemberTypeService memberTypeService, IRuntimeCacheProvider runtimeCache) { _uowProvider = provider; _repositoryFactory = repositoryFactory; @@ -34,6 +36,7 @@ namespace Umbraco.Core.Services _dataTypeService = dataTypeService; _memberService = memberService; _memberTypeService = memberTypeService; + _runtimeCache = runtimeCache; _supportedObjectTypes = new Dictionary>> { @@ -45,6 +48,83 @@ namespace Umbraco.Core.Services {typeof (IMember).FullName, new Tuple>(UmbracoObjectTypes.Member, _memberService.GetById)}, {typeof (IMemberType).FullName, new Tuple>(UmbracoObjectTypes.MemberType, _memberTypeService.Get)} }; + + } + + /// + /// Returns the integer id for a given GUID + /// + /// + /// + /// + public Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType) + { + var result = _runtimeCache.GetCacheItem(CacheKeys.IdToKeyCacheKey + key, () => + { + using (var uow = _uowProvider.GetUnitOfWork()) + { + switch (umbracoObjectType) + { + case UmbracoObjectTypes.Document: + case UmbracoObjectTypes.MemberType: + case UmbracoObjectTypes.Media: + case UmbracoObjectTypes.Template: + case UmbracoObjectTypes.MediaType: + case UmbracoObjectTypes.DocumentType: + case UmbracoObjectTypes.Member: + case UmbracoObjectTypes.DataType: + return uow.Database.ExecuteScalar(new Sql().Select("id").From().Where(dto => dto.UniqueId == key)); + case UmbracoObjectTypes.RecycleBin: + case UmbracoObjectTypes.Stylesheet: + case UmbracoObjectTypes.MemberGroup: + case UmbracoObjectTypes.ContentItem: + case UmbracoObjectTypes.ContentItemType: + case UmbracoObjectTypes.ROOT: + case UmbracoObjectTypes.Unknown: + default: + throw new NotSupportedException(); + } + } + }); + return result.HasValue ? Attempt.Succeed(result.Value) : Attempt.Fail(); + } + + /// + /// Returns the GUID for a given integer id + /// + /// + /// + /// + public Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType) + { + var result = _runtimeCache.GetCacheItem(CacheKeys.KeyToIdCacheKey + id, () => + { + using (var uow = _uowProvider.GetUnitOfWork()) + { + switch (umbracoObjectType) + { + case UmbracoObjectTypes.Document: + case UmbracoObjectTypes.MemberType: + case UmbracoObjectTypes.Media: + case UmbracoObjectTypes.Template: + case UmbracoObjectTypes.MediaType: + case UmbracoObjectTypes.DocumentType: + case UmbracoObjectTypes.Member: + case UmbracoObjectTypes.DataType: + return uow.Database.ExecuteScalar(new Sql().Select("uniqueID").From().Where(dto => dto.NodeId == id)); + case UmbracoObjectTypes.RecycleBin: + case UmbracoObjectTypes.Stylesheet: + case UmbracoObjectTypes.MemberGroup: + case UmbracoObjectTypes.ContentItem: + case UmbracoObjectTypes.ContentItemType: + case UmbracoObjectTypes.ROOT: + case UmbracoObjectTypes.Unknown: + default: + throw new NotSupportedException(); + } + } + }); + return result.HasValue ? Attempt.Succeed(result.Value) : Attempt.Fail(); } public IUmbracoEntity GetByKey(Guid key, bool loadBaseType = true) diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 24247197de..aafbb09933 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -7,6 +7,21 @@ namespace Umbraco.Core.Services { public interface IEntityService { + /// + /// Returns the integer id for a given GUID + /// + /// + /// + /// + Attempt GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType); + + /// + /// Returns the GUID for a given integer id + /// + /// + /// + /// + Attempt GetKeyForId(int id, UmbracoObjectTypes umbracoObjectType); /// /// Gets an UmbracoEntity by its Id, and optionally loads the complete object graph. diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index 6dabf8b0b6..e5359769e9 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -167,7 +167,8 @@ namespace Umbraco.Core.Services if (_entityService == null) _entityService = new Lazy(() => new EntityService( provider, repositoryFactory.Value, - _contentService.Value, _contentTypeService.Value, _mediaService.Value, _dataTypeService.Value, _memberService.Value, _memberTypeService.Value)); + _contentService.Value, _contentTypeService.Value, _mediaService.Value, _dataTypeService.Value, _memberService.Value, _memberTypeService.Value, + cache.RuntimeCache)); if (_relationService == null) _relationService = new Lazy(() => new RelationService(provider, repositoryFactory.Value, _entityService.Value)); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 070090d8c7..67ce55b8f1 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1217,6 +1217,7 @@ + diff --git a/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs b/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs new file mode 100644 index 0000000000..7802ddc438 --- /dev/null +++ b/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Xml +{ + /// + /// This is used to parse our customize Umbraco XPath expressions (i.e. that include special tokens like $site) into + /// a real XPath statement + /// + internal class UmbracoXPathPathSyntaxParser + { + /// + /// Parses custom umbraco xpath expression + /// + /// The Xpath expression + /// + /// The current node id context of executing the query - null if there is no current node, in which case + /// some of the parameters like $current, $parent, $site will be disabled + /// + /// The callback to create the nodeId path, given a node Id + /// The callback to return whether a published node exists based on Id + /// + public static string ParseXPathQuery( + string xpathExpression, + int? nodeContextId, + Func> getPath, + Func publishedContentExists) + { + + //TODO: This should probably support some of the old syntax and token replacements, currently + // it does not, there is a ticket raised here about it: http://issues.umbraco.org/issue/U4-6364 + // previous tokens were: "$currentPage", "$ancestorOrSelf", "$parentPage" and I beleive they were + // allowed 'inline', not just at the beginning... whether or not we want to support that is up + // for discussion. + + Mandate.ParameterNotNullOrEmpty(xpathExpression, "xpathExpression"); + Mandate.ParameterNotNull(getPath, "getPath"); + Mandate.ParameterNotNull(publishedContentExists, "publishedContentExists"); + + //no need to parse it + if (xpathExpression.StartsWith("$") == false) + return xpathExpression; + + //get nearest published item + Func, int> getClosestPublishedAncestor = (path => + { + foreach (var i in path) + { + int idAsInt; + if (int.TryParse(i, out idAsInt)) + { + var exists = publishedContentExists(int.Parse(i)); + if (exists) + return idAsInt; + } + } + return -1; + }); + + const string rootXpath = "descendant::*[@id={0}]"; + + //parseable items: + var vars = new Dictionary>(); + + //These parameters must have a node id context + if (nodeContextId.HasValue) + { + vars.Add("$current", q => + { + var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); + return q.Replace("$current", string.Format(rootXpath, closestPublishedAncestorId)); + }); + + vars.Add("$parent", q => + { + //remove the first item in the array if its the current node + //this happens when current is published, but we are looking for its parent specifically + var path = getPath(nodeContextId.Value).ToArray(); + if (path[0] == nodeContextId.ToString()) + { + path = path.Skip(1).ToArray(); + } + + var closestPublishedAncestorId = getClosestPublishedAncestor(path); + return q.Replace("$parent", string.Format(rootXpath, closestPublishedAncestorId)); + }); + + + vars.Add("$site", q => + { + var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value)); + return q.Replace("$site", string.Format(rootXpath, closestPublishedAncestorId) + "/ancestor-or-self::*[@level = 1]"); + }); + } + + //TODO: This used to just replace $root with string.Empty BUT, that would never work + // the root is always "/root . Need to confirm with Per why this was string.Empty before! + vars.Add("$root", q => q.Replace("$root", "/root")); + + + foreach (var varible in vars) + { + if (xpathExpression.StartsWith(varible.Key)) + { + xpathExpression = varible.Value(xpathExpression); + break; + } + } + + return xpathExpression; + } + + } +} diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs index 668845b7b7..06dab42556 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs @@ -28,10 +28,16 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings Assert.IsTrue(SettingsSection.Content.Error404Collection.Count() == 3); Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(0).Culture == "default"); Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(0).ContentId == 1047); + Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(0).HasContentId); + Assert.IsFalse(SettingsSection.Content.Error404Collection.ElementAt(0).HasContentKey); Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(1).Culture == "en-US"); - Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(1).ContentId == 1048); + Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(1).ContentXPath == "$site/error [@name = 'error']"); + Assert.IsFalse(SettingsSection.Content.Error404Collection.ElementAt(1).HasContentId); + Assert.IsFalse(SettingsSection.Content.Error404Collection.ElementAt(1).HasContentKey); Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(2).Culture == "en-UK"); - Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(2).ContentId == 1049); + Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(2).ContentKey == new Guid("8560867F-B88F-4C74-A9A4-679D8E5B3BFC")); + Assert.IsTrue(SettingsSection.Content.Error404Collection.ElementAt(2).HasContentKey); + Assert.IsFalse(SettingsSection.Content.Error404Collection.ElementAt(2).HasContentId); } [Test] diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config index e2ab4e0a69..bff1cf9406 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config @@ -42,8 +42,8 @@ --> 1047 - 1048 - 1049 + $site/error [@name = 'error'] + 8560867F-B88F-4C74-A9A4-679D8E5B3BFC diff --git a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs index 3db1e30fa7..10a0efbab7 100644 --- a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs @@ -183,7 +183,10 @@ namespace Umbraco.Web.Cache private static void ClearContentTypeCache(JsonPayload[] payloads) { var needsContentRefresh = false; - + + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); + payloads.ForEach(payload => { //clear the cache for each item @@ -271,7 +274,7 @@ namespace Umbraco.Web.Cache ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(string.Format("{0}{1}", CacheKeys.ContentTypeCacheKey, payload.Id)); //clears the cache associated with the content type properties collection ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(CacheKeys.ContentTypePropertiesCacheKey + payload.Id); - + //clears the dictionary object cache of the legacy ContentType global::umbraco.cms.businesslogic.ContentType.RemoveFromDataTypeCache(payload.Alias); diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index f91c08b455..43530d4cc8 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -120,11 +120,13 @@ namespace Umbraco.Web.Cache // db data type to store the value against and anytime a datatype changes, this also might change // we basically need to clear all sorts of runtime caches here because so many things depend upon a data type RuntimeCacheProvider.Current.Clear(typeof(IContent)); - RuntimeCacheProvider.Current.Clear(typeof (IContentType)); + RuntimeCacheProvider.Current.Clear(typeof(IContentType)); RuntimeCacheProvider.Current.Clear(typeof(IMedia)); RuntimeCacheProvider.Current.Clear(typeof(IMediaType)); RuntimeCacheProvider.Current.Clear(typeof(IMember)); RuntimeCacheProvider.Current.Clear(typeof(IMemberType)); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); payloads.ForEach(payload => { diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs index bd12261b44..f586ce76fe 100644 --- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs @@ -152,6 +152,8 @@ namespace Umbraco.Web.Cache { if (payloads == null) return; + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); payloads.ForEach(payload => diff --git a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs index 97afd092a7..250922c04a 100644 --- a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs @@ -58,6 +58,8 @@ namespace Umbraco.Web.Cache private void ClearCache(int id) { + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); ApplicationContext.Current.ApplicationCache. diff --git a/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs b/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs index 42026a7523..a8999c61ae 100644 --- a/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs @@ -52,6 +52,9 @@ namespace Umbraco.Web.Cache private void RemoveFromCache(int id) { + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); + ApplicationContext.Current.ApplicationCache.ClearCacheItem( string.Format("{0}{1}", CacheKeys.TemplateFrontEndCacheKey, id)); diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 4c1b8dd849..d05ed9414d 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -28,6 +28,7 @@ using Examine.SearchCriteria; using Umbraco.Web.Dynamics; using umbraco; using System.Text.RegularExpressions; +using Umbraco.Core.Xml; namespace Umbraco.Web.Editors { @@ -146,6 +147,8 @@ namespace Umbraco.Web.Editors /// public EntityBasic GetByQuery(string query, int nodeContextId, UmbracoEntityTypes type) { + //TODO: Rename this!!! It's a bit misleading, it should be GetByXPath + if (type != UmbracoEntityTypes.Document) throw new ArgumentException("Get by query is only compatible with enitities of type Document"); @@ -163,76 +166,15 @@ namespace Umbraco.Web.Editors //PP: wip in progress on the query parser private string ParseXPathQuery(string query, int id) { - //no need to parse it - if (!query.StartsWith("$")) - return query; - - //get full path - Func> getPath = delegate(int nodeid){ - var ent = Services.EntityService.Get(nodeid); - return ent.Path.Split(',').Reverse(); - }; - - //get nearest published item - Func, int> getClosestPublishedAncestor = (path => - { - - foreach (var _id in path) + return UmbracoXPathPathSyntaxParser.ParseXPathQuery( + xpathExpression: query, + nodeContextId: id, + getPath: nodeid => { - var item = Umbraco.TypedContent(int.Parse(_id)); - - if (item != null) - return item.Id; - } - return -1; - }); - - var rootXpath = "descendant::*[@id={0}]"; - - //parseable items: - var vars = new Dictionary>(); - vars.Add("$current", q => { - var _id = getClosestPublishedAncestor(getPath(id)); - return q.Replace("$current", string.Format(rootXpath, _id)); - }); - - vars.Add("$parent", q => - { - //remove the first item in the array if its the current node - //this happens when current is published, but we are looking for its parent specifically - var path = getPath(id); - if(path.ElementAt(0) == id.ToString()){ - path = path.Skip(1); - } - - var _id = getClosestPublishedAncestor(path); - return q.Replace("$parent", string.Format(rootXpath, _id)); - }); - - - vars.Add("$site", q => - { - var _id = getClosestPublishedAncestor(getPath(id)); - return q.Replace("$site", string.Format(rootXpath, _id) + "/ancestor-or-self::*[@level = 1]"); - }); - - - vars.Add("$root", q => - { - return q.Replace("$root", string.Empty); - }); - - - foreach (var varible in vars) - { - if (query.StartsWith(varible.Key)) - { - query = varible.Value.Invoke(query); - break; - } - } - - return query; + var ent = Services.EntityService.Get(nodeid); + return ent.Path.Split(',').Reverse(); + }, + publishedContentExists: i => Umbraco.TypedContent(i) != null); } public EntityBasic GetById(int id, UmbracoEntityTypes type) diff --git a/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs b/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs index 1181ac3195..5acbbc5c38 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs @@ -1,4 +1,7 @@ -using Umbraco.Core.Logging; +using System.Linq; +using System.Web; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; using Umbraco.Core.Models; namespace Umbraco.Web.Routing @@ -17,17 +20,22 @@ namespace Umbraco.Web.Routing { LogHelper.Debug("Looking for a page to handle 404."); - // TODO - replace the whole logic and stop calling into library! - var error404 = global::umbraco.library.GetCurrentNotFoundPageId(); - var id = int.Parse(error404); + // TODO - replace the whole logic + var error404 = NotFoundHandlerHelper.GetCurrentNotFoundPageId( + //TODO: The IContentSection should be ctor injected into this class in v8! + UmbracoConfig.For.UmbracoSettings().Content.Error404Collection.ToArray(), + //TODO: Is there a better way to extract this value? at least we're not relying on singletons here though + pcr.RoutingContext.UmbracoContext.HttpContext.Request.ServerVariables["SERVER_NAME"], + pcr.RoutingContext.UmbracoContext.Application.Services.EntityService, + new PublishedContentQuery(pcr.RoutingContext.UmbracoContext.ContentCache, pcr.RoutingContext.UmbracoContext.MediaCache)); IPublishedContent content = null; - if (id > 0) + if (error404.HasValue) { - LogHelper.Debug("Got id={0}.", () => id); + LogHelper.Debug("Got id={0}.", () => error404.Value); - content = pcr.RoutingContext.UmbracoContext.ContentCache.GetById(id); + content = pcr.RoutingContext.UmbracoContext.ContentCache.GetById(error404.Value); LogHelper.Debug(content == null ? "Could not find content with that id." diff --git a/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs b/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs index c7af4003aa..a2af85943e 100644 --- a/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs +++ b/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs @@ -4,84 +4,90 @@ using System.Linq; using System.Web; using System.Xml; using System.Reflection; +using umbraco.cms.businesslogic.web; using Umbraco.Core; using Umbraco.Core.Logging; using umbraco.interfaces; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Xml; namespace Umbraco.Web.Routing { - // provides internal access to legacy url -- should get rid of it eventually - internal class NotFoundHandlerHelper - { - const string ContextKey = "Umbraco.Web.Routing.NotFoundHandlerHelper.Url"; + // provides internal access to legacy url -- should get rid of it eventually + internal class NotFoundHandlerHelper + { + const string ContextKey = "Umbraco.Web.Routing.NotFoundHandlerHelper.Url"; static NotFoundHandlerHelper() { InitializeNotFoundHandlers(); } - public static string GetLegacyUrlForNotFoundHandlers() - { - // that's not backward-compatible because when requesting "/foo.aspx" - // 4.9 : url = "foo.aspx" - // 4.10 : url = "/foo" - //return pcr.Uri.AbsolutePath; + public static string GetLegacyUrlForNotFoundHandlers() + { + // that's not backward-compatible because when requesting "/foo.aspx" + // 4.9 : url = "foo.aspx" + // 4.10 : url = "/foo" + //return pcr.Uri.AbsolutePath; - // so we have to run the legacy code for url preparation :-( + // so we have to run the legacy code for url preparation :-( - var httpContext = HttpContext.Current; + var httpContext = HttpContext.Current; - if (httpContext == null) - return ""; + if (httpContext == null) + return ""; - var url = httpContext.Items[ContextKey] as string; - if (url != null) - return url; + var url = httpContext.Items[ContextKey] as string; + if (url != null) + return url; - // code from requestModule.UmbracoRewrite - var tmp = httpContext.Request.Path.ToLower(); + // code from requestModule.UmbracoRewrite + var tmp = httpContext.Request.Path.ToLower(); - // note: requestModule.UmbracoRewrite also did some stripping of &umbPage - // from the querystring... that was in v3.x to fix some issues with pre-forms - // auth. Paul Sterling confirmed in jan. 2013 that we can get rid of it. + // note: requestModule.UmbracoRewrite also did some stripping of &umbPage + // from the querystring... that was in v3.x to fix some issues with pre-forms + // auth. Paul Sterling confirmed in jan. 2013 that we can get rid of it. - // code from requestHandler.cleanUrl - var root = Core.IO.SystemDirectories.Root.ToLower(); - if (!string.IsNullOrEmpty(root) && tmp.StartsWith(root)) - tmp = tmp.Substring(root.Length); - tmp = tmp.TrimEnd('/'); - if (tmp == "/default.aspx") - tmp = string.Empty; - else if (tmp == root) - tmp = string.Empty; + // code from requestHandler.cleanUrl + var root = Core.IO.SystemDirectories.Root.ToLower(); + if (!string.IsNullOrEmpty(root) && tmp.StartsWith(root)) + tmp = tmp.Substring(root.Length); + tmp = tmp.TrimEnd('/'); + if (tmp == "/default.aspx") + tmp = string.Empty; + else if (tmp == root) + tmp = string.Empty; - // code from UmbracoDefault.Page_PreInit - if (tmp != "" && httpContext.Request["umbPageID"] == null) - { - var tryIntParse = tmp.Replace("/", "").Replace(".aspx", string.Empty); - int result; - if (int.TryParse(tryIntParse, out result)) - tmp = tmp.Replace(".aspx", string.Empty); - } - else if (!string.IsNullOrEmpty(httpContext.Request["umbPageID"])) - { - int result; - if (int.TryParse(httpContext.Request["umbPageID"], out result)) - { - tmp = httpContext.Request["umbPageID"]; - } - } + // code from UmbracoDefault.Page_PreInit + if (tmp != "" && httpContext.Request["umbPageID"] == null) + { + var tryIntParse = tmp.Replace("/", "").Replace(".aspx", string.Empty); + int result; + if (int.TryParse(tryIntParse, out result)) + tmp = tmp.Replace(".aspx", string.Empty); + } + else if (!string.IsNullOrEmpty(httpContext.Request["umbPageID"])) + { + int result; + if (int.TryParse(httpContext.Request["umbPageID"], out result)) + { + tmp = httpContext.Request["umbPageID"]; + } + } - // code from requestHandler.ctor - if (tmp != "") - tmp = tmp.Substring(1); + // code from requestHandler.ctor + if (tmp != "") + tmp = tmp.Substring(1); - httpContext.Items[ContextKey] = tmp; - return tmp; - } + httpContext.Items[ContextKey] = tmp; + return tmp; + } private static IEnumerable _customHandlerTypes; - private static Type _customLastChanceHandlerType; + private static Type _customLastChanceHandlerType; static void InitializeNotFoundHandlers() { @@ -175,14 +181,14 @@ namespace Umbraco.Web.Routing return handlers; } - public static bool IsNotFoundHandlerEnabled() - { - return _customHandlerTypes.Contains(typeof (T)); - } + public static bool IsNotFoundHandlerEnabled() + { + return _customHandlerTypes.Contains(typeof(T)); + } - public static INotFoundHandler GetNotFoundLastChanceHandler() - { - if (_customLastChanceHandlerType == null) return null; + public static INotFoundHandler GetNotFoundLastChanceHandler() + { + if (_customLastChanceHandlerType == null) return null; try { @@ -214,5 +220,121 @@ namespace Umbraco.Web.Routing return finder; } + /// + /// Returns the Umbraco page id to use as the Not Found page based on the configured 404 pages and the current request + /// + /// + /// + /// The server name attached to the request, normally would be the source of HttpContext.Current.Request.ServerVariables["SERVER_NAME"] + /// + /// + /// + /// + internal static int? GetCurrentNotFoundPageId( + IContentErrorPage[] error404Collection, + string requestServerName, + IEntityService entityService, + PublishedContentQuery publishedContentQuery) + { + if (error404Collection.Count() > 1) + { + // try to get the 404 based on current culture (via domain) + IContentErrorPage cultureErr; + + //TODO: Remove the dependency on this legacy Domain service, + // in 7.3 the real domain service should be passed in as a parameter. + if (Domain.Exists(requestServerName)) + { + var d = Domain.GetDomain(requestServerName); + + // test if a 404 page exists with current culture + cultureErr = error404Collection + .FirstOrDefault(x => x.Culture == d.Language.CultureAlias); + + if (cultureErr != null) + { + return GetContentIdFromErrorPageConfig(cultureErr, entityService, publishedContentQuery); + } + } + + // test if a 404 page exists with current culture thread + cultureErr = error404Collection + .FirstOrDefault(x => x.Culture == System.Threading.Thread.CurrentThread.CurrentUICulture.Name); + if (cultureErr != null) + { + return GetContentIdFromErrorPageConfig(cultureErr, entityService, publishedContentQuery); + } + + // there should be a default one! + cultureErr = error404Collection + .FirstOrDefault(x => x.Culture == "default"); + + if (cultureErr != null) + { + return GetContentIdFromErrorPageConfig(cultureErr, entityService, publishedContentQuery); + } + } + else + { + return GetContentIdFromErrorPageConfig(error404Collection.First(), entityService, publishedContentQuery); + } + + return null; + } + + /// + /// Returns the content id based on the configured IContentErrorPage section + /// + /// + /// + /// + /// + internal static int? GetContentIdFromErrorPageConfig(IContentErrorPage errorPage, IEntityService entityService, PublishedContentQuery publishedContentQuery) + { + if (errorPage.HasContentId) return errorPage.ContentId; + + if (errorPage.HasContentKey) + { + //need to get the Id for the GUID + //TODO: When we start storing GUIDs into the IPublishedContent, then we won't have to look this up + // but until then we need to look it up in the db. For now we've implemented a cached service for + // converting Int -> Guid and vice versa. + var found = entityService.GetIdForKey(errorPage.ContentKey, UmbracoObjectTypes.Document); + if (found) + { + return found.Result; + } + return null; + } + + if (errorPage.ContentXPath.IsNullOrWhiteSpace() == false) + { + try + { + //we have an xpath statement to execute + var xpathResult = UmbracoXPathPathSyntaxParser.ParseXPathQuery( + xpathExpression: errorPage.ContentXPath, + nodeContextId: null, + getPath: nodeid => + { + var ent = entityService.Get(nodeid); + return ent.Path.Split(',').Reverse(); + }, + publishedContentExists: i => publishedContentQuery.TypedContent(i) != null); + + //now we'll try to execute the expression + var nodeResult = publishedContentQuery.TypedContentSingleAtXPath(xpathResult); + if (nodeResult != null) + return nodeResult.Id; + } + catch (Exception ex) + { + LogHelper.Error("Could not parse xpath expression: " + errorPage.ContentXPath, ex); + return null; + } + } + return null; + } + } } diff --git a/src/Umbraco.Web/umbraco.presentation/NotFoundHandlers.cs b/src/Umbraco.Web/umbraco.presentation/NotFoundHandlers.cs index 56021687bd..43aeefd970 100644 --- a/src/Umbraco.Web/umbraco.presentation/NotFoundHandlers.cs +++ b/src/Umbraco.Web/umbraco.presentation/NotFoundHandlers.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Diagnostics; +using System.Linq; using System.Reflection; using System.Web; using System.Xml; @@ -14,6 +15,8 @@ using umbraco.interfaces; using Umbraco.Core.IO; using umbraco.NodeFactory; using Umbraco.Core; +using Umbraco.Web; +using Umbraco.Web.Routing; namespace umbraco { @@ -317,13 +320,21 @@ namespace umbraco { { LogHelper.Info(string.Format("NotFound url {0} (from '{1}')", url, HttpContext.Current.Request.UrlReferrer)); - // Test if the error404 not child elements - string error404 = umbraco.library.GetCurrentNotFoundPageId(); + // Test if the error404 not child elements + var error404 = NotFoundHandlerHelper.GetCurrentNotFoundPageId( + UmbracoConfig.For.UmbracoSettings().Content.Error404Collection.ToArray(), + HttpContext.Current.Request.ServerVariables["SERVER_NAME"], + ApplicationContext.Current.Services.EntityService, + new PublishedContentQuery(UmbracoContext.Current.ContentCache, UmbracoContext.Current.MediaCache)); + if (error404.HasValue) + { + _redirectID = error404.Value; + HttpContext.Current.Response.StatusCode = 404; + return true; + } - _redirectID = int.Parse(error404); - HttpContext.Current.Response.StatusCode = 404; - return true; + return false; } catch (Exception err) { diff --git a/src/Umbraco.Web/umbraco.presentation/library.cs b/src/Umbraco.Web/umbraco.presentation/library.cs index 3a557b3873..b9fac739f2 100644 --- a/src/Umbraco.Web/umbraco.presentation/library.cs +++ b/src/Umbraco.Web/umbraco.presentation/library.cs @@ -1909,52 +1909,7 @@ namespace umbraco return cms.helpers.xhtml.TidyHtml(StringToTidy); } - internal static string GetCurrentNotFoundPageId() - { - //XmlNode error404Node = UmbracoSettings.GetKeyAsNode("/settings/content/errors/error404"); - if (UmbracoConfig.For.UmbracoSettings().Content.Error404Collection.Count() > 1) - { - // try to get the 404 based on current culture (via domain) - IContentErrorPage cultureErr; - if (Domain.Exists(HttpContext.Current.Request.ServerVariables["SERVER_NAME"])) - { - var d = Domain.GetDomain(HttpContext.Current.Request.ServerVariables["SERVER_NAME"]); - - // test if a 404 page exists with current culture - cultureErr = UmbracoConfig.For.UmbracoSettings().Content.Error404Collection - .FirstOrDefault(x => x.Culture == d.Language.CultureAlias); - - if (cultureErr != null) - { - return cultureErr.ContentId.ToInvariantString(); - } - - } - - // test if a 404 page exists with current culture thread - cultureErr = UmbracoConfig.For.UmbracoSettings().Content.Error404Collection - .FirstOrDefault(x => x.Culture == System.Threading.Thread.CurrentThread.CurrentUICulture.Name); - if (cultureErr != null) - { - return cultureErr.ContentId.ToInvariantString(); - } - - // there should be a default one! - cultureErr = UmbracoConfig.For.UmbracoSettings().Content.Error404Collection - .FirstOrDefault(x => x.Culture == "default"); - if (cultureErr != null) - { - return cultureErr.ContentId.ToInvariantString(); - } - } - else - { - - return UmbracoConfig.For.UmbracoSettings().Content.Error404Collection.First().ContentId.ToInvariantString(); - } - - return ""; - } + #endregion