using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.PropertyEditors.Validation;
namespace Umbraco.Web
{
internal static class ModelStateExtensions
{
///
/// Merges ModelState that has names matching the prefix
///
///
///
///
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;
}
}
///
/// Checks if there are any model errors on any fields containing the prefix
///
///
///
///
public static bool IsValid(this ModelStateDictionary state, string prefix)
{
return state.Where(v => v.Key.StartsWith(prefix + ".")).All(v => !v.Value.Errors.Any());
}
///
/// Adds an error to model state for a property so we can use it on the client side.
///
///
///
///
/// The culture for the property, if the property is invariant than this is empty
internal static void AddPropertyError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
ValidationResult result, string propertyAlias, string culture = "", string segment = "")
{
modelState.AddPropertyValidationError(new ContentPropertyValidationResult(result, culture, segment), propertyAlias, culture, segment);
}
///
/// Adds the to the model state with the appropriate keys for property errors
///
///
///
///
///
///
internal static void AddPropertyValidationError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
ValidationResult result, string propertyAlias, string culture = "", string segment = "")
{
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,
// if the segment is null, we'll add the term 'null' as part of the key
segment.IsNullOrWhiteSpace() ? "null" : segment);
}
///
/// Adds a generic culture error for use in displaying the culture validation error in the save/publish/etc... dialogs
///
///
///
///
///
internal static void AddVariantValidationError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
string culture, string segment, string errMsg)
{
var key = "_content_variant_" + (culture.IsNullOrWhiteSpace() ? "invariant" : culture) + "_" + (segment.IsNullOrWhiteSpace() ? "null" : segment) + "_";
if (modelState.ContainsKey(key)) return;
modelState.AddModelError(key, errMsg);
}
///
/// Returns a list of cultures that have property validation errors
///
///
///
/// The culture to affiliate invariant errors with
///
/// A list of cultures that have property validation errors. The default culture will be returned for any invariant property errors.
///
internal static IReadOnlyList<(string culture, string segment)> GetVariantsWithPropertyErrors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
string cultureForInvariantErrors)
{
//Add any variant specific errors here
var variantErrors = modelState.Keys
.Where(key => key.StartsWith("_Properties.")) //only choose _Properties errors
.Select(x => x.Split('.')) //split into parts
.Where(x => x.Length >= 4 && !x[2].IsNullOrWhiteSpace() && !x[3].IsNullOrWhiteSpace())
.Select(x => (culture: x[2], segment: x[3]))
//if the culture is 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.
//if the segment is marked "null" then return an actual null
.Select(x =>
{
var culture = x.culture == "invariant" ? cultureForInvariantErrors : x.culture;
var segment = x.segment == "null" ? null : x.segment;
return (culture, segment);
})
.Distinct()
.ToList();
return variantErrors;
}
///
/// Returns a list of cultures that have any validation errors
///
///
///
/// The culture to affiliate invariant errors with
///
/// A list of cultures that have validation errors. The default culture will be returned for any invariant errors.
///
internal static IReadOnlyList<(string culture, string segment)> GetVariantsWithErrors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, string cultureForInvariantErrors)
{
var propertyVariantErrors = modelState.GetVariantsWithPropertyErrors(cultureForInvariantErrors);
//now check the other special variant errors that are
var genericVariantErrors = modelState.Keys
.Where(x => x.StartsWith("_content_variant_") && x.EndsWith("_"))
.Select(x => x.TrimStart("_content_variant_").TrimEnd("_"))
.Select(x =>
{
// Format "_"
var cs = x.Split(new[] { '_' });
return (culture: cs[0], segment: cs[1]);
})
.Where(x => !x.culture.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.
//if the segment is marked "null" then return an actual null
.Select(x =>
{
var culture = x.culture == "invariant" ? cultureForInvariantErrors : x.culture;
var segment = x.segment == "null" ? null : x.segment;
return (culture, segment);
})
.Distinct();
return propertyVariantErrors.Union(genericVariantErrors).Distinct().ToList();
}
///
/// Adds the error to model state correctly for a property so we can use it on the client side.
///
///
///
///
/// Each model state validation error has a name and in most cases this name is made up of parts which are delimited by a '.'
///
internal static void AddValidationError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState,
ValidationResult result, params string[] parts)
{
// 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.
// Owner = the component being validated, like a content property but could be just an HTML field on another editor
var withNames = false;
var delimitedParts = string.Join(".", parts);
foreach (var memberName in result.MemberNames)
{
modelState.TryAddModelError($"{delimitedParts}.{memberName}", result.ToString());
withNames = true;
}
if (!withNames)
{
modelState.TryAddModelError($"{delimitedParts}", result.ToString());
}
}
///
/// Will add an error to model state for a key if that key and error don't already exist
///
///
///
///
///
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;
}
public static IDictionary ToErrorDictionary(this System.Web.Http.ModelBinding.ModelStateDictionary modelState)
{
var modelStateError = new Dictionary();
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;
}
///
/// Serializes the ModelState to JSON for JavaScript to interrogate the errors
///
///
///
public static JsonResult ToJsonErrors(this ModelStateDictionary state)
{
return new JsonResult
{
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))
}
}
};
}
}
}