Merge branch 'v9/dev' into v9/task/package-refactor

# Conflicts:
#	src/Umbraco.Web.BackOffice/Controllers/PackageController.cs
#	src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs
This commit is contained in:
Shannon
2021-07-05 15:06:26 -06:00
141 changed files with 3993 additions and 1994 deletions

View File

@@ -1,8 +1,9 @@
using System.Net;
using System.Net;
using Microsoft.AspNetCore.Mvc;
namespace Umbraco.Cms.Web.Common.ActionsResults
{
// TODO: What is the purpose of this? Doesn't seem to add any benefit
public class UmbracoProblemResult : ObjectResult
{
public UmbracoProblemResult(string message, HttpStatusCode httpStatusCode = HttpStatusCode.InternalServerError) : base(new {Message = message})

View File

@@ -1,10 +1,19 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Common.ActionsResults
{
// TODO: This should probably follow the same conventions as in aspnet core and use ProblemDetails
// and ProblemDetails factory. See https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ControllerBase.cs#L1977
// ProblemDetails is explicitly checked for in the application model.
// In our base class UmbracoAuthorizedApiController the logic is there to create a ProblemDetails.
// However, to do this will require changing how angular deals with errors since the response will
// probably be different. Would just be better to follow the aspnet patterns.
/// <summary>
/// Custom result to return a validation error message with required headers
/// </summary>
@@ -13,6 +22,11 @@ namespace Umbraco.Cms.Web.Common.ActionsResults
/// </remarks>
public class ValidationErrorResult : ObjectResult
{
/// <summary>
/// Typically this should not be used and just use the ValidationProblem method on the base controller class.
/// </summary>
/// <param name="errorMessage"></param>
/// <returns></returns>
public static ValidationErrorResult CreateNotificationValidationErrorResult(string errorMessage)
{
var notificationModel = new SimpleNotificationModel
@@ -23,6 +37,9 @@ namespace Umbraco.Cms.Web.Common.ActionsResults
return new ValidationErrorResult(notificationModel);
}
public ValidationErrorResult(ModelStateDictionary modelState)
: this(new SimpleValidationModel(modelState.ToErrorDictionary())) { }
public ValidationErrorResult(object value, int statusCode) : base(value)
{
StatusCode = statusCode;
@@ -32,6 +49,7 @@ namespace Umbraco.Cms.Web.Common.ActionsResults
{
}
// TODO: Like here, shouldn't we use ProblemDetails?
public ValidationErrorResult(string errorMessage, int statusCode) : base(new { Message = errorMessage })
{
StatusCode = statusCode;

View File

@@ -122,5 +122,27 @@ namespace Umbraco.Extensions
true,
constraints);
}
public static void MapUmbracoSurfaceRoute(
this IEndpointRouteBuilder endpoints,
Type controllerType,
string rootSegment,
string areaName,
string defaultAction = "Index",
bool includeControllerNameInRoute = true,
object constraints = null)
{
// If there is an area name it's a plugin controller, and we should use the area name instead of surface
string prefixPathSegment = areaName.IsNullOrWhiteSpace() ? "Surface" : areaName;
endpoints.MapUmbracoRoute(
controllerType,
rootSegment,
areaName,
prefixPathSegment,
defaultAction,
includeControllerNameInRoute,
constraints);
}
}
}

View File

@@ -1,10 +1,11 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Core.Models;
namespace Umbraco.Extensions
{
@@ -37,12 +38,23 @@ namespace Umbraco.Extensions
string cropAlias) =>
mediaItem.GetCropUrl(cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider);
public static string GetCropUrl(this MediaWithCrops mediaWithCrops, string cropAlias)
=> ImageCropperTemplateCoreExtensions.GetCropUrl(mediaWithCrops, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider);
/// <summary>
/// Gets the crop URL by using only the specified <paramref name="imageCropperValue" />.
/// </summary>
/// <param name="mediaItem">The media item.</param>
/// <param name="imageCropperValue">The image cropper value.</param>
/// <param name="cropAlias">The crop alias.</param>
/// <returns>
/// The image crop URL.
/// </returns>
public static string GetCropUrl(
this IPublishedContent mediaItem,
string cropAlias,
ImageCropperValue imageCropperValue)
=> mediaItem.GetCropUrl(cropAlias, ImageUrlGenerator, imageCropperValue);
ImageCropperValue imageCropperValue,
string cropAlias)
=> ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, imageCropperValue, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider);
/// <summary>
/// Gets the underlying image processing service URL by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item.
@@ -65,6 +77,9 @@ namespace Umbraco.Extensions
string cropAlias) =>
mediaItem.GetCropUrl(propertyAlias, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider);
public static string GetCropUrl(this MediaWithCrops mediaWithCrops, string propertyAlias, string cropAlias)
=> ImageCropperTemplateCoreExtensions.GetCropUrl(mediaWithCrops, propertyAlias, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider);
/// <summary>
/// Gets the underlying image processing service URL from the IPublishedContent item.
/// </summary>
@@ -326,6 +341,6 @@ namespace Umbraco.Extensions
this MediaWithCrops mediaWithCrops,
string alias,
string cacheBusterValue = null)
=> mediaWithCrops.GetLocalCropUrl(alias, ImageUrlGenerator, cacheBusterValue);
=> mediaWithCrops.GetLocalCropUrl(alias, cacheBusterValue);
}
}

