Merge pull request #11386 from matthewcare/temp-11381

Request Handler Settings for character replacement
This commit is contained in:
Nikolaj Geisle
2022-01-05 14:56:11 +01:00
committed by GitHub
9 changed files with 298 additions and 59 deletions

View File

@@ -0,0 +1,17 @@
using Umbraco.Cms.Core.Configuration.UmbracoSettings;
namespace Umbraco.Cms.Core.Configuration.Models
{
public class CharItem : IChar
{
/// <summary>
/// The character to replace
/// </summary>
public string Char { get; set; }
/// <summary>
/// The replacement character
/// </summary>
public string Replacement { get; set; }
}
}

View File

@@ -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 }
};
/// <summary>
@@ -67,41 +69,21 @@ namespace Umbraco.Cms.Core.Configuration.Models
/// </summary>
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<string>("Char"),
//// Replacement = x.GetValue<string>("Replacement"),
//// }).ToArray();
//// if (collection.Any() || _configuration.GetSection("Prefix").GetChildren().Any(x =>
//// x.Key.Equals("CharCollection", StringComparison.OrdinalIgnoreCase)))
//// {
//// return collection;
//// }
//// return DefaultCharCollection;
/// <summary>
/// Disable all default character replacements
/// </summary>
[DefaultValue(StaticEnableDefaultCharReplacements)]
public bool EnableDefaultCharReplacements { get; set; } = StaticEnableDefaultCharReplacements;
/// <summary>
/// Gets or sets a value for the default character collection for replacements.
/// Add additional character replacements, or override defaults
/// </summary>
/// WB-TODO
[Obsolete("Use the GetCharReplacements extension method in the Umbraco.Extensions namespace instead. Scheduled for removal in V11")]
public IEnumerable<IChar> CharCollection { get; set; } = DefaultCharCollection;
/// <summary>
/// Defines a character replacement.
/// Add additional character replacements, or override defaults
/// </summary>
public class CharItem : IChar
{
/// <inheritdoc/>
public string Char { get; set; }
/// <inheritdoc/>
public string Replacement { get; set; }
}
public IEnumerable<CharItem> UserDefinedCharCollection { get; set; }
}
}

View File

@@ -0,0 +1,41 @@
using System.Collections.Generic;
using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Core.Configuration.UmbracoSettings
{
public class CharacterReplacementEqualityComparer : IEqualityComparer<IChar>
{
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);
}
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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<LegacyPasswordMigrationSettings>()
.AddUmbracoOptions<PackageMigrationSettings>();
builder.Services.Configure<RequestHandlerSettings>(options => options.MergeReplacements(builder.Config));
return builder;
}
}

View File

