From 845502a9616844f15f8d82c6115a448fe1e8f3f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Wed, 14 Jun 2023 13:08:17 +0200 Subject: [PATCH 01/12] Add Thumbnail to CommonContentDisplayTypes. --- .../Contents/CommonContentDisplayTypes.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs index c9c08ce6..10f3bea2 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs @@ -11,4 +11,5 @@ public static class CommonContentDisplayTypes public const string Detail = nameof(Detail); public const string Summary = nameof(Summary); public const string SummaryAdmin = nameof(SummaryAdmin); + public const string Thumbnail = nameof(Thumbnail); } From 32b36c90ea9d8fe1a53af7da138ebfbd417d3036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Mon, 19 Jun 2023 13:11:41 +0200 Subject: [PATCH 02/12] Add NavigationItemBuilder.SiteSettings extension. --- .../Navigation/NavigationItemBuilderExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs index 1d83f587..048f287b 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs @@ -60,4 +60,7 @@ public static NavigationBuilder AddSeparator(this NavigationBuilder builder, ISt /// public static NavigationBuilder AddLabel(this NavigationBuilder builder, LocalizedString label) => builder.Add(label, subMenu => subMenu.Url("#").AddClass("disabled menuWidget__link_label")); + + public static NavigationItemBuilder SiteSettings(this NavigationItemBuilder builder, string groupId) => + builder.Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId }); } From 87d2ce23db83a76ccdafba4d01722fd2dd86088a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Mon, 19 Jun 2023 13:21:18 +0200 Subject: [PATCH 03/12] Add docs. --- .../Navigation/NavigationItemBuilderExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs index 048f287b..5b71a0a2 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs @@ -61,6 +61,9 @@ public static NavigationBuilder AddSeparator(this NavigationBuilder builder, ISt public static NavigationBuilder AddLabel(this NavigationBuilder builder, LocalizedString label) => builder.Add(label, subMenu => subMenu.Url("#").AddClass("disabled menuWidget__link_label")); + /// + /// Adds a link to the menu item pointing to the site settings page identified by the . + /// public static NavigationItemBuilder SiteSettings(this NavigationItemBuilder builder, string groupId) => builder.Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId }); } From cbbf70eee1abd8dc11c445e4972013a35a5afbbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Tue, 20 Jun 2023 10:59:51 +0200 Subject: [PATCH 04/12] Move IEmailAndPasswordValidator to HL. --- .../Validation/EmailAndPasswordValidator.cs | 46 +++++++++++++++++++ .../Validation/IEmailAndPasswordValidator.cs | 21 +++++++++ 2 files changed, 67 insertions(+) create mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Validation/EmailAndPasswordValidator.cs create mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Validation/IEmailAndPasswordValidator.cs diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Validation/EmailAndPasswordValidator.cs b/Lombiq.HelpfulLibraries.OrchardCore/Validation/EmailAndPasswordValidator.cs new file mode 100644 index 00000000..cc5d9663 --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Validation/EmailAndPasswordValidator.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Localization; +using OrchardCore.Email; +using OrchardCore.Users; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulLibraries.OrchardCore.Validation; + +public class EmailAndPasswordValidator : IEmailAndPasswordValidator +{ + private readonly IEmailAddressValidator _emailAddressValidator; + private readonly UserManager _userManager; + private readonly IStringLocalizer T; + public EmailAndPasswordValidator( + IEmailAddressValidator emailAddressValidator, + UserManager userManager, + IStringLocalizer stringLocalizer) + { + _emailAddressValidator = emailAddressValidator; + _userManager = userManager; + T = stringLocalizer; + } + + public Task> ValidateEmailAsync(string email) => + Task.FromResult(_emailAddressValidator.Validate(email) + ? Enumerable.Empty() + : new[] { T["Invalid email address."] }); + + public async Task> ValidatePasswordAsync(string password) + { + var errors = new List(); + if (password == null) return errors; + + foreach (var passwordValidator in _userManager.PasswordValidators) + { + var result = await passwordValidator.ValidateAsync(_userManager, user: null, password); + + if (result.Succeeded) continue; + errors.AddRange(result.Errors.Select(error => T[error.Description])); + } + + return errors; + } +} diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Validation/IEmailAndPasswordValidator.cs b/Lombiq.HelpfulLibraries.OrchardCore/Validation/IEmailAndPasswordValidator.cs new file mode 100644 index 00000000..bc6bd600 --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Validation/IEmailAndPasswordValidator.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Localization; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulLibraries.OrchardCore.Validation; + +/// +/// A service for returning validation errors for email of password. +/// +public interface IEmailAndPasswordValidator +{ + /// + /// Validates the provided and returns any validation errors as a localized string. + /// + Task> ValidateEmailAsync(string email); + + /// + /// Validates the provided and returns any validation errors as a localized string. + /// + Task> ValidatePasswordAsync(string password); +} From e4a4379cf0bae86457d1051acd1686a789f6c4d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Tue, 20 Jun 2023 11:00:00 +0200 Subject: [PATCH 05/12] Add documentation. --- Lombiq.HelpfulLibraries.OrchardCore/Docs/Validation.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Docs/Validation.md diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Validation.md b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Validation.md new file mode 100644 index 00000000..494a017b --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Validation.md @@ -0,0 +1,5 @@ +# Lombiq Helpful Libraries - Orchard Core Libraries - Validation for Orchard Core + +## `IEmailAndPasswordValidator` + +A service for returning validation errors for email of password. Use its `ValidateEmailAsync` method to get email format validation errors, and its `ValidatePasswordAsync` to get validation errors from the registered `IPasswordValidator` instances. From f6724008e553d04eed540d12552e4cc832c4278e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Tue, 20 Jun 2023 18:59:23 +0200 Subject: [PATCH 06/12] Collection valued dictionary methods. --- .../Extensions/DictionaryExtensions.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs index 1038e497..6eb3ae57 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; @@ -165,4 +166,55 @@ public static void AddToList( /// public static IReadOnlyDictionary ToReadOnly(this IDictionary dictionary) => dictionary as IReadOnlyDictionary ?? new Dictionary(dictionary); + + /// + /// Creates a new dictionary based on by taking each entry's value, and if it's not + /// and has at least one not item then that item is added under the + /// same key. + /// + [SuppressMessage( + "Design", + "MA0016:Prefer returning collection abstraction instead of implementation", + Justification = "Better compatibility.")] + public static Dictionary WithFirstValues( + this IEnumerable> dictionary) + where TValues : IEnumerable + { + var result = new Dictionary(); + + foreach (var (key, values) in dictionary) + { + if (values != null && values.FirstOrDefault() is { } value) + { + result[key] = value; + } + } + + return result; + } + + /// + /// Creates a new dictionary based on where each value is a new instance of + /// which is an of . If the value + /// is not then it's added to the list. + /// + [SuppressMessage( + "Design", + "MA0016:Prefer returning collection abstraction instead of implementation", + Justification = "Better compatibility.")] + public static Dictionary ToListValuedDictionary( + this IEnumerable> dictionary) + where TValues : IList, new() + { + var result = new Dictionary(); + + foreach (var (key, value) in dictionary) + { + var list = new TValues(); + if (value != null) list.Add(value); + result[key] = list; + } + + return result; + } } From 089fa8bc4afcbd40284fef1ad92434115a9d2506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Tue, 20 Jun 2023 19:18:20 +0200 Subject: [PATCH 07/12] Add selectors. --- .../Extensions/DictionaryExtensions.cs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs index 6eb3ae57..c5eb75fb 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs @@ -177,16 +177,27 @@ public static IReadOnlyDictionary ToReadOnly(this ID "MA0016:Prefer returning collection abstraction instead of implementation", Justification = "Better compatibility.")] public static Dictionary WithFirstValues( - this IEnumerable> dictionary) + this IEnumerable> dictionary, + Func select = null) where TValues : IEnumerable { var result = new Dictionary(); foreach (var (key, values) in dictionary) { - if (values != null && values.FirstOrDefault() is { } value) + if (select == null) { - result[key] = value; + if (values != null && values.FirstOrDefault() is { } value) + { + result[key] = value; + } + } + else + { + if (select(values) is { } value) + { + result[key] = value; + } } } @@ -203,13 +214,20 @@ public static Dictionary WithFirstValues( "MA0016:Prefer returning collection abstraction instead of implementation", Justification = "Better compatibility.")] public static Dictionary ToListValuedDictionary( - this IEnumerable> dictionary) + this IEnumerable> dictionary, + Func select = null) where TValues : IList, new() { var result = new Dictionary(); foreach (var (key, value) in dictionary) { + if (select != null) + { + result[key] = select(value); + continue; + } + var list = new TValues(); if (value != null) list.Add(value); result[key] = list; From 7bed9e454a0f76a14578e00a8e93950a09496811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Wed, 21 Jun 2023 16:28:44 +0200 Subject: [PATCH 08/12] Add LiquidContentDisplayService. --- .../Liquid/ILiquidContentDisplayService.cs | 33 +++++++++++++ .../Liquid/LiquidContentDisplayService.cs | 48 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Liquid/ILiquidContentDisplayService.cs create mode 100644 Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Liquid/ILiquidContentDisplayService.cs b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/ILiquidContentDisplayService.cs new file mode 100644 index 00000000..6d55e5da --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/ILiquidContentDisplayService.cs @@ -0,0 +1,33 @@ +using Fluid.Values; +using OrchardCore.DisplayManagement; +using OrchardCore.DisplayManagement.Implementation; +using System; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulLibraries.OrchardCore.Liquid; + +/// +/// Service that displays shapes as HTML in a . +/// +public interface ILiquidContentDisplayService +{ + /// + /// Creates new instances of a dynamic shape object and renders it to HTML in a . + /// + ValueTask DisplayShapeAsync(IShape shape); + + /// + /// Creates new instances of a typed shape object and renders it to HTML in a . + /// + /// The type to instantiate. + ValueTask DisplayNewAsync(string shapeType, Action initialize); + + /// + /// Displays an already instantiated as a . + /// + ValueTask DisplayNewAsync( + string shapeType, + Func> shapeFactory, + Action creating = null, + Action created = null); +} \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs new file mode 100644 index 00000000..f5617e17 --- /dev/null +++ b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs @@ -0,0 +1,48 @@ +using Fluid; +using Fluid.Values; +using Microsoft.AspNetCore.Html; +using OrchardCore.DisplayManagement; +using OrchardCore.DisplayManagement.Implementation; +using System; +using System.IO; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulLibraries.OrchardCore.Liquid; + +public class LiquidContentDisplayService : ILiquidContentDisplayService +{ + private readonly IDisplayHelper _displayHelper; + private readonly IShapeFactory _shapeFactory; + public LiquidContentDisplayService(IDisplayHelper displayHelper, IShapeFactory shapeFactory) + { + _displayHelper = displayHelper; + _shapeFactory = shapeFactory; + } + + public async ValueTask DisplayNewAsync( + string shapeType, + Func> shapeFactory, + Action creating = null, + Action created = null) + { + var shape = await _shapeFactory.CreateAsync(shapeType, shapeFactory, creating, created); + return await DisplayShapeAsync(shape); + } + + public async ValueTask DisplayNewAsync(string shapeType, Action initialize) + { + var shape = await _shapeFactory.CreateAsync(shapeType, initialize); + return await DisplayShapeAsync(shape); + } + + public async ValueTask DisplayShapeAsync(IShape shape) + { + var content = await _displayHelper.ShapeExecuteAsync(shape); + + await using var stringWriter = new StringWriter(); + content.WriteTo(stringWriter, HtmlEncoder.Default); + + return FluidValue.Create(new HtmlString(stringWriter.ToString()), TemplateOptions.Default); + } +} From a487ed9b786c8a2dc585a5675d8a93ad237ab170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Wed, 21 Jun 2023 18:14:06 +0200 Subject: [PATCH 09/12] Return HtmlContentValue instead. --- .../Liquid/LiquidContentDisplayService.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs index f5617e17..5cefd7ac 100644 --- a/Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs +++ b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs @@ -1,11 +1,8 @@ -using Fluid; -using Fluid.Values; -using Microsoft.AspNetCore.Html; +using Fluid.Values; using OrchardCore.DisplayManagement; using OrchardCore.DisplayManagement.Implementation; +using OrchardCore.DisplayManagement.Liquid; using System; -using System.IO; -using System.Text.Encodings.Web; using System.Threading.Tasks; namespace Lombiq.HelpfulLibraries.OrchardCore.Liquid; @@ -39,10 +36,6 @@ public async ValueTask DisplayNewAsync(string shapeType, Act public async ValueTask DisplayShapeAsync(IShape shape) { var content = await _displayHelper.ShapeExecuteAsync(shape); - - await using var stringWriter = new StringWriter(); - content.WriteTo(stringWriter, HtmlEncoder.Default); - - return FluidValue.Create(new HtmlString(stringWriter.ToString()), TemplateOptions.Default); + return new HtmlContentValue(content); } } From 1a94f5777735129e73e66f4951f2cac9beecd604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Thu, 22 Jun 2023 15:31:24 +0200 Subject: [PATCH 10/12] HTML string concatenation. --- .../LocalizedHtmlStringExtensions.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs index 5140451e..0ce75535 100644 --- a/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs +++ b/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Html; using Newtonsoft.Json; using System.IO; +using System.Linq; +using System.Text; using System.Text.Encodings.Web; namespace Microsoft.AspNetCore.Mvc.Localization; @@ -29,4 +31,29 @@ public static string Html(this IHtmlContent htmlContent) htmlContent.WriteTo(stringWriter, HtmlEncoder.Default); return stringWriter.ToString(); } + + /// + /// Concatenates the localized HTML string with the provided HTML + /// content. This is suitable for joining individually localizable HTML strings. + /// + public static LocalizedHtmlString Concat(this LocalizedHtmlString first, params IHtmlContent[] other) + { + if (other.Length == 0) return first; + + var builder = new StringBuilder(first.Html()); + foreach (var content in other) builder.Append(content.Html()); + var html = builder.ToString(); + + return new LocalizedHtmlString(html, html); + } + + public static LocalizedHtmlString Join(this IHtmlContent separator, params LocalizedHtmlString[] items) + { + if (items.Length == 0) return null; + + var first = items[0]; + var other = items.Skip(1).SelectMany(item => new[] { separator, item }).ToArray(); + + return first.Concat(other); + } } From b494fb48bdbf6476aca8ecf3515e7fc99e140c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Thu, 22 Jun 2023 16:29:39 +0200 Subject: [PATCH 11/12] Case insensitive dictionary. --- .../Extensions/DictionaryExtensions.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs index c5eb75fb..afa458cd 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs @@ -235,4 +235,32 @@ public static Dictionary ToListValuedDictionary + /// Creates a new string keyed dictionary where the key is compared in a case-insensitive manner. + /// + public static IDictionary ToDictionaryIgnoreCase( + this IEnumerable> source) => + new Dictionary(source, StringComparer.OrdinalIgnoreCase); + + /// + /// Creates a new string keyed dictionary where the key is compared in a case-insensitive manner. + /// + public static IDictionary ToDictionaryIgnoreCase( + this IEnumerable source, + Func keySelector) => + new Dictionary( + source.Select(item => new KeyValuePair(keySelector(item), item)), + StringComparer.OrdinalIgnoreCase); + + /// + /// Creates a new string keyed dictionary where the key is compared in a case-insensitive manner. + /// + public static IDictionary ToDictionaryIgnoreCase( + this IEnumerable source, + Func keySelector, + Func valueSelector) => + new Dictionary( + source.Select(item => new KeyValuePair(keySelector(item), valueSelector(item))), + StringComparer.OrdinalIgnoreCase); } From 721729144b28dbfc5a0b05b211fd44a80475ee9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20El-Saig?= Date: Thu, 22 Jun 2023 16:57:48 +0200 Subject: [PATCH 12/12] code cleanup --- .../Extensions/DictionaryExtensions.cs | 4 ++-- .../Liquid/ILiquidContentDisplayService.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs index afa458cd..a3f081e4 100644 --- a/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs +++ b/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs @@ -187,7 +187,7 @@ public static Dictionary WithFirstValues( { if (select == null) { - if (values != null && values.FirstOrDefault() is { } value) + if (values is not null && values.FirstOrDefault() is { } value) { result[key] = value; } @@ -229,7 +229,7 @@ public static Dictionary ToListValuedDictionary DisplayNewAsync( Func> shapeFactory, Action creating = null, Action created = null); -} \ No newline at end of file +}