View File

@@ -1,4 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq.Expressions;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Web.Common.DependencyInjection;
@@ -70,5 +72,11 @@ namespace Umbraco.Extensions
public static bool IsVisible(this IPublishedElement content) => content.IsVisible(PublishedValueFallback);
/// <summary>
/// Gets the value of a property.
/// </summary>
public static TValue ValueFor<TModel, TValue>(this TModel model, Expression<Func<TModel, TValue>> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default)
where TModel : IPublishedElement =>
model.ValueFor(PublishedValueFallback, property, culture, segment, fallback);
}
}

View File

@@ -1,11 +1,13 @@
using System;
using System;
using System.Globalization;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Routing;
using Umbraco.Core.Models;
namespace Umbraco.Extensions
{
@@ -36,9 +38,35 @@ namespace Umbraco.Extensions
return mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, cropAlias: cropAlias, useCropDimensions: true);
}
public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, IImageUrlGenerator imageUrlGenerator, ImageCropperValue imageCropperValue)
public static string GetCropUrl(
this MediaWithCrops mediaWithCrops,
string cropAlias,
IImageUrlGenerator imageUrlGenerator,
IPublishedValueFallback publishedValueFallback,
IPublishedUrlProvider publishedUrlProvider)
{
return mediaItem.Url().GetCropUrl(imageUrlGenerator, imageCropperValue, cropAlias: cropAlias, useCropDimensions: true);
return mediaWithCrops.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, cropAlias: cropAlias, useCropDimensions: true);
}
/// <summary>
/// Gets the crop URL by using only the specified <paramref name="imageCropperValue" />.
/// </summary>
/// <param name="mediaItem">The media item.</param>
/// <param name="imageCropperValue">The image cropper value.</param>
/// <param name="cropAlias">The crop alias.</param>
/// <param name="imageUrlGenerator">The image URL generator.</param>
/// <returns>
/// The image crop URL.
/// </returns>
public static string GetCropUrl(
this IPublishedContent mediaItem,
ImageCropperValue imageCropperValue,
string cropAlias,
IImageUrlGenerator imageUrlGenerator,
IPublishedValueFallback publishedValueFallback,
IPublishedUrlProvider publishedUrlProvider)
{
return mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, imageCropperValue, true, cropAlias: cropAlias, useCropDimensions: true);
}
/// <summary>
@@ -70,6 +98,16 @@ namespace Umbraco.Extensions
return mediaItem.GetCropUrl( imageUrlGenerator, publishedValueFallback, publishedUrlProvider, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true);
}
public static string GetCropUrl(this MediaWithCrops mediaWithCrops,
IPublishedValueFallback publishedValueFallback,
IPublishedUrlProvider publishedUrlProvider,
string propertyAlias,
string cropAlias,
IImageUrlGenerator imageUrlGenerator)
{
return mediaWithCrops.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true);
}
/// <summary>
/// Gets the underlying image processing service URL from the IPublishedContent item.
/// </summary>
@@ -145,7 +183,55 @@ namespace Umbraco.Extensions
ImageCropRatioMode? ratioMode = null,
bool upScale = true)
{
if (mediaItem == null) throw new ArgumentNullException("mediaItem");
return mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, null, false, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale);
}
public static string GetCropUrl(
this MediaWithCrops mediaWithCrops,
IImageUrlGenerator imageUrlGenerator,
IPublishedValueFallback publishedValueFallback,
IPublishedUrlProvider publishedUrlProvider,
int? width = null,
int? height = null,
string propertyAlias = Constants.Conventions.Media.File,
string cropAlias = null,
int? quality = null,
ImageCropMode? imageCropMode = null,
ImageCropAnchor? imageCropAnchor = null,
bool preferFocalPoint = false,
bool useCropDimensions = false,
bool cacheBuster = true,
string furtherOptions = null,
ImageCropRatioMode? ratioMode = null,
bool upScale = true)
{
if (mediaWithCrops == null) throw new ArgumentNullException(nameof(mediaWithCrops));
return mediaWithCrops.Content.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, mediaWithCrops.LocalCrops, false, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale);
}
private static string GetCropUrl(
this IPublishedContent mediaItem,
IImageUrlGenerator imageUrlGenerator,
IPublishedValueFallback publishedValueFallback,
IPublishedUrlProvider publishedUrlProvider,
ImageCropperValue localCrops,
bool localCropsOnly,
int? width = null,
int? height = null,
string propertyAlias = Constants.Conventions.Media.File,
string cropAlias = null,
int? quality = null,
ImageCropMode? imageCropMode = null,
ImageCropAnchor? imageCropAnchor = null,
bool preferFocalPoint = false,
bool useCropDimensions = false,
bool cacheBuster = true,
string furtherOptions = null,
ImageCropRatioMode? ratioMode = null,
bool upScale = true)
{
if (mediaItem == null) throw new ArgumentNullException(nameof(mediaItem));
var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null;
@@ -154,31 +240,38 @@ namespace Umbraco.Extensions
var mediaItemUrl = mediaItem.MediaUrl(publishedUrlProvider, propertyAlias: propertyAlias);
//get the default obj from the value converter
var cropperValue = mediaItem.Value(publishedValueFallback, propertyAlias);
//is it strongly typed?
var stronglyTyped = cropperValue as ImageCropperValue;
if (stronglyTyped != null)
// Only get crops from media when required and used
if (localCropsOnly == false && (imageCropMode == ImageCropMode.Crop || imageCropMode == null))
{
return GetCropUrl(
mediaItemUrl, imageUrlGenerator, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions,
cacheBusterValue, furtherOptions, ratioMode, upScale);
// Get the default cropper value from the value converter
var cropperValue = mediaItem.Value(publishedValueFallback, propertyAlias);
var mediaCrops = cropperValue as ImageCropperValue;
if (mediaCrops == null && cropperValue is JObject jobj)
{
mediaCrops = jobj.ToObject<ImageCropperValue>();
}
if (mediaCrops == null && cropperValue is string imageCropperValue &&
string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson())
{
mediaCrops = imageCropperValue.DeserializeImageCropperValue();
}
// Merge crops
if (localCrops == null)
{
localCrops = mediaCrops;
}
else if (mediaCrops != null)
{
localCrops = localCrops.Merge(mediaCrops);
}
}
//this shouldn't be the case but we'll check
var jobj = cropperValue as JObject;
if (jobj != null)
{
stronglyTyped = jobj.ToObject<ImageCropperValue>();
return GetCropUrl(
mediaItemUrl, imageUrlGenerator, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions,
cacheBusterValue, furtherOptions, ratioMode, upScale);
}
//it's a single string
return GetCropUrl(
mediaItemUrl, imageUrlGenerator, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions,
mediaItemUrl, imageUrlGenerator, localCrops, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions,
cacheBusterValue, furtherOptions, ratioMode, upScale);
}
@@ -259,6 +352,7 @@ namespace Umbraco.Extensions
{
cropDataSet = imageCropperValue.DeserializeImageCropperValue();
}
return GetCropUrl(
imageUrl, imageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode,
imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale);
@@ -402,11 +496,5 @@ namespace Umbraco.Extensions
return imageUrlGenerator.GetImageUrl(options);
}
public static string GetLocalCropUrl(this MediaWithCrops mediaWithCrops, string alias, IImageUrlGenerator imageUrlGenerator, string cacheBusterValue)
{
return mediaWithCrops.LocalCrops.Src + mediaWithCrops.LocalCrops.GetCropUrl(alias, imageUrlGenerator, cacheBusterValue: cacheBusterValue);
}
}
}

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Umbraco.Extensions
{
public static class ModelStateExtensions
{
/// <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) =>
state.Where(v => v.Key.StartsWith(prefix + ".")).All(v => !v.Value.Errors.Any());
public static IDictionary<string, object> ToErrorDictionary(this ModelStateDictionary modelState)
{
var modelStateError = new Dictionary<string, object>();
foreach (KeyValuePair<string, ModelStateEntry> keyModelStatePair in modelState)
{
var key = keyModelStatePair.Key;
ModelErrorCollection 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) =>
new JsonResult(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))
}
});
}
}

