diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 410baede55..2536316906 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -33,9 +33,8 @@ - - - + + diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index d743daca6a..aed7f61a06 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -4,8 +4,7 @@ using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { - - internal class ContentElement : ConfigurationElement, IContentSection + internal class ContentElement : UmbracoConfigurationElement, IContentSection { [ConfigurationProperty("imaging")] internal ContentImagingElement Imaging @@ -22,25 +21,13 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("ResolveUrlsFromTextString")] internal InnerTextConfigurationElement ResolveUrlsFromTextString { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["ResolveUrlsFromTextString"], - //set the default - false); - } + get { return GetOptionalTextElement("ResolveUrlsFromTextString", false); } } [ConfigurationProperty("UploadAllowDirectories")] internal InnerTextConfigurationElement UploadAllowDirectories { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["UploadAllowDirectories"], - //set the default - true); - } + get { return GetOptionalTextElement("UploadAllowDirectories", true); } } public IEnumerable Error404Collection @@ -63,121 +50,61 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("ensureUniqueNaming")] internal InnerTextConfigurationElement EnsureUniqueNaming { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["ensureUniqueNaming"], - //set the default - true); - } + get { return GetOptionalTextElement("ensureUniqueNaming", true); } } [ConfigurationProperty("TidyEditorContent")] internal InnerTextConfigurationElement TidyEditorContent { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["TidyEditorContent"], - //set the default - false); - } + get { return GetOptionalTextElement("TidyEditorContent", false); } } [ConfigurationProperty("TidyCharEncoding")] internal InnerTextConfigurationElement TidyCharEncoding { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["TidyCharEncoding"], - //set the default - "UTF8"); - } + get { return GetOptionalTextElement("TidyCharEncoding", "UTF8"); } } [ConfigurationProperty("XmlCacheEnabled")] internal InnerTextConfigurationElement XmlCacheEnabled { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["XmlCacheEnabled"], - //set the default - true); - } + get { return GetOptionalTextElement("XmlCacheEnabled", true); } } [ConfigurationProperty("ContinouslyUpdateXmlDiskCache")] internal InnerTextConfigurationElement ContinouslyUpdateXmlDiskCache { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["ContinouslyUpdateXmlDiskCache"], - //set the default - true); - } + get { return GetOptionalTextElement("ContinouslyUpdateXmlDiskCache", true); } } [ConfigurationProperty("XmlContentCheckForDiskChanges")] internal InnerTextConfigurationElement XmlContentCheckForDiskChanges { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["XmlContentCheckForDiskChanges"], - //set the default - false); - } + get { return GetOptionalTextElement("XmlContentCheckForDiskChanges", false); } } [ConfigurationProperty("EnableSplashWhileLoading")] internal InnerTextConfigurationElement EnableSplashWhileLoading { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["EnableSplashWhileLoading"], - //set the default - false); - } + get { return GetOptionalTextElement("EnableSplashWhileLoading", false); } } [ConfigurationProperty("PropertyContextHelpOption")] internal InnerTextConfigurationElement PropertyContextHelpOption { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["PropertyContextHelpOption"], - //set the default - "text"); - } + get { return GetOptionalTextElement("PropertyContextHelpOption", "text"); } } [ConfigurationProperty("UseLegacyXmlSchema")] internal InnerTextConfigurationElement UseLegacyXmlSchema { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["UseLegacyXmlSchema"], - //set the default - false); - } + get { return GetOptionalTextElement("UseLegacyXmlSchema", false); } } [ConfigurationProperty("ForceSafeAliases")] internal InnerTextConfigurationElement ForceSafeAliases { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["ForceSafeAliases"], - //set the default - true); - } + get { return GetOptionalTextElement("ForceSafeAliases", true); } } [ConfigurationProperty("PreviewBadge")] @@ -185,123 +112,63 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { get { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["PreviewBadge"], - //set the default - @"In Preview Mode - click to end"); + return GetOptionalTextElement("PreviewBadge", @"In Preview Mode - click to end"); } } [ConfigurationProperty("UmbracoLibraryCacheDuration")] internal InnerTextConfigurationElement UmbracoLibraryCacheDuration { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["UmbracoLibraryCacheDuration"], - //set the default - 1800); - - } + get { return GetOptionalTextElement("UmbracoLibraryCacheDuration", 1800); } } [ConfigurationProperty("MacroErrors")] internal InnerTextConfigurationElement MacroErrors { - get - { - - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["MacroErrors"], - //set the default - MacroErrorBehaviour.Inline); - } + get { return GetOptionalTextElement("MacroErrors", MacroErrorBehaviour.Inline); } } [Obsolete("This is here so that if this config element exists we won't get a YSOD, it is not used whatsoever and will be removed in future versions")] [ConfigurationProperty("DocumentTypeIconList")] internal InnerTextConfigurationElement DocumentTypeIconList { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["DocumentTypeIconList"], - //set the default - IconPickerBehaviour.HideFileDuplicates); - } + get { return GetOptionalTextElement("DocumentTypeIconList", IconPickerBehaviour.HideFileDuplicates); } } [ConfigurationProperty("disallowedUploadFiles")] internal CommaDelimitedConfigurationElement DisallowedUploadFiles { - get - { - return new OptionalCommaDelimitedConfigurationElement( - (CommaDelimitedConfigurationElement)this["disallowedUploadFiles"], - //set the default - new[] { "ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd" }); - - } + get { return GetOptionalDelimitedElement("disallowedUploadFiles", new[] {"ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd"}); } } [ConfigurationProperty("cloneXmlContent")] internal InnerTextConfigurationElement CloneXmlContent { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["cloneXmlContent"], - //set the default - true); - } + get { return GetOptionalTextElement("cloneXmlContent", true); } } [ConfigurationProperty("GlobalPreviewStorageEnabled")] internal InnerTextConfigurationElement GlobalPreviewStorageEnabled { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["GlobalPreviewStorageEnabled"], - //set the default - false); - } + get { return GetOptionalTextElement("GlobalPreviewStorageEnabled", false); } } [ConfigurationProperty("defaultDocumentTypeProperty")] internal InnerTextConfigurationElement DefaultDocumentTypeProperty { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["defaultDocumentTypeProperty"], - //set the default - "Textstring"); - } + get { return GetOptionalTextElement("defaultDocumentTypeProperty", "Textstring"); } } [ConfigurationProperty("EnableInheritedDocumentTypes")] internal InnerTextConfigurationElement EnableInheritedDocumentTypes { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement) this["EnableInheritedDocumentTypes"], - //set the default - true); - } + get { return GetOptionalTextElement("EnableInheritedDocumentTypes", true); } } [ConfigurationProperty("EnableInheritedMediaTypes")] internal InnerTextConfigurationElement EnableInheritedMediaTypes { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["EnableInheritedMediaTypes"], - //set the default - true); - } + get { return GetOptionalTextElement("EnableInheritedMediaTypes", true); } } string IContentSection.NotificationEmailAddress diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentScriptEditorElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentScriptEditorElement.cs index 0d14609caa..f60e964a04 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentScriptEditorElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentScriptEditorElement.cs @@ -3,42 +3,24 @@ using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class ContentScriptEditorElement : ConfigurationElement + internal class ContentScriptEditorElement : UmbracoConfigurationElement { [ConfigurationProperty("scriptFolderPath")] internal InnerTextConfigurationElement ScriptFolderPath { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["scriptFolderPath"], - //set the default - "/scripts"); - } + get { return GetOptionalTextElement("scriptFolderPath", "/scripts"); } } [ConfigurationProperty("scriptFileTypes")] internal OptionalCommaDelimitedConfigurationElement ScriptFileTypes { - get - { - return new OptionalCommaDelimitedConfigurationElement( - (OptionalCommaDelimitedConfigurationElement)this["scriptFileTypes"], - //set the default - new[] { "js", "xml" }); - } + get { return GetOptionalDelimitedElement("scriptFileTypes", new[] {"js", "xml"}); } } [ConfigurationProperty("scriptDisableEditor")] internal InnerTextConfigurationElement ScriptEditorDisable { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement) this["scriptDisableEditor"], - //set the default - false); - } + get { return GetOptionalTextElement("scriptDisableEditor", false); } } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ImagingAutoFillUploadFieldElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ImagingAutoFillUploadFieldElement.cs index 0dfc4afc00..eafe43817d 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ImagingAutoFillUploadFieldElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ImagingAutoFillUploadFieldElement.cs @@ -2,7 +2,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class ImagingAutoFillUploadFieldElement : ConfigurationElement, IImagingAutoFillUploadField + internal class ImagingAutoFillUploadFieldElement : UmbracoConfigurationElement, IImagingAutoFillUploadField { /// /// Allow setting internally so we can create a default @@ -17,49 +17,25 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("widthFieldAlias")] internal InnerTextConfigurationElement WidthFieldAlias { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["widthFieldAlias"], - //set the default - "umbracoWidth"); - } + get { return GetOptionalTextElement("widthFieldAlias", "umbracoWidth"); } } [ConfigurationProperty("heightFieldAlias")] internal InnerTextConfigurationElement HeightFieldAlias { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["heightFieldAlias"], - //set the default - "umbracoHeight"); - } + get { return GetOptionalTextElement("heightFieldAlias", "umbracoHeight"); } } [ConfigurationProperty("lengthFieldAlias")] internal InnerTextConfigurationElement LengthFieldAlias { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["lengthFieldAlias"], - //set the default - "umbracoBytes"); - } + get { return GetOptionalTextElement("lengthFieldAlias", "umbracoBytes"); } } [ConfigurationProperty("extensionFieldAlias")] internal InnerTextConfigurationElement ExtensionFieldAlias { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["extensionFieldAlias"], - //set the default - "umbracoExtension"); - } + get { return GetOptionalTextElement("extensionFieldAlias", "umbracoExtension"); } } string IImagingAutoFillUploadField.Alias diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/LoggingElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/LoggingElement.cs index f95a9c7e76..39e6327b3a 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/LoggingElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/LoggingElement.cs @@ -3,67 +3,37 @@ using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class LoggingElement : ConfigurationElement, ILoggingSection + internal class LoggingElement : UmbracoConfigurationElement, ILoggingSection { [ConfigurationProperty("autoCleanLogs")] internal InnerTextConfigurationElement AutoCleanLogs { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["autoCleanLogs"], - //set the default - false); - } + get { return GetOptionalTextElement("autoCleanLogs", false); } } [ConfigurationProperty("enableLogging")] internal InnerTextConfigurationElement EnableLogging { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["enableLogging"], - //set the default - true); - } + get { return GetOptionalTextElement("enableLogging", true); } } [ConfigurationProperty("enableAsyncLogging")] internal InnerTextConfigurationElement EnableAsyncLogging { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["enableAsyncLogging"], - //set the default - true); - } + get { return GetOptionalTextElement("enableAsyncLogging", true); } } [ConfigurationProperty("cleaningMiliseconds")] internal InnerTextConfigurationElement CleaningMiliseconds { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["cleaningMiliseconds"], - //set the default - -1); - } + get { return GetOptionalTextElement("cleaningMiliseconds", -1); } } [ConfigurationProperty("maxLogAge")] internal InnerTextConfigurationElement MaxLogAge { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["maxLogAge"], - //set the default - -1); - } + get { return GetOptionalTextElement("maxLogAge", -1); } } [ConfigurationCollection(typeof(DisabledLogTypesCollection), AddItemName = "logTypeAlias")] diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/NotificationsElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/NotificationsElement.cs index 16eb943887..89e3f447ee 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/NotificationsElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/NotificationsElement.cs @@ -2,7 +2,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class NotificationsElement : ConfigurationElement + internal class NotificationsElement : UmbracoConfigurationElement { [ConfigurationProperty("email")] internal InnerTextConfigurationElement NotificationEmailAddress @@ -13,13 +13,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("disableHtmlEmail")] internal InnerTextConfigurationElement DisableHtmlEmail { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement) this["disableHtmlEmail"], - //set the default - false); - } + get { return GetOptionalTextElement("disableHtmlEmail", false); } } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/RequestHandlerElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/RequestHandlerElement.cs index 9dc4a94824..779d33c8b8 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/RequestHandlerElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/RequestHandlerElement.cs @@ -5,30 +5,18 @@ using System.Collections.Generic; namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class RequestHandlerElement : ConfigurationElement, IRequestHandlerSection + internal class RequestHandlerElement : UmbracoConfigurationElement, IRequestHandlerSection { [ConfigurationProperty("useDomainPrefixes")] public InnerTextConfigurationElement UseDomainPrefixes { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["useDomainPrefixes"], - //set the default - false); - } + get { return GetOptionalTextElement("useDomainPrefixes", false); } } [ConfigurationProperty("addTrailingSlash")] public InnerTextConfigurationElement AddTrailingSlash { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["addTrailingSlash"], - //set the default - true); - } + get { return GetOptionalTextElement("addTrailingSlash", true); } } private UrlReplacingElement _defaultUrlReplacing; diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs index f280b3e20c..ddb168ddbd 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs @@ -2,66 +2,36 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class SecurityElement : ConfigurationElement, ISecuritySection + internal class SecurityElement : UmbracoConfigurationElement, ISecuritySection { [ConfigurationProperty("keepUserLoggedIn")] internal InnerTextConfigurationElement KeepUserLoggedIn { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["keepUserLoggedIn"], - //set the default - true); - } + get { return GetOptionalTextElement("keepUserLoggedIn", true); } } [ConfigurationProperty("hideDisabledUsersInBackoffice")] internal InnerTextConfigurationElement HideDisabledUsersInBackoffice { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["hideDisabledUsersInBackoffice"], - //set the default - false); - } + get { return GetOptionalTextElement("hideDisabledUsersInBackoffice", false); } } [ConfigurationProperty("allowPasswordReset")] internal InnerTextConfigurationElement AllowPasswordReset { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["allowPasswordReset"], - //set the default - true); - } + get { return GetOptionalTextElement("allowPasswordReset", true); } } [ConfigurationProperty("authCookieName")] internal InnerTextConfigurationElement AuthCookieName { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["authCookieName"], - //set the default - Constants.Web.AuthCookieName); - } + get { return GetOptionalTextElement("authCookieName", Constants.Web.AuthCookieName); } } [ConfigurationProperty("authCookieDomain")] internal InnerTextConfigurationElement AuthCookieDomain { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["authCookieDomain"], - //set the default - null); - } + get { return GetOptionalTextElement("authCookieDomain", null); } } bool ISecuritySection.KeepUserLoggedIn diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/TemplatesElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/TemplatesElement.cs index c7eb659766..4e249df3a2 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/TemplatesElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/TemplatesElement.cs @@ -3,55 +3,31 @@ using System.Configuration; namespace Umbraco.Core.Configuration.UmbracoSettings { - internal class TemplatesElement : ConfigurationElement, ITemplatesSection + internal class TemplatesElement : UmbracoConfigurationElement, ITemplatesSection { [ConfigurationProperty("useAspNetMasterPages")] internal InnerTextConfigurationElement UseAspNetMasterPages { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["useAspNetMasterPages"], - //set the default - true); - } + get { return GetOptionalTextElement("useAspNetMasterPages", true); } } [ConfigurationProperty("enableSkinSupport")] internal InnerTextConfigurationElement EnableSkinSupport { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["enableSkinSupport"], - //set the default - true); - } + get { return GetOptionalTextElement("enableSkinSupport", true); } } [ConfigurationProperty("defaultRenderingEngine", IsRequired = true)] internal InnerTextConfigurationElement DefaultRenderingEngine { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["defaultRenderingEngine"], - //set the default - RenderingEngine.Mvc); - } + get { return GetOptionalTextElement("defaultRenderingEngine", RenderingEngine.Mvc); } } [Obsolete("This has no affect and will be removed in future versions")] [ConfigurationProperty("enableTemplateFolders")] internal InnerTextConfigurationElement EnableTemplateFolders { - get - { - return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["enableTemplateFolders"], - //set the default - false); - } + get { return GetOptionalTextElement("enableTemplateFolders", false); } } bool ITemplatesSection.UseAspNetMasterPages diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoConfigurationElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoConfigurationElement.cs new file mode 100644 index 0000000000..063b5324d8 --- /dev/null +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoConfigurationElement.cs @@ -0,0 +1,36 @@ +using System.Collections.Concurrent; +using System.Configuration; + +namespace Umbraco.Core.Configuration.UmbracoSettings +{ + /// + /// Base class with shared helper methods + /// + internal class UmbracoConfigurationElement : ConfigurationElement + { + /// + /// Used so the RawElement types are not re-created every time they are accessed + /// + private readonly ConcurrentDictionary _rawElements = new ConcurrentDictionary(); + + protected OptionalInnerTextConfigurationElement GetOptionalTextElement(string name, T defaultVal) + { + return (OptionalInnerTextConfigurationElement) _rawElements.GetOrAdd( + name, + s => new OptionalInnerTextConfigurationElement( + (InnerTextConfigurationElement) this[s], + //set the default + defaultVal)); + } + + protected OptionalCommaDelimitedConfigurationElement GetOptionalDelimitedElement(string name, string[] defaultVal) + { + return (OptionalCommaDelimitedConfigurationElement) _rawElements.GetOrAdd( + name, + s => new OptionalCommaDelimitedConfigurationElement( + (CommaDelimitedConfigurationElement) this[name], + //set the default + defaultVal)); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 0741470f51..950a44502f 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -49,8 +49,8 @@ ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll - - ..\packages\ImageProcessor.2.5.0\lib\net45\ImageProcessor.dll + + ..\packages\ImageProcessor.2.5.1\lib\net45\ImageProcessor.dll ..\packages\log4net-mediumtrust.2.0.0\lib\log4net.dll @@ -282,6 +282,7 @@ + diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index e132059da8..221e4063f6 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -2,7 +2,7 @@ - + diff --git a/src/Umbraco.Tests.Benchmarks/LinqCastBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/LinqCastBenchmarks.cs new file mode 100644 index 0000000000..4118620515 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/LinqCastBenchmarks.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnostics.Windows; + +namespace Umbraco.Tests.Benchmarks +{ + /// + /// Want to check what is faster OfType or Cast when a enurable all has the same items + /// + [Config(typeof(Config))] + public class LinqCastBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + Add(new MemoryDiagnoser()); + } + } + + public LinqCastBenchmarks() + { + _array = new List(); + _array.AddRange(Enumerable.Range(0, 10000).Select(x => x.ToString())); + } + + private readonly List _array; + + [Benchmark(Baseline = true)] + public void OfType() + { + foreach (var i in _array.OfType()) + { + var a = i; + } + } + + [Benchmark()] + public void Cast() + { + foreach (var i in _array.Cast()) + { + var a = i; + } + } + + [Benchmark()] + public void ExplicitCast() + { + foreach (var i in _array) + { + var a = (string)i; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests.Benchmarks/Program.cs b/src/Umbraco.Tests.Benchmarks/Program.cs index 52736ed6a4..eb49f153cc 100644 --- a/src/Umbraco.Tests.Benchmarks/Program.cs +++ b/src/Umbraco.Tests.Benchmarks/Program.cs @@ -8,23 +8,14 @@ namespace Umbraco.Tests.Benchmarks { public static void Main(string[] args) { - if (args.Length == 1) + var switcher = new BenchmarkSwitcher(new[] { - var type = Assembly.GetExecutingAssembly().GetType("Umbraco.Tests.Benchmarks." +args[0]); - if (type == null) - { - Console.WriteLine("Unknown benchmark."); - } - else - { - var summary = BenchmarkRunner.Run(type); - Console.ReadLine(); - } - } - else - { - Console.WriteLine("?"); - } + typeof(BulkInsertBenchmarks), + typeof(ModelToSqlExpressionHelperBenchmarks), + typeof(XmlBenchmarks), + typeof(LinqCastBenchmarks) + }); + switcher.Run(args); } } } diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index 9f6d4e799f..39d77bbc26 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -85,6 +85,7 @@ + diff --git a/src/Umbraco.Tests/Routing/NiceUrlProviderTests.cs b/src/Umbraco.Tests/Routing/NiceUrlProviderTests.cs index 075205ef5c..a8e5b1e64d 100644 --- a/src/Umbraco.Tests/Routing/NiceUrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/NiceUrlProviderTests.cs @@ -32,7 +32,7 @@ namespace Umbraco.Tests.Routing } /// - /// This checks that when we retrieve a NiceUrl for multiple items that there are no issues with cache overlap + /// This checks that when we retrieve a NiceUrl for multiple items that there are no issues with cache overlap /// and that they are all cached correctly. /// [Test] @@ -71,25 +71,19 @@ namespace Umbraco.Tests.Routing var cache = routingContext.UmbracoContext.ContentCache.InnerCache as PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); + var cachedRoutes = cache.RoutesCache.GetCachedRoutes(); - Assert.AreEqual(8, cachedRoutes.Count); + Assert.AreEqual(8, cachedRoutes.Count); - foreach (var sample in samples) - { - Assert.IsTrue(cachedRoutes.ContainsKey(sample.Key)); - Assert.AreEqual(sample.Value, cachedRoutes[sample.Key]); - } + foreach (var sample in samples) + { + Assert.IsTrue(cachedRoutes.ContainsKey(sample.Key)); + Assert.AreEqual(sample.Value, cachedRoutes[sample.Key]); + } - var cachedIds = cache.RoutesCache.GetCachedIds(); - Assert.AreEqual(8, cachedIds.Count); - - foreach (var sample in samples) - { - var key = sample.Value; - Assert.IsTrue(cachedIds.ContainsKey(key)); - Assert.AreEqual(sample.Key, cachedIds[key]); - } - } + var cachedIds = cache.RoutesCache.GetCachedIds(); + Assert.AreEqual(0, cachedIds.Count); + } // test hideTopLevelNodeFromPath false [TestCase(1046, "/home/")] @@ -147,10 +141,10 @@ namespace Umbraco.Tests.Routing var routingContext = GetRoutingContext("http://example.com/test", 1111, umbracoSettings: _umbracoSettings); - + Assert.AreEqual("/home/sub1/custom-sub-1/", routingContext.UrlProvider.GetUrl(1177)); - + requestMock.Setup(x => x.UseDomainPrefixes).Returns(true); Assert.AreEqual("http://example.com/home/sub1/custom-sub-1/", routingContext.UrlProvider.GetUrl(1177)); @@ -173,14 +167,14 @@ namespace Umbraco.Tests.Routing Assert.AreEqual("#", routingContext.UrlProvider.GetUrl(999999)); - requestMock.Setup(x => x.UseDomainPrefixes).Returns(true); - + requestMock.Setup(x => x.UseDomainPrefixes).Returns(true); + Assert.AreEqual("#", routingContext.UrlProvider.GetUrl(999999)); requestMock.Setup(x => x.UseDomainPrefixes).Returns(false); routingContext.UrlProvider.Mode = UrlProviderMode.Absolute; - + Assert.AreEqual("#", routingContext.UrlProvider.GetUrl(999999)); } } diff --git a/src/Umbraco.Tests/Routing/NiceUrlRoutesTests.cs b/src/Umbraco.Tests/Routing/NiceUrlRoutesTests.cs new file mode 100644 index 0000000000..131107e8e6 --- /dev/null +++ b/src/Umbraco.Tests/Routing/NiceUrlRoutesTests.cs @@ -0,0 +1,325 @@ +using System; +using NUnit.Framework; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web.PublishedCache.XmlPublishedCache; +using Umbraco.Web.Routing; + +namespace Umbraco.Tests.Routing +{ + // purpose: test the values returned by PublishedContentCache.GetRouteById + // and .GetByRoute (no caching at all, just routing nice urls) including all + // the quirks due to hideTopLevelFromPath and backward compatibility. + + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerFixture)] + [TestFixture] + public class NiceUrlRoutesTests : BaseRoutingTest + { + #region Test Setup + + protected override void FreezeResolution() + { + SiteDomainHelperResolver.Current = new SiteDomainHelperResolver(new SiteDomainHelper()); + + base.FreezeResolution(); + } + + private IUmbracoSettingsSection _umbracoSettings; + + public override void Initialize() + { + base.Initialize(); + + //generate new mock settings and assign so we can configure in individual tests + _umbracoSettings = SettingsForTests.GenerateMockSettings(); + SettingsForTests.ConfigureSettings(_umbracoSettings); + } + + protected override string GetXmlContent(int templateId) + { + return @" + + +]> + + + + + + + + + + + + + + + + + + + + + + + +"; + } + + #endregion + + /* + * Just so it's documented somewhere, as of jan. 2017, routes obey the following pseudo-code: + +GetByRoute(route, hide = null): + + route is "[id]/[path]" + + hide = hide ?? global.hide + + root = id ? node(id) : document + + content = cached(route) ?? DetermineIdByRoute(route, hide) + + # route is "1234/path/to/content", finds "content" + # but if there is domain 5678 on "to", the *true* route of "content" is "5678/content" + # so although the route does match, we don't cache it + # there are not other reason not to cache it + + if content and no domain between root and content: + cache route (as trusted) + + return content + + +DetermineIdByRoute(route, hide): + + route is "[id]/[path]" + + try return NavigateRoute(id ?? 0, path, hide:hide) + return null + + +NavigateRoute(id, path, hide): + + if path: + if id: + start = node(id) + else: + start = document + + # 'navigate ... from ...' uses lowest sortOrder in case of collision + + if hide and ![id]: + # if hiding, then for "/foo" we want to look for "/[any]/foo" + for each child of start: + try return navigate path from child + + # but if it fails, we also want to try "/foo" + # fail now if more than one part eg "/foo/bar" + if path is "/[any]/...": + fail + + try return navigate path from start + + else: + if id: + return node(id) + else: + return root node with lowest sortOrder + + +GetRouteById(id): + + + route = cached(id) + if route: + return route + + # never cache the route, it may be colliding + + route = DetermineRouteById(id) + if route: + cache route (as not trusted) + + return route + + + +DetermineRouteById(id): + + + node = node(id) + + walk up from node to domain or root, assemble parts = url segments + + if !domain and global.hide: + if id.parent: + # got /top/[path]content, can remove /top + remove top part + else: + # got /content, should remove only if it is the + # node with lowest sort order + root = root node with lowest sortOrder + if root == node: + remove top part + + compose path from parts + route = assemble "[domain.id]/[path]" + return route + + */ + + /* + * The Xml structure for the following tests is: + * + * root + * A 1000 + * B 1001 + * C 1002 + * D 1003 + * X 2000 + * Y 2001 + * Z 2002 + * A 2003 + * B 2004 + * C 2005 + * E 2006 + * + */ + + [TestCase(1000, false, "/a")] + [TestCase(1001, false, "/a/b")] + [TestCase(1002, false, "/a/b/c")] + [TestCase(1003, false, "/a/b/c/d")] + [TestCase(2000, false, "/x")] + [TestCase(2001, false, "/x/y")] + [TestCase(2002, false, "/x/y/z")] + [TestCase(2003, false, "/x/a")] + [TestCase(2004, false, "/x/b")] + [TestCase(2005, false, "/x/b/c")] + [TestCase(2006, false, "/x/b/e")] + [TestCase(1000, true, "/")] + [TestCase(1001, true, "/b")] + [TestCase(1002, true, "/b/c")] + [TestCase(1003, true, "/b/c/d")] + [TestCase(2000, true, "/x")] + [TestCase(2001, true, "/y")] + [TestCase(2002, true, "/y/z")] + [TestCase(2003, true, "/a")] + [TestCase(2004, true, "/b")] // collision! + [TestCase(2005, true, "/b/c")] // collision! + [TestCase(2006, true, "/b/e")] // risky! + public void GetRouteById(int id, bool hide, string expected) + { + var umbracoContext = GetUmbracoContext("/test", 0); + var cache = umbracoContext.ContentCache.InnerCache as PublishedContentCache; + if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); + + SettingsForTests.HideTopLevelNodeFromPath = hide; + + const bool preview = true; // make sure we don't cache + var route = cache.GetRouteById(umbracoContext, preview, id); + Assert.AreEqual(expected, route); + } + + [Test] + public void GetRouteByIdCache() + { + var umbracoContext = GetUmbracoContext("/test", 0); + var cache = umbracoContext.ContentCache.InnerCache as PublishedContentCache; + if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); + + SettingsForTests.HideTopLevelNodeFromPath = false; + + // make sure we cache + PublishedContentCache.UnitTesting = false; + const bool preview = false; + + var route = cache.GetRouteById(umbracoContext, preview, 1000); + Assert.AreEqual("/a", route); + + // GetRouteById registers a non-trusted route, which is cached for + // id -> route queries (fast GetUrl) but *not* for route -> id + // queries (safe inbound routing) + + var cachedRoutes = cache.RoutesCache.GetCachedRoutes(); + Assert.AreEqual(1, cachedRoutes.Count); + Assert.IsTrue(cachedRoutes.ContainsKey(1000)); + Assert.AreEqual("/a", cachedRoutes[1000]); + + var cachedIds = cache.RoutesCache.GetCachedIds(); + Assert.AreEqual(0, cachedIds.Count); + } + + [TestCase("/", false, 1000)] + [TestCase("/a", false, 1000)] // yes! + [TestCase("/a/b", false, 1001)] + [TestCase("/a/b/c", false, 1002)] + [TestCase("/a/b/c/d", false, 1003)] + [TestCase("/x", false, 2000)] + [TestCase("/", true, 1000)] + [TestCase("/a", true, 2003)] + [TestCase("/a/b", true, -1)] + [TestCase("/x", true, 2000)] // oops! + [TestCase("/x/y", true, -1)] // yes! + [TestCase("/y", true, 2001)] + [TestCase("/y/z", true, 2002)] + [TestCase("/b", true, 1001)] // (hence the 2004 collision) + [TestCase("/b/c", true, 1002)] // (hence the 2005 collision) + public void GetByRoute(string route, bool hide, int expected) + { + var umbracoContext = GetUmbracoContext("/test", 0); + var cache = umbracoContext.ContentCache.InnerCache as PublishedContentCache; + if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); + + SettingsForTests.HideTopLevelNodeFromPath = hide; + + const bool preview = true; // make sure we don't cache + var content = cache.GetByRoute(umbracoContext, preview, route); + if (expected < 0) + { + Assert.IsNull(content); + } + else + { + Assert.IsNotNull(content); + Assert.AreEqual(expected, content.Id); + } + } + + [Test] + public void GetByRouteCache() + { + var umbracoContext = GetUmbracoContext("/test", 0); + var cache = umbracoContext.ContentCache.InnerCache as PublishedContentCache; + if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); + + SettingsForTests.HideTopLevelNodeFromPath = false; + + // make sure we cache + PublishedContentCache.UnitTesting = false; + const bool preview = false; + + var content = cache.GetByRoute(umbracoContext, preview, "/a/b/c"); + Assert.IsNotNull(content); + Assert.AreEqual(1002, content.Id); + + // GetByRoute registers a trusted route, which is cached both for + // id -> route queries (fast GetUrl) and for route -> id queries + // (fast inbound routing) + + var cachedRoutes = cache.RoutesCache.GetCachedRoutes(); + Assert.AreEqual(1, cachedRoutes.Count); + Assert.IsTrue(cachedRoutes.ContainsKey(1002)); + Assert.AreEqual("/a/b/c", cachedRoutes[1002]); + + var cachedIds = cache.RoutesCache.GetCachedIds(); + Assert.AreEqual(1, cachedIds.Count); + Assert.IsTrue(cachedIds.ContainsKey("/a/b/c")); + Assert.AreEqual(1002, cachedIds["/a/b/c"]); + } + } +} diff --git a/src/Umbraco.Tests/Routing/NiceUrlsProviderWithDomainsTests.cs b/src/Umbraco.Tests/Routing/NiceUrlsProviderWithDomainsTests.cs index 8cc975954b..ab25e0b43c 100644 --- a/src/Umbraco.Tests/Routing/NiceUrlsProviderWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/NiceUrlsProviderWithDomainsTests.cs @@ -79,7 +79,7 @@ namespace Umbraco.Tests.Routing protected override string GetXmlContent(int templateId) { return @" - ]> @@ -302,11 +302,12 @@ namespace Umbraco.Tests.Routing var cache = routingContext.UmbracoContext.ContentCache.InnerCache as PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); + var cachedRoutes = cache.RoutesCache.GetCachedRoutes(); Assert.AreEqual(7, cachedRoutes.Count); var cachedIds = cache.RoutesCache.GetCachedIds(); - Assert.AreEqual(7, cachedIds.Count); + Assert.AreEqual(0, cachedIds.Count); CheckRoute(cachedRoutes, cachedIds, 1001, "1001/"); CheckRoute(cachedRoutes, cachedIds, 10011, "10011/"); @@ -328,12 +329,11 @@ namespace Umbraco.Tests.Routing Assert.AreEqual("http://domain1.com/fr/1001-2-1/", routingContext.UrlProvider.GetUrl(100121, new Uri("http://domain2.com"), false)); } - void CheckRoute(IDictionary routes, IDictionary ids, int id, string route) + private static void CheckRoute(IDictionary routes, IDictionary ids, int id, string route) { Assert.IsTrue(routes.ContainsKey(id)); Assert.AreEqual(route, routes[id]); - Assert.IsTrue(ids.ContainsKey(route)); - Assert.AreEqual(id, ids[route]); + Assert.IsFalse(ids.ContainsKey(route)); } [Test] diff --git a/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs b/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs index 2e868f9f44..19420e3120 100644 --- a/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs +++ b/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs @@ -26,7 +26,7 @@ namespace Umbraco.Tests.Routing public void DoNotPolluteCache() { SettingsForTests.UseDirectoryUrls = true; - SettingsForTests.HideTopLevelNodeFromPath = false; // ignored w/domains + SettingsForTests.HideTopLevelNodeFromPath = false; // ignored w/domains var settings = SettingsForTests.GenerateMockSettings(); var request = Mock.Get(settings.RequestHandler); @@ -36,7 +36,7 @@ namespace Umbraco.Tests.Routing RoutingContext routingContext; string url = "http://domain1.com/1001-1/1001-1-1"; - + // get the nice url for 100111 routingContext = GetRoutingContext(url, 9999, umbracoSettings: settings); Assert.AreEqual("http://domain2.com/1001-1-1/", routingContext.UrlProvider.GetUrl(100111, true)); @@ -89,7 +89,7 @@ namespace Umbraco.Tests.Routing protected override string GetXmlContent(int templateId) { return @" - ]> diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index b5faf40c83..1f5e324142 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -167,6 +167,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmroutechange.html b/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmroutechange.html index 0b2926307c..6d47084f08 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmroutechange.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmroutechange.html @@ -2,6 +2,6 @@

You have unsaved changes

Are you sure you want to navigate away from this page? - you have unsaved changes

- - - \ No newline at end of file + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js index d8f8557fa5..02d5a62432 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js @@ -178,8 +178,6 @@ function DataTypeEditController($scope, $routeParams, $location, appState, navig //share state editorState.set($scope.content); - - dataTypeHelper.rebindChangedProperties($scope.content, data); }); } diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index d24491defe..27e437d8bf 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -135,11 +135,11 @@ False ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll
- - ..\packages\ImageProcessor.2.5.0\lib\net45\ImageProcessor.dll + + ..\packages\ImageProcessor.2.5.1\lib\net45\ImageProcessor.dll - - ..\packages\ImageProcessor.Web.4.7.1\lib\net45\ImageProcessor.Web.dll + + ..\packages\ImageProcessor.Web.4.7.2\lib\net45\ImageProcessor.Web.dll False @@ -654,7 +654,9 @@ - + + Designer + log4net.config diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index b708ff8de0..6919e10f64 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -5,8 +5,8 @@ - - + + diff --git a/src/Umbraco.Web/Models/ContentExtensions.cs b/src/Umbraco.Web/Models/ContentExtensions.cs index cfee14a2a8..d1f153125e 100644 --- a/src/Umbraco.Web/Models/ContentExtensions.cs +++ b/src/Umbraco.Web/Models/ContentExtensions.cs @@ -46,9 +46,7 @@ namespace Umbraco.Web.Models { var route = umbracoContext == null ? null // for tests only - : umbracoContext.ContentCache.GetRouteById(contentId); // cached - - if (route != null && route.StartsWith("err/")) route = null; + : umbracoContext.ContentCache.GetRouteById(contentId); // may be cached var domainHelper = new DomainHelper(domainService); IDomain domain; @@ -73,7 +71,7 @@ namespace Umbraco.Web.Models } else { - // if content is published then we have a (cached) route + // if content is published then we have a route // from which we can figure out the domain var pos = route.IndexOf('/'); diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index e58bc73b25..c8ddd77afd 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -2,11 +2,9 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Runtime.CompilerServices; -using System.Text; using System.Xml; using System.Xml.XPath; using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; @@ -18,7 +16,6 @@ using umbraco.BusinessLogic; using umbraco.presentation.preview; using Umbraco.Core.Services; using GlobalSettings = umbraco.GlobalSettings; -using Task = System.Threading.Tasks.Task; namespace Umbraco.Web.PublishedCache.XmlPublishedCache { @@ -83,7 +80,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache && DomainHelper.ExistsDomainInPath(umbracoContext.Application.Services.DomainService.GetAll(false), content.Path, domainRootNodeId) == false; if (deepest) - _routesCache.Store(content.Id, route); + _routesCache.Store(content.Id, route, true); // trusted route } public virtual string GetRouteById(UmbracoContext umbracoContext, bool preview, int contentId) @@ -102,38 +99,13 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (route == null) return null; - // find the content back, detect routes collisions: we should find ourselves back, - // else it means that another content with "higher priority" is sharing the same route. - // perf impact: - // - non-colliding, adds one complete "by route" lookup, only on the first time a url is computed (then it's cached anyways) - // - colliding, adds one "by route" lookup, the first time the url is computed, then one dictionary looked each time it is computed again - // assuming no collisions, the impact is one complete "by route" lookup the first time each url is computed - // - // U4-9121 - this lookup is too expensive when computing a large amount of urls on a front-end (eg menu) - // ... thinking about moving the lookup out of the path into its own async task, so we are not reporting errors - // in the back-office anymore, but at least we are not polluting the cache - // instead, refactored DeterminedIdByRoute to stop using XPath, with a 16x improvement according to benchmarks - // will it be enough? + // cache the route BUT do NOT trust it as it can be a colliding route + // meaning if we GetRouteById again, we'll get it from cache, but it + // won't be used for inbound routing + if (preview == false) + _routesCache.Store(contentId, route, false); - var loopId = preview ? 0 : _routesCache.GetNodeId(route); // might be cached already in case of collision - if (loopId == 0) - { - var content = DetermineIdByRoute(umbracoContext, preview, route, GlobalSettings.HideTopLevelNodeFromPath); - - // add the other route to cache so next time we have it already - if (content != null && preview == false) - AddToCacheIfDeepestRoute(umbracoContext, content, route); - - loopId = content == null ? 0 : content.Id; // though... 0 here would be quite weird? - } - - // cache if we have a route and not previewing and it's not a colliding route - // (the result of DetermineRouteById is always the deepest route) - if (/*route != null &&*/ preview == false && loopId == contentId) - _routesCache.Store(contentId, route); - - // return route if no collision, else report collision - return loopId == contentId ? route : ("err/" + loopId); + return route; } IPublishedContent DetermineIdByRoute(UmbracoContext umbracoContext, bool preview, string route, bool hideTopLevelNode) @@ -149,18 +121,25 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache //check if we can find the node in our xml cache var id = NavigateRoute(umbracoContext, preview, startNodeId, path, hideTopLevelNode); - if (id > 0) return GetById(umbracoContext, preview, id); + return id > 0 ? GetById(umbracoContext, preview, id) : null; + } - // if hideTopLevelNodePath is true then for url /foo we looked for /*/foo - // but maybe that was the url of a non-default top-level node, so we also - // have to look for /foo (see note in ApplyHideTopLevelNodeFromPath). - if (hideTopLevelNode && path.Length > 1 && path.IndexOf('/', 1) < 0) + private static XmlElement GetXmlElementChildWithLowestSortOrder(XmlNode element) + { + XmlElement elt = null; + var min = int.MaxValue; + foreach (var n in element.ChildNodes) { - var id2 = NavigateRoute(umbracoContext, preview, startNodeId, path, false); - if (id2 > 0) return GetById(umbracoContext, preview, id2); - } + var e = n as XmlElement; + if (e == null) continue; - return null; + var sortOrder = int.Parse(e.GetAttribute("sortOrder")); + if (sortOrder >= min) continue; + + min = sortOrder; + elt = e; + } + return elt; } private int NavigateRoute(UmbracoContext umbracoContext, bool preview, int startNodeId, string path, bool hideTopLevelNode) @@ -177,17 +156,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache return elt == null ? -1 : startNodeId; } - elt = null; - var min = int.MaxValue; - foreach (var e in xml.DocumentElement.ChildNodes.OfType()) - { - var sortOrder = int.Parse(e.GetAttribute("sortOrder")); - if (sortOrder < min) - { - min = sortOrder; - elt = e; - } - } + elt = GetXmlElementChildWithLowestSortOrder(xml.DocumentElement); return elt == null ? -1 : int.Parse(elt.GetAttribute("id")); } @@ -201,12 +170,19 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (hideTopLevelNode && startNodeId <= 0) { - foreach (var e in elt.ChildNodes.OfType()) + //Don't use OfType or Cast, this is critical code, all ChildNodes are XmlElement so explicitly cast + // https://gist.github.com/Shazwazza/04e2e5642a316f4a87e52dada2901198 + foreach (var n in elt.ChildNodes) { + var e = n as XmlElement; + if (e == null) continue; + var id = NavigateElementRoute(e, urlParts); if (id > 0) return id; } - return -1; + + if (urlParts.Length > 1) + return -1; } return NavigateElementRoute(elt, urlParts); @@ -224,8 +200,14 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache while (found && i < urlParts.Length) { found = false; - foreach (var child in elt.ChildNodes.OfType()) + //Don't use OfType or Cast, this is critical code, all ChildNodes are XmlElement so explicitly cast + // https://gist.github.com/Shazwazza/04e2e5642a316f4a87e52dada2901198 + var sortOrder = -1; + foreach (var o in elt.ChildNodes) { + var child = o as XmlElement; + if (child == null) continue; + var noNode = UseLegacySchema ? child.Name != "node" : child.GetAttributeNode("isDoc") == null; @@ -233,8 +215,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (child.GetAttribute("urlName") != urlParts[i]) continue; found = true; + + var so = int.Parse(child.GetAttribute("sortOrder")); + if (sortOrder >= 0 && so >= sortOrder) continue; + + sortOrder = so; elt = child; - break; } i++; } @@ -243,7 +229,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache string DetermineRouteById(UmbracoContext umbracoContext, bool preview, int contentId) { - var elt = GetXml(umbracoContext, preview).GetElementById(contentId.ToString(CultureInfo.InvariantCulture)); + var xml = GetXml(umbracoContext, preview); + var elt = xml.GetElementById(contentId.ToString(CultureInfo.InvariantCulture)); if (elt == null) return null; var domainHelper = GetDomainHelper(umbracoContext.Application.Services.DomainService); @@ -270,7 +257,18 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // no domain, respect HideTopLevelNodeFromPath for legacy purposes if (hasDomains == false && GlobalSettings.HideTopLevelNodeFromPath) - ApplyHideTopLevelNodeFromPath(umbracoContext, eltId, eltParentId, pathParts); + { + if (eltParentId == -1) + { + var rootNode = GetXmlElementChildWithLowestSortOrder(xml.DocumentElement); + if (rootNode != null && rootNode == elt) + pathParts.RemoveAt(pathParts.Count - 1); + } + else + { + pathParts.RemoveAt(pathParts.Count - 1); + } + } // assemble the route pathParts.Reverse(); @@ -280,30 +278,6 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache return route; } - static void ApplyHideTopLevelNodeFromPath(UmbracoContext umbracoContext, int nodeId, int parentId, IList pathParts) - { - // in theory if hideTopLevelNodeFromPath is true, then there should be only once - // top-level node, or else domains should be assigned. but for backward compatibility - // we add this check - we look for the document matching "/" and if it's not us, then - // we do not hide the top level path - // it has to be taken care of in GetByRoute too so if - // "/foo" fails (looking for "/*/foo") we try also "/foo". - // this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but - // that's the way it works pre-4.10 and we try to be backward compat for the time being - if (parentId == -1) - { - var rootNode = umbracoContext.ContentCache.GetByRoute("/", true); - if (rootNode == null) - throw new Exception("Failed to get node at /."); - if (rootNode.Id == nodeId) // remove only if we're the default node - pathParts.RemoveAt(pathParts.Count - 1); - } - else - { - pathParts.RemoveAt(pathParts.Count - 1); - } - } - #endregion #region XPath Strings diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/RoutesCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/RoutesCache.cs index 9c8eed322b..4e775b3253 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/RoutesCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/RoutesCache.cs @@ -83,10 +83,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// /// The node identified. /// The route. - public void Store(int nodeId, string route) + /// A value indicating whether the value can be trusted for inbound routing. + public void Store(int nodeId, string route, bool trust) { _routes.AddOrUpdate(nodeId, i => route, (i, s) => route); - _nodeIds.AddOrUpdate(route, i => nodeId, (i, s) => nodeId); + if (trust) + _nodeIds.AddOrUpdate(route, i => nodeId, (i, s) => nodeId); } /// @@ -119,15 +121,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// The node identifier. public void ClearNode(int nodeId) { - if (!_routes.ContainsKey(nodeId)) return; - - string key; - if (!_routes.TryGetValue(nodeId, out key)) return; - - int val; - _nodeIds.TryRemove(key, out val); - string val2; - _routes.TryRemove(nodeId, out val2); + string route; + if (_routes.TryRemove(nodeId, out route)) + { + int id; + _nodeIds.TryRemove(route, out id); + } } /// diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs index d55095d151..6b3c0fe69b 100644 --- a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -65,14 +65,6 @@ namespace Umbraco.Web.Routing return null; } - if (route.StartsWith("err/")) - { - LogHelper.Debug( - "Page with nodeId={0} has a colliding url with page with nodeId={1}.", - () => id, () => route.Substring(4)); - return "#err-" + route.Substring(4); - } - var domainHelper = new DomainHelper(umbracoContext.Application.Services.DomainService); // extract domainUri and path @@ -115,9 +107,6 @@ namespace Umbraco.Web.Routing return null; } - if (route.StartsWith("err/")) - return null; - var domainHelper = new DomainHelper(umbracoContext.Application.Services.DomainService); // extract domainUri and path diff --git a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs index 03a50a4b99..05b6b0e468 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs @@ -67,6 +67,32 @@ namespace Umbraco.Web.Routing #region Public + /// + /// Tries to route the request. + /// + internal bool TryRouteRequest() + { + // disabled - is it going to change the routing? + //_pcr.OnPreparing(); + + FindDomain(); + if (_pcr.IsRedirect) return false; + if (_pcr.HasPublishedContent) return true; + FindPublishedContent(); + if (_pcr.IsRedirect) return false; + + // don't handle anything - we just want to ensure that we find the content + //HandlePublishedContent(); + //FindTemplate(); + //FollowExternalRedirect(); + //HandleWildcardDomains(); + + // disabled - we just want to ensure that we find the content + //_pcr.OnPrepared(); + + return _pcr.HasPublishedContent; + } + /// /// Prepares the request. /// diff --git a/src/Umbraco.Web/Routing/RedirectTrackingEventHandler.cs b/src/Umbraco.Web/Routing/RedirectTrackingEventHandler.cs index b13cb7b296..80e4192f72 100644 --- a/src/Umbraco.Web/Routing/RedirectTrackingEventHandler.cs +++ b/src/Umbraco.Web/Routing/RedirectTrackingEventHandler.cs @@ -295,8 +295,7 @@ namespace Umbraco.Web.Routing private static bool IsNotRoute(string route) { // null if content not found - // err/- if collision or anomaly or ... - return route == null || route.StartsWith("err/"); + return route == null; } // gets a value indicating whether server is 'slave' diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index c6e08c16c5..54ef8a42b5 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.Web.Security; using Umbraco.Core.Models; using umbraco; +using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Logging; namespace Umbraco.Web.Routing @@ -62,34 +65,45 @@ namespace Umbraco.Web.Routing { urls.Add(ui.Text("content", "getUrlException", umbracoContext.Security.CurrentUser)); } - else if (url.StartsWith("#err-")) + else { - // route error, report - var id = int.Parse(url.Substring(5)); - var o = umbracoContext.ContentCache.GetById(id); - string s; - if (o == null) + // test for collisions + var uri = new Uri(url.TrimEnd('/'), UriKind.RelativeOrAbsolute); + if (uri.IsAbsoluteUri == false) uri = uri.MakeAbsolute(UmbracoContext.Current.CleanedUmbracoUrl); + var pcr = new PublishedContentRequest(uri, UmbracoContext.Current.RoutingContext, UmbracoConfig.For.UmbracoSettings().WebRouting, s => Roles.Provider.GetRolesForUser(s)); + pcr.Engine.TryRouteRequest(); + + if (pcr.HasPublishedContent == false) { - s = "(unknown)"; + urls.Add(ui.Text("content", "routeError", "(error)", umbracoContext.Security.CurrentUser)); + } + else if (pcr.PublishedContent.Id != content.Id) + { + var o = pcr.PublishedContent; + string s; + if (o == null) + { + s = "(unknown)"; + } + else + { + var l = new List(); + while (o != null) + { + l.Add(o.Name); + o = o.Parent; + } + l.Reverse(); + s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")"; + + } + urls.Add(ui.Text("content", "routeError", s, umbracoContext.Security.CurrentUser)); } else { - var l = new List(); - while (o != null) - { - l.Add(o.Name); - o = o.Parent; - } - l.Reverse(); - s = "/" + string.Join("/", l) + " (id=" + id + ")"; - + urls.Add(url); + urls.AddRange(urlProvider.GetOtherUrls(content.Id)); } - urls.Add(ui.Text("content", "routeError", s, umbracoContext.Security.CurrentUser)); - } - else - { - urls.Add(url); - urls.AddRange(urlProvider.GetOtherUrls(content.Id)); } return urls; }