diff --git a/src/Umbraco.Core/Configuration/Models/CharItem.cs b/src/Umbraco.Core/Configuration/Models/CharItem.cs
new file mode 100644
index 0000000000..e269e0a83e
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/Models/CharItem.cs
@@ -0,0 +1,17 @@
+using Umbraco.Cms.Core.Configuration.UmbracoSettings;
+
+namespace Umbraco.Cms.Core.Configuration.Models
+{
+ public class CharItem : IChar
+ {
+ ///
+ /// The character to replace
+ ///
+ public string Char { get; set; }
+
+ ///
+ /// The replacement character
+ ///
+ public string Replacement { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs
index ee223b36c6..051c31dc26 100644
--- a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System;
using System.Collections.Generic;
using System.ComponentModel;
using Umbraco.Cms.Core.Configuration.UmbracoSettings;
@@ -16,33 +17,34 @@ namespace Umbraco.Cms.Core.Configuration.Models
{
internal const bool StaticAddTrailingSlash = true;
internal const string StaticConvertUrlsToAscii = "try";
+ internal const bool StaticEnableDefaultCharReplacements = true;
internal static readonly CharItem[] DefaultCharCollection =
{
- new CharItem { Char = " ", Replacement = "-" },
- new CharItem { Char = "\"", Replacement = string.Empty },
- new CharItem { Char = "'", Replacement = string.Empty },
- new CharItem { Char = "%", Replacement = string.Empty },
- new CharItem { Char = ".", Replacement = string.Empty },
- new CharItem { Char = ";", Replacement = string.Empty },
- new CharItem { Char = "/", Replacement = string.Empty },
- new CharItem { Char = "\\", Replacement = string.Empty },
- new CharItem { Char = ":", Replacement = string.Empty },
- new CharItem { Char = "#", Replacement = string.Empty },
- new CharItem { Char = "+", Replacement = "plus" },
- new CharItem { Char = "*", Replacement = "star" },
- new CharItem { Char = "&", Replacement = string.Empty },
- new CharItem { Char = "?", Replacement = string.Empty },
- new CharItem { Char = "æ", Replacement = "ae" },
- new CharItem { Char = "ä", Replacement = "ae" },
- new CharItem { Char = "ø", Replacement = "oe" },
- new CharItem { Char = "ö", Replacement = "oe" },
- new CharItem { Char = "å", Replacement = "aa" },
- new CharItem { Char = "ü", Replacement = "ue" },
- new CharItem { Char = "ß", Replacement = "ss" },
- new CharItem { Char = "|", Replacement = "-" },
- new CharItem { Char = "<", Replacement = string.Empty },
- new CharItem { Char = ">", Replacement = string.Empty }
+ new () { Char = " ", Replacement = "-" },
+ new () { Char = "\"", Replacement = string.Empty },
+ new () { Char = "'", Replacement = string.Empty },
+ new () { Char = "%", Replacement = string.Empty },
+ new () { Char = ".", Replacement = string.Empty },
+ new () { Char = ";", Replacement = string.Empty },
+ new () { Char = "/", Replacement = string.Empty },
+ new () { Char = "\\", Replacement = string.Empty },
+ new () { Char = ":", Replacement = string.Empty },
+ new () { Char = "#", Replacement = string.Empty },
+ new () { Char = "+", Replacement = "plus" },
+ new () { Char = "*", Replacement = "star" },
+ new () { Char = "&", Replacement = string.Empty },
+ new () { Char = "?", Replacement = string.Empty },
+ new () { Char = "æ", Replacement = "ae" },
+ new () { Char = "ä", Replacement = "ae" },
+ new () { Char = "ø", Replacement = "oe" },
+ new () { Char = "ö", Replacement = "oe" },
+ new () { Char = "å", Replacement = "aa" },
+ new () { Char = "ü", Replacement = "ue" },
+ new () { Char = "ß", Replacement = "ss" },
+ new () { Char = "|", Replacement = "-" },
+ new () { Char = "<", Replacement = string.Empty },
+ new () { Char = ">", Replacement = string.Empty }
};
///
@@ -67,41 +69,21 @@ namespace Umbraco.Cms.Core.Configuration.Models
///
public bool ShouldTryConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("try");
- // We need to special handle ":", as this character is special in keys
-
- // TODO: implement from configuration
-
- //// var collection = _configuration.GetSection(Prefix + "CharCollection").GetChildren()
- //// .Select(x => new CharItem()
- //// {
- //// Char = x.GetValue("Char"),
- //// Replacement = x.GetValue("Replacement"),
- //// }).ToArray();
-
- //// if (collection.Any() || _configuration.GetSection("Prefix").GetChildren().Any(x =>
- //// x.Key.Equals("CharCollection", StringComparison.OrdinalIgnoreCase)))
- //// {
- //// return collection;
- //// }
-
- //// return DefaultCharCollection;
+ ///
+ /// Disable all default character replacements
+ ///
+ [DefaultValue(StaticEnableDefaultCharReplacements)]
+ public bool EnableDefaultCharReplacements { get; set; } = StaticEnableDefaultCharReplacements;
///
- /// Gets or sets a value for the default character collection for replacements.
+ /// Add additional character replacements, or override defaults
///
- /// WB-TODO
+ [Obsolete("Use the GetCharReplacements extension method in the Umbraco.Extensions namespace instead. Scheduled for removal in V11")]
public IEnumerable CharCollection { get; set; } = DefaultCharCollection;
///
- /// Defines a character replacement.
+ /// Add additional character replacements, or override defaults
///
- public class CharItem : IChar
- {
- ///
- public string Char { get; set; }
-
- ///
- public string Replacement { get; set; }
- }
+ public IEnumerable UserDefinedCharCollection { get; set; }
}
}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs
new file mode 100644
index 0000000000..a916febb93
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Configuration.Models;
+
+namespace Umbraco.Cms.Core.Configuration.UmbracoSettings
+{
+ public class CharacterReplacementEqualityComparer : IEqualityComparer
+ {
+ public bool Equals(IChar x, IChar y)
+ {
+ if (ReferenceEquals(x, y))
+ {
+ return true;
+ }
+
+ if (x is null)
+ {
+ return false;
+ }
+
+ if (y is null)
+ {
+ return false;
+ }
+
+ if (x.GetType() != y.GetType())
+ {
+ return false;
+ }
+
+ return x.Char == y.Char && x.Replacement == y.Replacement;
+ }
+
+ public int GetHashCode(IChar obj)
+ {
+ unchecked
+ {
+ return ((obj.Char != null ? obj.Char.GetHashCode() : 0) * 397) ^ (obj.Replacement != null ? obj.Replacement.GetHashCode() : 0);
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs
index 4073a12149..61e840245c 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs
@@ -1,8 +1,9 @@
-namespace Umbraco.Cms.Core.Configuration.UmbracoSettings
+namespace Umbraco.Cms.Core.Configuration.UmbracoSettings
{
public interface IChar
{
string Char { get; }
+
string Replacement { get; }
}
}
diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs
index 6ef87464e8..d1a8542688 100644
--- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs
+++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs
@@ -1,9 +1,12 @@
using System;
+using System.Collections.Generic;
using System.Reflection;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Configuration.Models.Validation;
+using Umbraco.Extensions;
namespace Umbraco.Cms.Core.DependencyInjection
{
@@ -76,6 +79,8 @@ namespace Umbraco.Cms.Core.DependencyInjection
.AddUmbracoOptions()
.AddUmbracoOptions();
+ builder.Services.Configure(options => options.MergeReplacements(builder.Config));
+
return builder;
}
}
diff --git a/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs b/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs
new file mode 100644
index 0000000000..e9e6618f8c
--- /dev/null
+++ b/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs
@@ -0,0 +1,96 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Configuration;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Configuration.UmbracoSettings;
+
+namespace Umbraco.Extensions
+{
+ ///
+ /// Get concatenated user and default character replacements
+ /// taking into account
+ ///
+ public static class RequestHandlerSettingsExtension
+ {
+ ///
+ /// Get concatenated user and default character replacements
+ /// taking into account
+ ///
+ public static IEnumerable GetCharReplacements(this RequestHandlerSettings requestHandlerSettings)
+ {
+ if (requestHandlerSettings.EnableDefaultCharReplacements is false)
+ {
+ return requestHandlerSettings.UserDefinedCharCollection ?? Enumerable.Empty();
+ }
+
+ if (requestHandlerSettings.UserDefinedCharCollection == null || requestHandlerSettings.UserDefinedCharCollection.Any() is false)
+ {
+ return RequestHandlerSettings.DefaultCharCollection;
+ }
+
+ return MergeUnique(requestHandlerSettings.UserDefinedCharCollection, RequestHandlerSettings.DefaultCharCollection);
+ }
+
+ ///
+ /// Merges CharCollection and UserDefinedCharCollection, prioritizing UserDefinedCharCollection
+ ///
+ internal static void MergeReplacements(this RequestHandlerSettings requestHandlerSettings, IConfiguration configuration)
+ {
+ string sectionKey = $"{Constants.Configuration.ConfigRequestHandler}:";
+
+ IEnumerable charCollection = GetReplacements(
+ configuration,
+ $"{sectionKey}{nameof(RequestHandlerSettings.CharCollection)}");
+
+ IEnumerable userDefinedCharCollection = GetReplacements(
+ configuration,
+ $"{sectionKey}{nameof(requestHandlerSettings.UserDefinedCharCollection)}");
+
+ IEnumerable mergedCollection = MergeUnique(userDefinedCharCollection, charCollection);
+
+ requestHandlerSettings.UserDefinedCharCollection = mergedCollection;
+ }
+
+ private static IEnumerable GetReplacements(IConfiguration configuration, string key)
+ {
+ var replacements = new List();
+ IEnumerable config = configuration.GetSection(key).GetChildren();
+
+ foreach (IConfigurationSection section in config)
+ {
+ var @char = section.GetValue(nameof(CharItem.Char));
+ var replacement = section.GetValue(nameof(CharItem.Replacement));
+ replacements.Add(new CharItem { Char = @char, Replacement = replacement });
+ }
+
+ return replacements;
+ }
+
+ ///
+ /// Merges two IEnumerable of CharItem without any duplicates, items in priorityReplacements will override those in alternativeReplacements
+ ///
+ private static IEnumerable MergeUnique(
+ IEnumerable priorityReplacements,
+ IEnumerable alternativeReplacements)
+ {
+ var priorityReplacementsList = priorityReplacements.ToList();
+ var alternativeReplacementsList = alternativeReplacements.ToList();
+
+ foreach (CharItem alternativeReplacement in alternativeReplacementsList)
+ {
+ foreach (CharItem priorityReplacement in priorityReplacementsList)
+ {
+ if (priorityReplacement.Char == alternativeReplacement.Char)
+ {
+ alternativeReplacement.Replacement = priorityReplacement.Replacement;
+ }
+ }
+ }
+
+ return priorityReplacementsList.Union(
+ alternativeReplacementsList,
+ new CharacterReplacementEqualityComparer());
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs
index cf5e71a568..b0f0a9b003 100644
--- a/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs
+++ b/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs
@@ -1,7 +1,8 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Configuration.UmbracoSettings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Strings
@@ -60,7 +61,9 @@ namespace Umbraco.Cms.Core.Strings
/// The short string helper.
public DefaultShortStringHelperConfig WithDefault(RequestHandlerSettings requestHandlerSettings)
{
- UrlReplaceCharacters = requestHandlerSettings.CharCollection
+ IEnumerable charCollection = requestHandlerSettings.GetCharReplacements();
+
+ UrlReplaceCharacters = charCollection
.Where(x => string.IsNullOrEmpty(x.Char) == false)
.ToDictionary(x => x.Char, x => x.Replacement);
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/RequestHandlerSettingsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/RequestHandlerSettingsTests.cs
new file mode 100644
index 0000000000..f159ecbc85
--- /dev/null
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/RequestHandlerSettingsTests.cs
@@ -0,0 +1,90 @@
+using System.Linq;
+using NUnit.Framework;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configuration.Models
+{
+ [TestFixture]
+ public class RequestHandlerSettingsTests
+ {
+ [Test]
+ public void Given_CharCollection_With_DefaultEnabled_MergesCollection()
+ {
+ var userCollection = new CharItem[]
+ {
+ new () { Char = "test", Replacement = "replace" },
+ new () { Char = "test2", Replacement = "replace2" }
+ };
+
+
+ var settings = new RequestHandlerSettings { UserDefinedCharCollection = userCollection };
+ var actual = settings.GetCharReplacements().ToList();
+
+ var expectedCollection = RequestHandlerSettings.DefaultCharCollection.ToList();
+ expectedCollection.AddRange(userCollection);
+
+ Assert.AreEqual(expectedCollection.Count, actual.Count);
+ Assert.That(actual, Is.EquivalentTo(expectedCollection));
+ }
+
+ [Test]
+ public void Given_CharCollection_With_DefaultDisabled_ReturnsUserCollection()
+ {
+ var userCollection = new CharItem[]
+ {
+ new () { Char = "test", Replacement = "replace" },
+ new () { Char = "test2", Replacement = "replace2" }
+ };
+
+ var settings = new RequestHandlerSettings { UserDefinedCharCollection = userCollection, EnableDefaultCharReplacements = false };
+ var actual = settings.GetCharReplacements().ToList();
+
+ Assert.AreEqual(userCollection.Length, actual.Count);
+ Assert.That(actual, Is.EquivalentTo(userCollection));
+ }
+
+ [Test]
+ public void Given_CharCollection_That_OverridesDefaultValues_ReturnsReplacements()
+ {
+ var userCollection = new CharItem[]
+ {
+ new () { Char = "%", Replacement = "percent" },
+ new () { Char = ".", Replacement = "dot" }
+ };
+
+ var settings = new RequestHandlerSettings { UserDefinedCharCollection = userCollection };
+ var actual = settings.GetCharReplacements().ToList();
+
+ Assert.AreEqual(RequestHandlerSettings.DefaultCharCollection.Length, actual.Count);
+
+ Assert.That(actual, Has.Exactly(1).Matches(x => x.Char == "%" && x.Replacement == "percent"));
+ Assert.That(actual, Has.Exactly(1).Matches(x => x.Char == "." && x.Replacement == "dot"));
+ Assert.That(actual, Has.Exactly(0).Matches(x => x.Char == "%" && x.Replacement == string.Empty));
+ Assert.That(actual, Has.Exactly(0).Matches(x => x.Char == "." && x.Replacement == string.Empty));
+ }
+
+ [Test]
+ public void Given_CharCollection_That_OverridesDefaultValues_And_ContainsNew_ReturnsMergedWithReplacements()
+ {
+ var userCollection = new CharItem[]
+ {
+ new () { Char = "%", Replacement = "percent" },
+ new () { Char = ".", Replacement = "dot" },
+ new () { Char = "new", Replacement = "new" }
+ };
+
+ var settings = new RequestHandlerSettings { UserDefinedCharCollection = userCollection };
+ var actual = settings.GetCharReplacements().ToList();
+
+ // Add 1 to the length, because we're expecting to only add one new one
+ Assert.AreEqual(RequestHandlerSettings.DefaultCharCollection.Length + 1, actual.Count);
+
+ Assert.That(actual, Has.Exactly(1).Matches(x => x.Char == "%" && x.Replacement == "percent"));
+ Assert.That(actual, Has.Exactly(1).Matches(x => x.Char == "." && x.Replacement == "dot"));
+ Assert.That(actual, Has.Exactly(1).Matches(x => x.Char == "new" && x.Replacement == "new"));
+ Assert.That(actual, Has.Exactly(0).Matches(x => x.Char == "%" && x.Replacement == string.Empty));
+ Assert.That(actual, Has.Exactly(0).Matches(x => x.Char == "." && x.Replacement == string.Empty));
+ }
+ }
+}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs
index 6f9ee481cc..b686aee278 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System;
using System.Diagnostics;
using System.Linq;
using System.Text;
@@ -19,7 +20,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper
{
var requestHandlerSettings = new RequestHandlerSettings()
{
- CharCollection = Enumerable.Empty(),
+ CharCollection = Array.Empty(),
+ EnableDefaultCharReplacements = false,
ConvertUrlsToAscii = "false"
};
@@ -45,7 +47,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper
{
var requestHandlerSettings = new RequestHandlerSettings()
{
- CharCollection = Enumerable.Empty(),
+ CharCollection = Array.Empty(),
+ EnableDefaultCharReplacements = false,
ConvertUrlsToAscii = "false"
};
@@ -339,7 +342,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper
{
var requestHandlerSettings = new RequestHandlerSettings()
{
- CharCollection = Enumerable.Empty(),
+ CharCollection = Array.Empty(),
+ EnableDefaultCharReplacements = false,
ConvertUrlsToAscii = "false"
};