View File

@@ -10,6 +10,7 @@ using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.WebAssets;
using Umbraco.Cms.Web.Common.Controllers;
@@ -204,6 +205,56 @@ namespace Umbraco.Extensions
return $"{version}.{runtimeMinifier.CacheBuster}".GenerateHash();
}
public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, IPublishedContent mediaItem, string cropAlias, bool htmlEncode = true)
{
if (mediaItem == null)
{
return HtmlString.Empty;
}
var url = mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true);
return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url);
}
public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, IPublishedContent mediaItem, string propertyAlias, string cropAlias, bool htmlEncode = true)
{
if (mediaItem == null)
{
return HtmlString.Empty;
}
var url = mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true);
return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url);
}
public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper,
IPublishedContent mediaItem,
int? width = null,
int? height = null,
string propertyAlias = Constants.Conventions.Media.File,
string cropAlias = null,
int? quality = null,
ImageCropMode? imageCropMode = null,
ImageCropAnchor? imageCropAnchor = null,
bool preferFocalPoint = false,
bool useCropDimensions = false,
bool cacheBuster = true,
string furtherOptions = null,
ImageCropRatioMode? ratioMode = null,
bool upScale = true,
bool htmlEncode = true)
{
if (mediaItem == null)
{
return HtmlString.Empty;
}
var url = mediaItem.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode,
imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode,
upScale);
return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url);
}
public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper,
ImageCropperValue imageCropperValue,
string cropAlias,

View File

@@ -362,7 +362,7 @@ namespace Umbraco.Cms.Web.Common.Macros
$"Executing PartialView: MacroSource=\"{model.MacroSource}\".",
"Executed PartialView.",
() => _partialViewMacroEngine.Execute(model, content),
() => _textService.Localize("errors/macroErrorLoadingPartialView", new[] { model.MacroSource }));
() => _textService.Localize("errors", "macroErrorLoadingPartialView", new[] { model.MacroSource }));
}