2018-06-29 19:52:40 +02:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.ComponentModel.DataAnnotations;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Web.Mvc;
|
2018-09-03 22:46:24 +10:00
|
|
|
|
using Umbraco.Core;
|
2019-04-04 21:57:49 +11:00
|
|
|
|
using Umbraco.Core.Models;
|
2019-03-14 00:59:51 +11:00
|
|
|
|
using Umbraco.Core.Services;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
|
|
|
|
|
|
namespace Umbraco.Web
|
|
|
|
|
|
{
|
|
|
|
|
|
internal static class ModelStateExtensions
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Merges ModelState that has names matching the prefix
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="state"></param>
|
|
|
|
|
|
/// <param name="dictionary"></param>
|
|
|
|
|
|
/// <param name="prefix"></param>
|
|
|
|
|
|
public static void Merge(this ModelStateDictionary state, ModelStateDictionary dictionary, string prefix)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (dictionary == null)
|
|
|
|
|
|
return;
|
|
|
|
|
|
foreach (var keyValuePair in dictionary
|
|
|
|
|
|
//It can either equal the prefix exactly (model level errors) or start with the prefix. (property level errors)
|
|
|
|
|
|
.Where(keyValuePair => keyValuePair.Key == prefix || keyValuePair.Key.StartsWith(prefix + ".")))
|
|
|
|
|
|
{
|
|
|
|
|
|
state[keyValuePair.Key] = keyValuePair.Value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Checks if there are any model errors on any fields containing the prefix
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="state"></param>
|
|
|
|
|
|
/// <param name="prefix"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
public static bool IsValid(this ModelStateDictionary state, string prefix)
|
|
|
|
|
|
{
|
|
|
|
|
|
return state.Where(v => v.Key.StartsWith(prefix + ".")).All(v => !v.Value.Errors.Any());
|
|
|
|
|
|
}
|
2019-03-27 12:39:05 +11:00
|
|
|
|
|
2018-06-29 19:52:40 +02:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Adds the error to model state correctly for a property so we can use it on the client side.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="modelState"></param>
|
|
|
|
|
|
/// <param name="result"></param>
|
|
|
|
|
|
/// <param name="propertyAlias"></param>
|
2018-08-02 20:00:35 +10:00
|
|
|
|
/// <param name="culture">The culture for the property, if the property is invariant than this is empty</param>
|
|
|
|
|
|
internal static void AddPropertyError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
|
|
|
|
|
|
ValidationResult result, string propertyAlias, string culture = "")
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
2018-08-02 20:00:35 +10:00
|
|
|
|
if (culture == null)
|
|
|
|
|
|
culture = "";
|
2019-03-14 00:59:51 +11:00
|
|
|
|
modelState.AddValidationError(result, "_Properties", propertyAlias,
|
|
|
|
|
|
//if the culture is null, we'll add the term 'invariant' as part of the key
|
|
|
|
|
|
culture.IsNullOrWhiteSpace() ? "invariant" : culture);
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2018-09-03 22:46:24 +10:00
|
|
|
|
/// <summary>
|
2019-03-14 14:18:42 +11:00
|
|
|
|
/// Adds a generic culture error for use in displaying the culture validation error in the save/publish/etc... dialogs
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="modelState"></param>
|
|
|
|
|
|
/// <param name="culture"></param>
|
|
|
|
|
|
/// <param name="errMsg"></param>
|
|
|
|
|
|
internal static void AddCultureValidationError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
|
|
|
|
|
|
string culture, string errMsg)
|
|
|
|
|
|
{
|
|
|
|
|
|
var key = "_content_variant_" + culture + "_";
|
|
|
|
|
|
if (modelState.ContainsKey(key)) return;
|
|
|
|
|
|
modelState.AddModelError(key, errMsg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Returns a list of cultures that have property validation errors errors
|
2018-09-03 22:46:24 +10:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="modelState"></param>
|
2019-03-14 00:59:51 +11:00
|
|
|
|
/// <param name="localizationService"></param>
|
2019-04-04 21:57:49 +11:00
|
|
|
|
/// <param name="cultureForInvariantErrors">The culture to affiliate invariant errors with</param>
|
|
|
|
|
|
/// <returns>
|
|
|
|
|
|
/// A list of cultures that have property validation errors. The default culture will be returned for any invariant property errors.
|
|
|
|
|
|
/// </returns>
|
2019-03-14 00:59:51 +11:00
|
|
|
|
internal static IReadOnlyList<string> GetCulturesWithPropertyErrors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
|
2019-04-04 21:57:49 +11:00
|
|
|
|
ILocalizationService localizationService, string cultureForInvariantErrors)
|
2018-09-03 22:46:24 +10:00
|
|
|
|
{
|
|
|
|
|
|
//Add any culture specific errors here
|
|
|
|
|
|
var cultureErrors = modelState.Keys
|
|
|
|
|
|
.Select(x => x.Split('.')) //split into parts
|
|
|
|
|
|
.Where(x => x.Length >= 3 && x[0] == "_Properties") //only choose _Properties errors
|
|
|
|
|
|
.Select(x => x[2]) //select the culture part
|
2019-03-27 12:39:05 +11:00
|
|
|
|
.Where(x => !x.IsNullOrWhiteSpace()) //if it has a value
|
|
|
|
|
|
//if it's marked "invariant" than return the default language, this is because we can only edit invariant properties on the default language
|
|
|
|
|
|
//so errors for those must show up under the default lang.
|
2019-04-04 21:57:49 +11:00
|
|
|
|
.Select(x => x == "invariant" ? cultureForInvariantErrors : x)
|
|
|
|
|
|
.WhereNotNull()
|
2019-03-27 12:39:05 +11:00
|
|
|
|
.Distinct()
|
|
|
|
|
|
.ToList();
|
2018-09-03 22:46:24 +10:00
|
|
|
|
|
|
|
|
|
|
return cultureErrors;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-03-14 14:18:42 +11:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Returns a list of cultures that have any validation errors
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="modelState"></param>
|
|
|
|
|
|
/// <param name="localizationService"></param>
|
2019-04-04 21:57:49 +11:00
|
|
|
|
/// <param name="cultureForInvariantErrors">The culture to affiliate invariant errors with</param>
|
|
|
|
|
|
/// <returns>
|
|
|
|
|
|
/// A list of cultures that have validation errors. The default culture will be returned for any invariant errors.
|
|
|
|
|
|
/// </returns>
|
2019-03-14 14:18:42 +11:00
|
|
|
|
internal static IReadOnlyList<string> GetCulturesWithErrors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
|
2019-04-04 21:57:49 +11:00
|
|
|
|
ILocalizationService localizationService, string cultureForInvariantErrors)
|
2019-03-14 14:18:42 +11:00
|
|
|
|
{
|
2019-04-04 21:57:49 +11:00
|
|
|
|
var propertyCultureErrors = modelState.GetCulturesWithPropertyErrors(localizationService, cultureForInvariantErrors);
|
2019-03-14 14:18:42 +11:00
|
|
|
|
|
|
|
|
|
|
//now check the other special culture errors that are
|
|
|
|
|
|
var genericCultureErrors = modelState.Keys
|
|
|
|
|
|
.Where(x => x.StartsWith("_content_variant_") && x.EndsWith("_"))
|
|
|
|
|
|
.Select(x => x.TrimStart("_content_variant_").TrimEnd("_"))
|
|
|
|
|
|
.Where(x => !x.IsNullOrWhiteSpace())
|
|
|
|
|
|
//if it's marked "invariant" than return the default language, this is because we can only edit invariant properties on the default language
|
|
|
|
|
|
//so errors for those must show up under the default lang.
|
2019-04-04 21:57:49 +11:00
|
|
|
|
.Select(x => x == "invariant" ? cultureForInvariantErrors : x)
|
|
|
|
|
|
.WhereNotNull()
|
2019-03-14 14:18:42 +11:00
|
|
|
|
.Distinct();
|
|
|
|
|
|
|
|
|
|
|
|
return propertyCultureErrors.Union(genericCultureErrors).ToList();
|
|
|
|
|
|
}
|
2019-04-04 21:57:49 +11:00
|
|
|
|
|
2018-08-02 20:00:35 +10:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Adds the error to model state correctly for a property so we can use it on the client side.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="modelState"></param>
|
|
|
|
|
|
/// <param name="result"></param>
|
|
|
|
|
|
/// <param name="parts">
|
|
|
|
|
|
/// Each model state validation error has a name and in most cases this name is made up of parts which are delimited by a '.'
|
|
|
|
|
|
/// </param>
|
|
|
|
|
|
internal static void AddValidationError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
|
|
|
|
|
|
ValidationResult result, params string[] parts)
|
2018-06-29 19:52:40 +02:00
|
|
|
|
{
|
|
|
|
|
|
// if there are assigned member names, we combine the member name with the owner name
|
|
|
|
|
|
// so that we can try to match it up to a real field. otherwise, we assume that the
|
|
|
|
|
|
// validation message is for the overall owner.
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// Owner = the component being validated, like a content property but could be just an HTML field on another editor
|
2018-08-02 20:00:35 +10:00
|
|
|
|
|
2018-06-29 19:52:40 +02:00
|
|
|
|
var withNames = false;
|
2018-08-02 20:00:35 +10:00
|
|
|
|
var delimitedParts = string.Join(".", parts);
|
2018-06-29 19:52:40 +02:00
|
|
|
|
foreach (var memberName in result.MemberNames)
|
|
|
|
|
|
{
|
2019-05-08 16:52:25 +10:00
|
|
|
|
modelState.TryAddModelError($"{delimitedParts}.{memberName}", result.ErrorMessage);
|
2018-06-29 19:52:40 +02:00
|
|
|
|
withNames = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!withNames)
|
2019-05-08 16:52:25 +10:00
|
|
|
|
{
|
|
|
|
|
|
modelState.TryAddModelError($"{delimitedParts}", result.ErrorMessage);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Will add an error to model state for a key if that key and error don't already exist
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="modelState"></param>
|
|
|
|
|
|
/// <param name="key"></param>
|
|
|
|
|
|
/// <param name="errorMsg"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
private static bool TryAddModelError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, string key, string errorMsg)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (modelState.TryGetValue(key, out var errs))
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach(var e in errs.Errors)
|
|
|
|
|
|
if (e.ErrorMessage == errorMsg) return false; //if this same error message exists for the same key, just exit
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
modelState.AddModelError(key, errorMsg);
|
|
|
|
|
|
return true;
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static IDictionary<string, object> ToErrorDictionary(this System.Web.Http.ModelBinding.ModelStateDictionary modelState)
|
|
|
|
|
|
{
|
|
|
|
|
|
var modelStateError = new Dictionary<string, object>();
|
|
|
|
|
|
foreach (var keyModelStatePair in modelState)
|
|
|
|
|
|
{
|
|
|
|
|
|
var key = keyModelStatePair.Key;
|
|
|
|
|
|
var errors = keyModelStatePair.Value.Errors;
|
|
|
|
|
|
if (errors != null && errors.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
modelStateError.Add(key, errors.Select(error => error.ErrorMessage));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return modelStateError;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Serializes the ModelState to JSON for JavaScript to interrogate the errors
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="state"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
public static JsonResult ToJsonErrors(this ModelStateDictionary state)
|
|
|
|
|
|
{
|
|
|
|
|
|
return new JsonResult
|
|
|
|
|
|
{
|
2019-03-27 12:39:05 +11:00
|
|
|
|
Data = new
|
|
|
|
|
|
{
|
|
|
|
|
|
success = state.IsValid.ToString().ToLower(),
|
|
|
|
|
|
failureType = "ValidationError",
|
|
|
|
|
|
validationErrors = from e in state
|
|
|
|
|
|
where e.Value.Errors.Count > 0
|
|
|
|
|
|
select new
|
|
|
|
|
|
{
|
|
|
|
|
|
name = e.Key,
|
|
|
|
|
|
errors = e.Value.Errors.Select(x => x.ErrorMessage)
|
|
|
|
|
|
.Concat(
|
|
|
|
|
|
e.Value.Errors.Where(x => x.Exception != null).Select(x => x.Exception.Message))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|