v10: Fix build warnings in Web.Common (#12349)

* Run code cleanup

* Run dotnet format

* Start manual cleanup in Web.Common

* Finish up manual cleanup

* Fix tests

* Fix up InMemoryModelFactory.cs

* Inject proper macroRenderer

* Update src/Umbraco.Web.Common/Filters/JsonDateTimeFormatAttribute.cs

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>

* Update src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>

* Fix based on review

Co-authored-by: Nikolaj Geisle <niko737@edu.ucl.dk>
Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
This commit is contained in:
Nikolaj Geisle
2022-05-09 09:39:46 +02:00
committed by GitHub
parent 02cd139770
commit c576bbea03
199 changed files with 12812 additions and 12443 deletions

View File

@@ -1,6 +1,4 @@
using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Events;
@@ -10,162 +8,160 @@ using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Web.Common.Routing;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Common.ModelBinders
namespace Umbraco.Cms.Web.Common.ModelBinders;
/// <summary>
/// Maps view models, supporting mapping to and from any <see cref="IPublishedContent" /> or
/// <see cref="IContentModel" />.
/// </summary>
public class ContentModelBinder : IModelBinder
{
private readonly IEventAggregator _eventAggregator;
/// <summary>
/// Maps view models, supporting mapping to and from any <see cref="IPublishedContent"/> or <see cref="IContentModel"/>.
/// Initializes a new instance of the <see cref="ContentModelBinder" /> class.
/// </summary>
public class ContentModelBinder : IModelBinder
public ContentModelBinder(IEventAggregator eventAggregator) => _eventAggregator = eventAggregator;
/// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext)
{
private readonly IEventAggregator _eventAggregator;
/// <summary>
/// Initializes a new instance of the <see cref="ContentModelBinder"/> class.
/// </summary>
public ContentModelBinder(IEventAggregator eventAggregator) => _eventAggregator = eventAggregator;
/// <inheritdoc/>
public Task BindModelAsync(ModelBindingContext bindingContext)
// Although this model binder is built to work both ways between IPublishedContent and IContentModel in reality
// only IPublishedContent will ever exist in the request so when this model binder is used as an IModelBinder
// in the aspnet pipeline it will really only support converting from IPublishedContent which is contained
// in the UmbracoRouteValues --> IContentModel
UmbracoRouteValues? umbracoRouteValues = bindingContext.HttpContext.Features.Get<UmbracoRouteValues>();
if (umbracoRouteValues is null)
{
// Although this model binder is built to work both ways between IPublishedContent and IContentModel in reality
// only IPublishedContent will ever exist in the request so when this model binder is used as an IModelBinder
// in the aspnet pipeline it will really only support converting from IPublishedContent which is contained
// in the UmbracoRouteValues --> IContentModel
UmbracoRouteValues? umbracoRouteValues = bindingContext.HttpContext.Features.Get<UmbracoRouteValues>();
if (umbracoRouteValues is null)
{
return Task.CompletedTask;
}
BindModel(bindingContext, umbracoRouteValues.PublishedRequest.PublishedContent, bindingContext.ModelType);
return Task.CompletedTask;
}
// source is the model that we have
// modelType is the type of the model that we need to bind to
//
// create a model object of the modelType by mapping:
// { ContentModel, ContentModel<TContent>, IPublishedContent }
// to
// { ContentModel, ContentModel<TContent>, IPublishedContent }
BindModel(bindingContext, umbracoRouteValues.PublishedRequest.PublishedContent, bindingContext.ModelType);
return Task.CompletedTask;
}
/// <summary>
/// Attempts to bind the model
/// </summary>
public void BindModel(ModelBindingContext bindingContext, object? source, Type modelType)
// source is the model that we have
// modelType is the type of the model that we need to bind to
//
// create a model object of the modelType by mapping:
// { ContentModel, ContentModel<TContent>, IPublishedContent }
// to
// { ContentModel, ContentModel<TContent>, IPublishedContent }
/// <summary>
/// Attempts to bind the model
/// </summary>
public void BindModel(ModelBindingContext bindingContext, object? source, Type modelType)
{
// Null model, return
if (source == null)
{
// Null model, return
if (source == null)
{
return;
}
// If types already match, return
Type sourceType = source.GetType();
if (sourceType.Inherits(modelType))
{
bindingContext.Result = ModelBindingResult.Success(source);
return;
}
// Try to grab the content
var sourceContent = source as IPublishedContent; // check if what we have is an IPublishedContent
if (sourceContent == null && sourceType.Implements<IContentModel>())
{
// else check if it's an IContentModel, and get the content
sourceContent = ((IContentModel)source).Content;
}
if (sourceContent == null)
{
// else check if we can convert it to a content
Attempt<IPublishedContent> attempt1 = source.TryConvertTo<IPublishedContent>();
if (attempt1.Success)
{
sourceContent = attempt1.Result;
}
}
// If we have a content
if (sourceContent != null)
{
// If model is IPublishedContent, check content type and return
if (modelType.Implements<IPublishedContent>())
{
if (sourceContent.GetType().Inherits(modelType) == false)
{
ThrowModelBindingException(true, false, sourceContent.GetType(), modelType);
}
bindingContext.Result = ModelBindingResult.Success(sourceContent);
return;
}
// If model is ContentModel, create and return
if (modelType == typeof(ContentModel))
{
bindingContext.Result = ModelBindingResult.Success(new ContentModel(sourceContent));
return;
}
// If model is ContentModel<TContent>, check content type, then create and return
if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(ContentModel<>))
{
Type targetContentType = modelType.GetGenericArguments()[0];
if (sourceContent.GetType().Inherits(targetContentType) == false)
{
ThrowModelBindingException(true, true, sourceContent.GetType(), targetContentType);
}
bindingContext.Result = ModelBindingResult.Success(Activator.CreateInstance(modelType, sourceContent));
return;
}
}
// Last chance : try to convert
Attempt<object?> attempt2 = source.TryConvertTo(modelType);
if (attempt2.Success)
{
bindingContext.Result = ModelBindingResult.Success(attempt2.Result);
return;
}
// Fail
ThrowModelBindingException(false, false, sourceType, modelType);
return;
}
private void ThrowModelBindingException(bool sourceContent, bool modelContent, Type sourceType, Type modelType)
// If types already match, return
Type sourceType = source.GetType();
if (sourceType.Inherits(modelType))
{
var msg = new StringBuilder();
// prepare message
msg.Append("Cannot bind source");
if (sourceContent)
{
msg.Append(" content");
}
msg.Append(" type ");
msg.Append(sourceType.FullName);
msg.Append(" to model");
if (modelContent)
{
msg.Append(" content");
}
msg.Append(" type ");
msg.Append(modelType.FullName);
msg.Append(".");
// raise event, to give model factories a chance at reporting
// the error with more details, and optionally request that
// the application restarts.
var args = new ModelBindingErrorNotification(sourceType, modelType, msg);
_eventAggregator.Publish(args);
throw new ModelBindingException(msg.ToString());
bindingContext.Result = ModelBindingResult.Success(source);
return;
}
// Try to grab the content
var sourceContent = source as IPublishedContent; // check if what we have is an IPublishedContent
if (sourceContent == null && sourceType.Implements<IContentModel>())
{
// else check if it's an IContentModel, and get the content
sourceContent = ((IContentModel)source).Content;
}
if (sourceContent == null)
{
// else check if we can convert it to a content
Attempt<IPublishedContent> attempt1 = source.TryConvertTo<IPublishedContent>();
if (attempt1.Success)
{
sourceContent = attempt1.Result;
}
}
// If we have a content
if (sourceContent != null)
{
// If model is IPublishedContent, check content type and return
if (modelType.Implements<IPublishedContent>())
{
if (sourceContent.GetType().Inherits(modelType) == false)
{
ThrowModelBindingException(true, false, sourceContent.GetType(), modelType);
}
bindingContext.Result = ModelBindingResult.Success(sourceContent);
return;
}
// If model is ContentModel, create and return
if (modelType == typeof(ContentModel))
{
bindingContext.Result = ModelBindingResult.Success(new ContentModel(sourceContent));
return;
}
// If model is ContentModel<TContent>, check content type, then create and return
if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(ContentModel<>))
{
Type targetContentType = modelType.GetGenericArguments()[0];
if (sourceContent.GetType().Inherits(targetContentType) == false)
{
ThrowModelBindingException(true, true, sourceContent.GetType(), targetContentType);
}
bindingContext.Result = ModelBindingResult.Success(Activator.CreateInstance(modelType, sourceContent));
return;
}
}
// Last chance : try to convert
Attempt<object?> attempt2 = source.TryConvertTo(modelType);
if (attempt2.Success)
{
bindingContext.Result = ModelBindingResult.Success(attempt2.Result);
return;
}
// Fail
ThrowModelBindingException(false, false, sourceType, modelType);
}
private void ThrowModelBindingException(bool sourceContent, bool modelContent, Type sourceType, Type modelType)
{
var msg = new StringBuilder();
// prepare message
msg.Append("Cannot bind source");
if (sourceContent)
{
msg.Append(" content");
}
msg.Append(" type ");
msg.Append(sourceType.FullName);
msg.Append(" to model");
if (modelContent)
{
msg.Append(" content");
}
msg.Append(" type ");
msg.Append(modelType.FullName);
msg.Append(".");
// raise event, to give model factories a chance at reporting
// the error with more details, and optionally request that
// the application restarts.
var args = new ModelBindingErrorNotification(sourceType, modelType, msg);
_eventAggregator.Publish(args);
throw new ModelBindingException(msg.ToString());
}
}