@@ -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
{
/// <summary>
/// Get concatenated user and default character replacements
/// taking into account <see cref="RequestHandlerSettings.EnableDefaultCharReplacements"/>
/// </summary>
public static class RequestHandlerSettingsExtension
{
/// <summary>
/// Get concatenated user and default character replacements
/// taking into account <see cref="RequestHandlerSettings.EnableDefaultCharReplacements"/>
/// </summary>
public static IEnumerable<CharItem> GetCharReplacements(this RequestHandlerSettings requestHandlerSettings)
{
if (requestHandlerSettings.EnableDefaultCharReplacements is false)
{
return requestHandlerSettings.UserDefinedCharCollection ?? Enumerable.Empty<CharItem>();
}
if (requestHandlerSettings.UserDefinedCharCollection == null || requestHandlerSettings.UserDefinedCharCollection.Any() is false)
{
return RequestHandlerSettings.DefaultCharCollection;
}
return MergeUnique(requestHandlerSettings.UserDefinedCharCollection, RequestHandlerSettings.DefaultCharCollection);
}
/// <summary>
/// Merges CharCollection and UserDefinedCharCollection, prioritizing UserDefinedCharCollection
/// </summary>
internal static void MergeReplacements(this RequestHandlerSettings requestHandlerSettings, IConfiguration configuration)
{
string sectionKey = $"{Constants.Configuration.ConfigRequestHandler}:";
IEnumerable<CharItem> charCollection = GetReplacements(
configuration,
$"{sectionKey}{nameof(RequestHandlerSettings.CharCollection)}");
IEnumerable<CharItem> userDefinedCharCollection = GetReplacements(
configuration,
$"{sectionKey}{nameof(requestHandlerSettings.UserDefinedCharCollection)}");
IEnumerable<CharItem> mergedCollection = MergeUnique(userDefinedCharCollection, charCollection);
requestHandlerSettings.UserDefinedCharCollection = mergedCollection;
}
private static IEnumerable<CharItem> GetReplacements(IConfiguration configuration, string key)
{
var replacements = new List<CharItem>();
IEnumerable<IConfigurationSection> config = configuration.GetSection(key).GetChildren();
foreach (IConfigurationSection section in config)
{
var @char = section.GetValue<string>(nameof(CharItem.Char));
var replacement = section.GetValue<string>(nameof(CharItem.Replacement));
replacements.Add(new CharItem { Char = @char, Replacement = replacement });
}
return replacements;
}
/// <summary>
/// Merges two IEnumerable of CharItem without any duplicates, items in priorityReplacements will override those in alternativeReplacements
/// </summary>
private static IEnumerable<CharItem> MergeUnique(
IEnumerable<CharItem> priorityReplacements,
IEnumerable<CharItem> 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<CharItem>(
alternativeReplacementsList,
new CharacterReplacementEqualityComparer());
}
}
}

View File

@@ -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
/// <returns>The short string helper.</returns>
public DefaultShortStringHelperConfig WithDefault(RequestHandlerSettings requestHandlerSettings)
{
UrlReplaceCharacters = requestHandlerSettings.CharCollection
IEnumerable<IChar> charCollection = requestHandlerSettings.GetCharReplacements();
UrlReplaceCharacters = charCollection
.Where(x => string.IsNullOrEmpty(x.Char) == false)
.ToDictionary(x => x.Char, x => x.Replacement);

View File

@@ -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<CharItem>(x => x.Char == "%" && x.Replacement == "percent"));
Assert.That(actual, Has.Exactly(1).Matches<CharItem>(x => x.Char == "." && x.Replacement == "dot"));
Assert.That(actual, Has.Exactly(0).Matches<CharItem>(x => x.Char == "%" && x.Replacement == string.Empty));
Assert.That(actual, Has.Exactly(0).Matches<CharItem>(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<CharItem>(x => x.Char == "%" && x.Replacement == "percent"));
Assert.That(actual, Has.Exactly(1).Matches<CharItem>(x => x.Char == "." && x.Replacement == "dot"));
Assert.That(actual, Has.Exactly(1).Matches<CharItem>(x => x.Char == "new" && x.Replacement == "new"));
Assert.That(actual, Has.Exactly(0).Matches<CharItem>(x => x.Char == "%" && x.Replacement == string.Empty));
Assert.That(actual, Has.Exactly(0).Matches<CharItem>(x => x.Char == "." && x.Replacement == string.Empty));
}
}
}

View File

@@ -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<IChar>(),
CharCollection = Array.Empty<CharItem>(),
EnableDefaultCharReplacements = false,
ConvertUrlsToAscii = "false"
};
@@ -45,7 +47,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper
{
var requestHandlerSettings = new RequestHandlerSettings()
{
CharCollection = Enumerable.Empty<IChar>(),
CharCollection = Array.Empty<CharItem>(),
EnableDefaultCharReplacements = false,
ConvertUrlsToAscii = "false"
};
@@ -339,7 +342,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper
{
var requestHandlerSettings = new RequestHandlerSettings()
{
CharCollection = Enumerable.Empty<IChar>(),
CharCollection = Array.Empty<CharItem>(),
EnableDefaultCharReplacements = false,
ConvertUrlsToAscii = "false"
};