From 793526bf1c600034ff2222f2c1fffd5c8bceee38 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Thu, 8 Sep 2022 21:17:03 +0300 Subject: [PATCH] React to localization workaround removal for data annotations attributes (#12333) --- .../ViewModels/SmtpSettingsViewModel.cs | 18 +------ .../OrchardCore.OpenId/UrlAttribute.cs | 27 +++++++++++ .../CreateOpenIdApplicationViewModel.cs | 43 ++++++++--------- .../EditOpenIdApplicationViewModel.cs | 43 ++++++++--------- .../ViewModels/ChangeEmailViewModel.cs | 22 ++------- .../EditUserInformationViewModel.cs | 18 +------ .../ViewModels/ForgotPasswordViewModel.cs | 24 ++-------- .../ViewModels/LinkExternalLoginViewModel.cs | 15 +----- .../ApplicationControllerTests.cs | 48 ++++++++++++------- 9 files changed, 108 insertions(+), 150 deletions(-) create mode 100644 src/OrchardCore.Modules/OrchardCore.OpenId/UrlAttribute.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/SmtpSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/SmtpSettingsViewModel.cs index 7d27792e686..2b1494681eb 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/SmtpSettingsViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/SmtpSettingsViewModel.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; namespace OrchardCore.Email.ViewModels { - public class SmtpSettingsViewModel : IValidatableObject + public class SmtpSettingsViewModel { [Required(AllowEmptyStrings = false)] public string To { get; set; } + [EmailAddress(ErrorMessage = "Invalid Email.")] public string Sender { get; set; } public string Bcc { get; set; } @@ -22,16 +19,5 @@ public class SmtpSettingsViewModel : IValidatableObject public string Subject { get; set; } public string Body { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - var emailAddressValidator = validationContext.GetService(); - var S = validationContext.GetService>(); - - if (!String.IsNullOrWhiteSpace(Sender) && !emailAddressValidator.Validate(Sender)) - { - yield return new ValidationResult(S["Invalid Email."], new[] { nameof(Sender) }); - } - } } } diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/UrlAttribute.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/UrlAttribute.cs new file mode 100644 index 00000000000..d66c205867b --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/UrlAttribute.cs @@ -0,0 +1,27 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace OrchardCore.OpenId; + +public class UrlAttribute : ValidationAttribute +{ + private static readonly char[] _urlSeparators = new[] { ' ', ',' }; + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + if (value != null) + { + var urls = value.ToString(); + + foreach (var url in urls.Split(_urlSeparators, StringSplitOptions.RemoveEmptyEntries)) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || !uri.IsWellFormedOriginalString()) + { + return new ValidationResult(ErrorMessage, new[] { urls }); + } + } + } + + return ValidationResult.Success; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/ViewModels/CreateOpenIdApplicationViewModel.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/ViewModels/CreateOpenIdApplicationViewModel.cs index a0d65eeb7fa..b35b8be656a 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/ViewModels/CreateOpenIdApplicationViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/ViewModels/CreateOpenIdApplicationViewModel.cs @@ -1,13 +1,9 @@ -using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; namespace OrchardCore.OpenId.ViewModels { - public class CreateOpenIdApplicationViewModel : IValidatableObject + public class CreateOpenIdApplicationViewModel { [Required] public string ClientId { get; set; } @@ -15,26 +11,41 @@ public class CreateOpenIdApplicationViewModel : IValidatableObject [Required] public string DisplayName { get; set; } + [Url(ErrorMessage = "{0} is not well-formed")] public string RedirectUris { get; set; } + + [Url(ErrorMessage = "{0} is not well-formed")] public string PostLogoutRedirectUris { get; set; } + public string Type { get; set; } + public string ConsentType { get; set; } + public string ClientSecret { get; set; } + public List RoleEntries { get; } = new List(); + public List ScopeEntries { get; } = new List(); + public bool AllowPasswordFlow { get; set; } + public bool AllowClientCredentialsFlow { get; set; } + public bool AllowAuthorizationCodeFlow { get; set; } + public bool AllowRefreshTokenFlow { get; set; } + public bool AllowHybridFlow { get; set; } + public bool AllowImplicitFlow { get; set; } + public bool AllowLogoutEndpoint { get; set; } + public bool AllowIntrospectionEndpoint { get; set; } + public bool AllowRevocationEndpoint { get; set; } - public bool RequireProofKeyForCodeExchange { get; set; } - public IEnumerable Validate(ValidationContext validationContext) => ValidateUrls(validationContext, nameof(RedirectUris), RedirectUris) - .Union(ValidateUrls(validationContext, nameof(PostLogoutRedirectUris), PostLogoutRedirectUris)); + public bool RequireProofKeyForCodeExchange { get; set; } public class RoleEntry { @@ -46,21 +57,5 @@ public class ScopeEntry public string Name { get; set; } public bool Selected { get; set; } } - - private IEnumerable ValidateUrls(ValidationContext context, string memberName, string member) - { - if (member != null) - { - var S = context.GetRequiredService>(); - - foreach (var url in member.Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries)) - { - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || !uri.IsWellFormedOriginalString()) - { - yield return new ValidationResult(S["{0} is not well-formed", url], new[] { memberName }); - } - } - } - } } } diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/ViewModels/EditOpenIdApplicationViewModel.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/ViewModels/EditOpenIdApplicationViewModel.cs index d222e40202b..2da1208f063 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/ViewModels/EditOpenIdApplicationViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/ViewModels/EditOpenIdApplicationViewModel.cs @@ -1,14 +1,10 @@ -using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; namespace OrchardCore.OpenId.ViewModels { - public class EditOpenIdApplicationViewModel : IValidatableObject + public class EditOpenIdApplicationViewModel { [HiddenInput] public string Id { get; set; } @@ -19,26 +15,41 @@ public class EditOpenIdApplicationViewModel : IValidatableObject [Required] public string DisplayName { get; set; } + [Url(ErrorMessage = "{0} is not well-formed")] public string RedirectUris { get; set; } + + [Url(ErrorMessage = "{0} is not well-formed")] public string PostLogoutRedirectUris { get; set; } + public string Type { get; set; } + public string ConsentType { get; set; } + public string ClientSecret { get; set; } + public List RoleEntries { get; } = new List(); + public List ScopeEntries { get; } = new List(); + public bool AllowPasswordFlow { get; set; } + public bool AllowClientCredentialsFlow { get; set; } + public bool AllowAuthorizationCodeFlow { get; set; } + public bool AllowRefreshTokenFlow { get; set; } + public bool AllowHybridFlow { get; set; } + public bool AllowImplicitFlow { get; set; } + public bool AllowLogoutEndpoint { get; set; } + public bool AllowIntrospectionEndpoint { get; set; } + public bool AllowRevocationEndpoint { get; set; } - public bool RequireProofKeyForCodeExchange { get; set; } - public IEnumerable Validate(ValidationContext validationContext) => ValidateUrls(validationContext, nameof(RedirectUris), RedirectUris) - .Union(ValidateUrls(validationContext, nameof(PostLogoutRedirectUris), PostLogoutRedirectUris)); + public bool RequireProofKeyForCodeExchange { get; set; } public class RoleEntry { @@ -51,21 +62,5 @@ public class ScopeEntry public string Name { get; set; } public bool Selected { get; set; } } - - private IEnumerable ValidateUrls(ValidationContext context, string memberName, string member) - { - if (member != null) - { - var S = context.GetRequiredService>(); - - foreach (var url in member.Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries)) - { - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || !uri.IsWellFormedOriginalString()) - { - yield return new ValidationResult(S["{0} is not well-formed", url], new[] { memberName }); - } - } - } - } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/ChangeEmailViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/ChangeEmailViewModel.cs index 25e3a89885d..e0ef7157b25 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/ChangeEmailViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/ChangeEmailViewModel.cs @@ -1,27 +1,11 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; -using OrchardCore.Email; namespace OrchardCore.Users.ViewModels { - public class ChangeEmailViewModel : IValidatableObject + public class ChangeEmailViewModel { + [Required(ErrorMessage = "Email is required.")] + [Email.EmailAddress(ErrorMessage = "Invalid Email.")] public string Email { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - var emailAddressValidator = validationContext.GetService(); - var S = validationContext.GetService>(); - if (string.IsNullOrWhiteSpace(Email)) - { - yield return new ValidationResult(S["Email is required."], new[] { nameof(Email) }); - } - else if (!emailAddressValidator.Validate(Email)) - { - yield return new ValidationResult(S["Invalid Email."], new[] { nameof(Email) }); - } - } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserInformationViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserInformationViewModel.cs index ba5d094b561..3fdb8a721cc 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserInformationViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserInformationViewModel.cs @@ -1,32 +1,18 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; -using OrchardCore.Email; namespace OrchardCore.Users.ViewModels { - public class EditUserInformationViewModel : IValidatableObject + public class EditUserInformationViewModel { [Required] public string UserName { get; set; } [Required] + [Email.EmailAddress(ErrorMessage = "Invalid Email.")] public string Email { get; set; } [BindNever] public bool IsEditingDisabled { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - var emailAddressValidator = validationContext.GetService(); - var S = validationContext.GetService>(); - - if (!string.IsNullOrEmpty(Email) && !emailAddressValidator.Validate(Email)) - { - yield return new ValidationResult(S["Invalid Email."], new[] { "Email" }); - } - } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/ForgotPasswordViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/ForgotPasswordViewModel.cs index 8e0f2acc05c..eca1cc288ee 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/ForgotPasswordViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/ForgotPasswordViewModel.cs @@ -1,29 +1,11 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; -using OrchardCore.Email; namespace OrchardCore.Users.ViewModels { - public class ForgotPasswordViewModel : IValidatableObject + public class ForgotPasswordViewModel { - [Required] + [Required(ErrorMessage = "Email is required.")] + [Email.EmailAddress(ErrorMessage = "Invalid Email.")] public string Email { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - var emailAddressValidator = validationContext.GetService(); - var S = validationContext.GetService>(); - - if (string.IsNullOrWhiteSpace(Email)) - { - yield return new ValidationResult(S["Email is required."], new[] { nameof(Email) }); - } - else if (!emailAddressValidator.Validate(Email)) - { - yield return new ValidationResult(S["Invalid Email."], new[] { nameof(Email) }); - } - } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/LinkExternalLoginViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/LinkExternalLoginViewModel.cs index f886dd37e57..381f0e1283e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/LinkExternalLoginViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/LinkExternalLoginViewModel.cs @@ -1,22 +1,11 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; namespace OrchardCore.Users.ViewModels { - public class LinkExternalLoginViewModel : IValidatableObject + public class LinkExternalLoginViewModel { [DataType(DataType.Password)] + [Required(ErrorMessage = "Password is required.")] public string Password { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - var S = validationContext.GetService>(); - if (string.IsNullOrWhiteSpace(Password)) - { - yield return new ValidationResult(S["Password is required"], new[] { nameof(Password) }); - } - } } } diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.OpenId/ApplicationControllerTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.OpenId/ApplicationControllerTests.cs index 3dfea387a2d..5d3b3a6a760 100644 --- a/test/OrchardCore.Tests/Modules/OrchardCore.OpenId/ApplicationControllerTests.cs +++ b/test/OrchardCore.Tests/Modules/OrchardCore.OpenId/ApplicationControllerTests.cs @@ -109,6 +109,7 @@ public async Task ConfidentionalClientNeedsSecret(string clientType, string clie [InlineData("http://localhost http://localhost:8080", true)] public async Task RedirectUrisAreValid(string uris, bool expectValidModel) { + // Arrange var controller = new ApplicationController( Mock.Of(), Mock.Of>(), @@ -118,27 +119,28 @@ public async Task RedirectUrisAreValid(string uris, bool expectValidModel) Mock.Of(), Mock.Of>(), Mock.Of(), - Mock.Of()); + Mock.Of()) + { + ControllerContext = CreateControllerContext() + }; - controller.ControllerContext = CreateControllerContext(); + var model = new CreateOpenIdApplicationViewModel + { + Type = OpenIddictConstants.ClientTypes.Public, + AllowAuthorizationCodeFlow = true, + ClientId = "123", + DisplayName = "Name", + RedirectUris = uris + }; - var model = new CreateOpenIdApplicationViewModel(); - model.Type = OpenIddictConstants.ClientTypes.Public; - model.AllowAuthorizationCodeFlow = true; - model.RedirectUris = uris; + ValidateControllerModel(controller, model); - var validationContext = new ValidationContext(model); - var localizerMock = new Mock>(); - localizerMock.Setup(x => x[It.IsAny(), It.IsAny()]) - .Returns((string name, object[] args) => new LocalizedString(name, string.Format(name, args))); - validationContext.InitializeServiceProvider((t) => localizerMock.Object); + // Act + var result = await controller.Create(model); - foreach (var validation in model.Validate(validationContext)) - { - controller.ModelState.AddModelError(validation.MemberNames.First(), validation.ErrorMessage); - } + // Assert + Assert.Equal(expectValidModel, controller.ModelState.IsValid); - var result = await controller.Create(model); if (expectValidModel) { Assert.IsType(result); @@ -147,7 +149,6 @@ public async Task RedirectUrisAreValid(string uris, bool expectValidModel) { Assert.IsType(result); } - Assert.Equal(expectValidModel, controller.ModelState.IsValid); } public Mock MockAuthorizationServiceMock() @@ -177,5 +178,18 @@ public Mock> MockStringLocalizer() return localizerMock; } + + private static void ValidateControllerModel(Controller controller, object model) + { + var results = new List(); + var context = new ValidationContext(model, null, null); + + Validator.TryValidateObject(model, context, results, true); + + foreach (var result in results) + { + controller.ModelState.AddModelError(result.MemberNames.First(), result.ErrorMessage); + } + } } }