View File

@@ -1,30 +1,30 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Web.Common.ModelBinders
namespace Umbraco.Cms.Web.Common.ModelBinders;
/// <summary>
/// The provider for <see cref="ContentModelBinder" /> mapping view models, supporting mapping to and from any
/// IPublishedContent or IContentModel.
/// </summary>
public class ContentModelBinderProvider : IModelBinderProvider
{
/// <summary>
/// The provider for <see cref="ContentModelBinder"/> mapping view models, supporting mapping to and from any IPublishedContent or IContentModel.
/// </summary>
public class ContentModelBinderProvider : IModelBinderProvider
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
Type modelType = context.Metadata.ModelType;
// Can bind to ContentModel (exact type match)
// or to ContentModel<TContent> (exact generic type match)
// or to TContent where TContent : IPublishedContent (any IPublishedContent implementation)
if (modelType == typeof(ContentModel) ||
(modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(ContentModel<>)) ||
typeof(IPublishedContent).IsAssignableFrom(modelType))
{
var modelType = context.Metadata.ModelType;
// Can bind to ContentModel (exact type match)
// or to ContentModel<TContent> (exact generic type match)
// or to TContent where TContent : IPublishedContent (any IPublishedContent implementation)
if (modelType == typeof(ContentModel) ||
(modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(ContentModel<>)) ||
typeof(IPublishedContent).IsAssignableFrom(modelType))
{
return new BinderTypeModelBinder(typeof(ContentModelBinder));
}
return null;
return new BinderTypeModelBinder(typeof(ContentModelBinder));
}
return null;
}
}

