// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc { /// /// A which configures Unobtrusive validation to send an Ajax request to the /// web site. The invoked action should return JSON indicating whether the value is valid. /// /// Does no server-side validation of the final form submission. [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class RemoteAttribute : ValidationAttribute, IClientModelValidator { private string _additionalFields = string.Empty; private string[] _additionalFieldsSplit = EmptyArray.Instance; private bool _checkedForLocalizer; private IStringLocalizer _stringLocalizer; /// /// Initializes a new instance of the class. /// /// /// Intended for subclasses that support URL generation with no route, action, or controller names. /// protected RemoteAttribute() : base(errorMessageAccessor: () => Resources.RemoteAttribute_RemoteValidationFailed) { RouteData = new RouteValueDictionary(); } /// /// Initializes a new instance of the class. /// /// /// The route name used when generating the URL where client should send a validation request. /// /// /// Finds the in any area of the application. /// public RemoteAttribute(string routeName) : this() { RouteName = routeName; } /// /// Initializes a new instance of the class. /// /// /// The action name used when generating the URL where client should send a validation request. /// /// /// The controller name used when generating the URL where client should send a validation request. /// /// /// /// If either or is null, uses the corresponding /// ambient value. /// /// Finds the in the current area. /// public RemoteAttribute(string action, string controller) : this() { if (action != null) { RouteData["action"] = action; } if (controller != null) { RouteData["controller"] = controller; } } /// /// Initializes a new instance of the class. /// /// /// The action name used when generating the URL where client should send a validation request. /// /// /// The controller name used when generating the URL where client should send a validation request. /// /// The name of the area containing the . /// /// /// If either or is null, uses the corresponding /// ambient value. /// /// If is null, finds the in the root area. /// Use the overload find the in /// the current area. Or explicitly pass the current area's name as the argument to /// this overload. /// public RemoteAttribute(string action, string controller, string areaName) : this(action, controller) { RouteData["area"] = areaName; } /// /// Gets or sets the HTTP method ("Get" or "Post") client should use when sending a validation /// request. /// public string HttpMethod { get; set; } /// /// Gets or sets the comma-separated names of fields the client should include in a validation request. /// public string AdditionalFields { get { return _additionalFields; } set { _additionalFields = value ?? string.Empty; _additionalFieldsSplit = SplitAndTrimPropertyNames(value) .Select(field => FormatPropertyForClientValidation(field)) .ToArray(); } } /// /// Gets the used when generating the URL where client should send a /// validation request. /// protected RouteValueDictionary RouteData { get; } /// /// Gets or sets the route name used when generating the URL where client should send a validation request. /// protected string RouteName { get; set; } /// /// Formats and for use in generated HTML. /// /// /// Name of the property associated with this instance. /// /// Comma-separated names of fields the client should include in a validation request. /// /// Excludes any whitespace from in the return value. /// Prefixes each field name in the return value with "*.". /// public string FormatAdditionalFieldsForClientValidation(string property) { if (string.IsNullOrEmpty(property)) { throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(property)); } var delimitedAdditionalFields = string.Join(",", _additionalFieldsSplit); if (!string.IsNullOrEmpty(delimitedAdditionalFields)) { delimitedAdditionalFields = "," + delimitedAdditionalFields; } var formattedString = FormatPropertyForClientValidation(property) + delimitedAdditionalFields; return formattedString; } /// /// Formats for use in generated HTML. /// /// One field name the client should include in a validation request. /// Name of a field the client should include in a validation request. /// Returns with a "*." prefix. public static string FormatPropertyForClientValidation(string property) { if (string.IsNullOrEmpty(property)) { throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(property)); } return "*." + property; } /// /// Returns the URL where the client should send a validation request. /// /// The used to generate the URL. /// The URL where the client should send a validation request. protected virtual string GetUrl(ClientModelValidationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var services = context.ActionContext.HttpContext.RequestServices; var factory = services.GetRequiredService(); var urlHelper = factory.GetUrlHelper(context.ActionContext); var url = urlHelper.RouteUrl(new UrlRouteContext() { RouteName = this.RouteName, Values = RouteData, }); if (url == null) { throw new InvalidOperationException(Resources.RemoteAttribute_NoUrlFound); } return url; } /// public override string FormatErrorMessage(string name) { return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name); } /// /// /// Always returns true since this does no validation itself. /// Related validations occur only when the client sends a validation request. /// public override bool IsValid(object value) { return true; } public virtual void AddValidation(ClientModelValidationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } MergeAttribute(context.Attributes, "data-val", "true"); CheckForLocalizer(context); var errorMessage = GetErrorMessage(context.ModelMetadata.GetDisplayName()); MergeAttribute(context.Attributes, "data-val-remote", errorMessage); MergeAttribute(context.Attributes, "data-val-remote-url", GetUrl(context)); if (!string.IsNullOrEmpty(HttpMethod)) { MergeAttribute(context.Attributes, "data-val-remote-type", HttpMethod); } var additionalFields = FormatAdditionalFieldsForClientValidation(context.ModelMetadata.PropertyName); MergeAttribute(context.Attributes, "data-val-remote-additionalfields", additionalFields); } private static bool MergeAttribute(IDictionary attributes, string key, string value) { if (attributes.ContainsKey(key)) { return false; } attributes.Add(key, value); return true; } private static IEnumerable SplitAndTrimPropertyNames(string original) { if (string.IsNullOrEmpty(original)) { return EmptyArray.Instance; } var split = original .Split(',') .Select(piece => piece.Trim()) .Where(trimmed => !string.IsNullOrEmpty(trimmed)); return split; } private void CheckForLocalizer(ClientModelValidationContext context) { if (!_checkedForLocalizer) { _checkedForLocalizer = true; var services = context.ActionContext.HttpContext.RequestServices; var options = services.GetRequiredService>(); var factory = services.GetService(); var provider = options.Value.DataAnnotationLocalizerProvider; if (factory != null && provider != null) { _stringLocalizer = provider( context.ModelMetadata.ContainerType ?? context.ModelMetadata.ModelType, factory); } } } private string GetErrorMessage(string displayName) { if (_stringLocalizer != null && !string.IsNullOrEmpty(ErrorMessage) && string.IsNullOrEmpty(ErrorMessageResourceName) && ErrorMessageResourceType == null) { return _stringLocalizer[ErrorMessage, displayName]; } return FormatErrorMessage(displayName); } } }