View File

@@ -1,50 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Primitives;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Common.ModelBinders
{
/// <summary>
/// Allows an Action to execute with an arbitrary number of QueryStrings
/// </summary>
/// <remarks>
/// Just like you can POST an arbitrary number of parameters to an Action, you can't GET an arbitrary number
/// but this will allow you to do it.
/// </remarks>
public sealed class HttpQueryStringModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var queryStrings = GetQueryAsDictionary(bindingContext.ActionContext.HttpContext.Request.Query);
var queryStringKeys = queryStrings.Select(kvp => kvp.Key).ToArray();
if (queryStringKeys.InvariantContains("culture") == false)
{
queryStrings.Add("culture", new StringValues(bindingContext.ActionContext.HttpContext.Request.ClientCulture()));
}
namespace Umbraco.Cms.Web.Common.ModelBinders;
var formData = new FormCollection(queryStrings);
bindingContext.Result = ModelBindingResult.Success(formData);
return Task.CompletedTask;
/// <summary>
/// Allows an Action to execute with an arbitrary number of QueryStrings
/// </summary>
/// <remarks>
/// Just like you can POST an arbitrary number of parameters to an Action, you can't GET an arbitrary number
/// but this will allow you to do it.
/// </remarks>
public sealed class HttpQueryStringModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
Dictionary<string, StringValues> queryStrings =
GetQueryAsDictionary(bindingContext.ActionContext.HttpContext.Request.Query);
var queryStringKeys = queryStrings.Select(kvp => kvp.Key).ToArray();
if (queryStringKeys.InvariantContains("culture") == false)
{
queryStrings.Add(
"culture",
new StringValues(bindingContext.ActionContext.HttpContext.Request.ClientCulture()));
}
private Dictionary<string, StringValues> GetQueryAsDictionary(IQueryCollection query)
var formData = new FormCollection(queryStrings);
bindingContext.Result = ModelBindingResult.Success(formData);
return Task.CompletedTask;
}
private Dictionary<string, StringValues> GetQueryAsDictionary(IQueryCollection? query)
{
var result = new Dictionary<string, StringValues>();
if (query == null)
{
var result = new Dictionary<string, StringValues>();
if (query == null)
{
return result;
}
foreach (var item in query)
{
result.Add(item.Key, item.Value);
}
return result;
}
foreach (KeyValuePair<string, StringValues> item in query)
{
result.Add(item.Key, item.Value);
}
return result;
}
}

View File

@@ -1,46 +1,57 @@
using System;
using System.Runtime.Serialization;
namespace Umbraco.Cms.Web.Common.ModelBinders
namespace Umbraco.Cms.Web.Common.ModelBinders;
/// <summary>
/// The exception that is thrown when an error occurs while binding a source to a model.
/// </summary>
/// <seealso cref="Exception" />
/// Migrated to .NET Core
[Serializable]
public class ModelBindingException : Exception
{
/// <summary>
/// The exception that is thrown when an error occurs while binding a source to a model.
/// Initializes a new instance of the <see cref="ModelBindingException" /> class.
/// </summary>
/// <seealso cref="Exception" />
/// Migrated to .NET Core
[Serializable]
public class ModelBindingException : Exception
public ModelBindingException()
{
/// <summary>
/// Initializes a new instance of the <see cref="ModelBindingException" /> class.
/// </summary>
public ModelBindingException()
{ }
}
/// <summary>
/// Initializes a new instance of the <see cref="ModelBindingException" /> class.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public ModelBindingException(string message)
: base(message)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="ModelBindingException" /> class.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public ModelBindingException(string message)
: base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ModelBindingException" /> class.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference (<see langword="Nothing" /> in Visual Basic) if no inner exception is specified.</param>
public ModelBindingException(string message, Exception innerException)
: base(message, innerException)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="ModelBindingException" /> class.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">
/// The exception that is the cause of the current exception, or a null reference (
/// <see langword="Nothing" /> in Visual Basic) if no inner exception is specified.
/// </param>
public ModelBindingException(string message, Exception innerException)
: base(message, innerException)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ModelBindingException" /> class.
/// </summary>
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> that holds the serialized object data about the exception being thrown.</param>
/// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual information about the source or destination.</param>
protected ModelBindingException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="ModelBindingException" /> class.
/// </summary>
/// <param name="info">
/// The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> that holds the serialized object
/// data about the exception being thrown.
/// </param>
/// <param name="context">
/// The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual
/// information about the source or destination.
/// </param>
protected ModelBindingException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}

View File

@@ -2,45 +2,46 @@ using System.Buffers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;
using Newtonsoft.Json;
using Umbraco.Cms.Web.Common.Formatters;
namespace Umbraco.Cms.Web.Common.ModelBinders
namespace Umbraco.Cms.Web.Common.ModelBinders;
/// <summary>
/// A custom body model binder that only uses a <see cref="NewtonsoftJsonInputFormatter" /> to bind body action
/// parameters
/// </summary>
public class UmbracoJsonModelBinder : BodyModelBinder
{
/// <summary>
/// A custom body model binder that only uses a <see cref="NewtonsoftJsonInputFormatter"/> to bind body action parameters
/// </summary>
public class UmbracoJsonModelBinder : BodyModelBinder, IModelBinder
public UmbracoJsonModelBinder(
ArrayPool<char> arrayPool,
ObjectPoolProvider objectPoolProvider,
IHttpRequestStreamReaderFactory readerFactory,
ILoggerFactory loggerFactory)
: base(GetNewtonsoftJsonFormatter(loggerFactory, arrayPool, objectPoolProvider), readerFactory, loggerFactory)
{
public UmbracoJsonModelBinder(ArrayPool<char> arrayPool, ObjectPoolProvider objectPoolProvider, IHttpRequestStreamReaderFactory readerFactory, ILoggerFactory loggerFactory)
: base(GetNewtonsoftJsonFormatter(loggerFactory, arrayPool, objectPoolProvider), readerFactory, loggerFactory)
}
private static IInputFormatter[] GetNewtonsoftJsonFormatter(ILoggerFactory logger, ArrayPool<char> arrayPool, ObjectPoolProvider objectPoolProvider)
{
var jsonOptions = new MvcNewtonsoftJsonOptions { AllowInputFormatterExceptionMessages = true };
JsonSerializerSettings ss = jsonOptions.SerializerSettings; // Just use the defaults as base
// We need to ignore required attributes when serializing. E.g UserSave.ChangePassword. Otherwise the model is not model bound.
ss.ContractResolver = new IgnoreRequiredAttributesResolver();
return new IInputFormatter[]
{
}
private static IInputFormatter[] GetNewtonsoftJsonFormatter(ILoggerFactory logger, ArrayPool<char> arrayPool, ObjectPoolProvider objectPoolProvider)
{
var jsonOptions = new MvcNewtonsoftJsonOptions
{
AllowInputFormatterExceptionMessages = true
};
var ss = jsonOptions.SerializerSettings; // Just use the defaults as base
// We need to ignore required attributes when serializing. E.g UserSave.ChangePassword. Otherwise the model is not model bound.
ss.ContractResolver = new IgnoreRequiredAttributesResolver();
return new IInputFormatter[]
{
new NewtonsoftJsonInputFormatter(
logger.CreateLogger<UmbracoJsonModelBinder>(),
jsonOptions.SerializerSettings, // Just use the defaults
arrayPool,
objectPoolProvider,
new MvcOptions(), // The only option that NewtonsoftJsonInputFormatter uses is SuppressInputFormatterBuffering
jsonOptions)
};
}
new NewtonsoftJsonInputFormatter(
logger.CreateLogger<UmbracoJsonModelBinder>(),
jsonOptions.SerializerSettings, // Just use the defaults
arrayPool,
objectPoolProvider,
new MvcOptions(), // The only option that NewtonsoftJsonInputFormatter uses is SuppressInputFormatterBuffering
jsonOptions),
};
}
}