diff --git a/nuget.config b/nuget.config
new file mode 100644
index 000000000000..6aa8697b8fe9
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs
index 8232746ead3e..77561713b2de 100644
--- a/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs
@@ -4,15 +4,20 @@
namespace Umbraco.Cms.Core.Configuration.Models;
///
-/// Typed configuration options for imaging settings.
+/// Typed configuration options for imaging settings.
///
[UmbracoOptions(Constants.Configuration.ConfigImaging)]
public class ImagingSettings
{
///
- /// Gets or sets a value for imaging cache settings.
+ /// Gets or sets a value for the Hash-based Message Authentication Code (HMAC) secret key for request authentication.
///
- public ImagingCacheSettings Cache { get; set; } = new();
+ public byte[]? HMACSecretKey { get; set; }
+
+ ///
+ /// Gets or sets a value for imaging cache settings.
+ ///
+ public ImagingCacheSettings Cache { get; set; } = new ImagingCacheSettings();
///
/// Gets or sets a value for imaging resize settings.
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs
index 8f7901b2b4f1..b0b9e7a1aaea 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using System.Web;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
@@ -10,13 +11,13 @@
namespace Umbraco.Cms.Web.BackOffice.Controllers;
///
-/// A controller used to return images for media
+/// A controller used to return images for media.
///
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
public class ImagesController : UmbracoAuthorizedApiController
{
- private readonly IImageUrlGenerator _imageUrlGenerator;
private readonly MediaFileManager _mediaFileManager;
+ private readonly IImageUrlGenerator _imageUrlGenerator;
public ImagesController(
MediaFileManager mediaFileManager,
@@ -27,26 +28,26 @@ public ImagesController(
}
///
- /// Gets the big thumbnail image for the original image path
+ /// Gets the big thumbnail image for the original image path.
///
///
///
///
- /// If there is no original image is found then this will return not found.
+ /// If there is no original image is found then this will return not found.
///
- public IActionResult GetBigThumbnail(string originalImagePath) =>
- string.IsNullOrWhiteSpace(originalImagePath)
- ? Ok()
- : GetResized(originalImagePath, 500);
+ public IActionResult GetBigThumbnail(string originalImagePath)
+ => string.IsNullOrWhiteSpace(originalImagePath)
+ ? Ok()
+ : GetResized(originalImagePath, 500);
///
- /// Gets a resized image for the image at the given path
+ /// Gets a resized image for the image at the given path.
///
///
///
///
///
- /// If there is no media, image property or image file is found then this will return not found.
+ /// If there is no media, image property or image file is found then this will return not found.
///
public IActionResult GetResized(string imagePath, int width)
{
@@ -54,7 +55,6 @@ public IActionResult GetResized(string imagePath, int width)
// We cannot use the WebUtility, as we only want to encode the path, and not the entire string
var encodedImagePath = HttpUtility.UrlPathEncode(imagePath);
-
var ext = Path.GetExtension(encodedImagePath);
// check if imagePath is local to prevent open redirect
@@ -69,13 +69,13 @@ public IActionResult GetResized(string imagePath, int width)
return NotFound();
}
- // redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file
+ // Redirect to thumbnail with cache buster value generated from last modified time of original media file
DateTimeOffset? imageLastModified = null;
try
{
imageLastModified = _mediaFileManager.FileSystem.GetLastModified(imagePath);
}
- catch (Exception)
+ catch
{
// if we get an exception here it's probably because the image path being requested is an image that doesn't exist
// in the local media file system. This can happen if someone is storing an absolute path to an image online, which
@@ -83,23 +83,26 @@ public IActionResult GetResized(string imagePath, int width)
// so ignore and we won't set a last modified date.
}
- var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : null;
+ var cacheBusterValue = imageLastModified.HasValue ? imageLastModified.Value.ToFileTime().ToString("x", CultureInfo.InvariantCulture) : null;
var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(encodedImagePath)
{
Width = width,
ImageCropMode = ImageCropMode.Max,
- CacheBusterValue = rnd
+ CacheBusterValue = cacheBusterValue
});
+
if (Url.IsLocalUrl(imageUrl))
{
return new LocalRedirectResult(imageUrl, false);
}
-
- return Unauthorized();
+ else
+ {
+ return Unauthorized();
+ }
}
///
- /// Gets a processed image for the image at the given path
+ /// Gets a processed image for the image at the given path
///
///
///
@@ -107,14 +110,9 @@ public IActionResult GetResized(string imagePath, int width)
///
///
///
- ///
- ///
- ///
- ///
- ///
///
///
- /// If there is no media, image property or image file is found then this will return not found.
+ /// If there is no media, image property or image file is found then this will return not found.
///
public string? GetProcessedImageUrl(
string imagePath,
@@ -123,7 +121,7 @@ public IActionResult GetResized(string imagePath, int width)
decimal? focalPointLeft = null,
decimal? focalPointTop = null,
ImageCropMode mode = ImageCropMode.Max,
- string cacheBusterValue = "",
+ string cacheBusterValue = "", // TODO Change to: string? cacheBusterValue = null
decimal? cropX1 = null,
decimal? cropX2 = null,
decimal? cropY1 = null,
@@ -139,13 +137,11 @@ public IActionResult GetResized(string imagePath, int width)
if (focalPointLeft.HasValue && focalPointTop.HasValue)
{
- options.FocalPoint =
- new ImageUrlGenerationOptions.FocalPointPosition(focalPointLeft.Value, focalPointTop.Value);
+ options.FocalPoint = new ImageUrlGenerationOptions.FocalPointPosition(focalPointLeft.Value, focalPointTop.Value);
}
else if (cropX1.HasValue && cropX2.HasValue && cropY1.HasValue && cropY2.HasValue)
{
- options.Crop =
- new ImageUrlGenerationOptions.CropCoordinates(cropX1.Value, cropY1.Value, cropX2.Value, cropY2.Value);
+ options.Crop = new ImageUrlGenerationOptions.CropCoordinates(cropX1.Value, cropY1.Value, cropX2.Value, cropY2.Value);
}
return _imageUrlGenerator.GetImageUrl(options);
diff --git a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs
index 2b372992ccdd..64e6ca19a68e 100644
--- a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs
+++ b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs
@@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Headers;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using SixLabors.ImageSharp;
@@ -7,11 +9,12 @@
using SixLabors.ImageSharp.Web.Middleware;
using SixLabors.ImageSharp.Web.Processors;
using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Media;
namespace Umbraco.Cms.Web.Common.DependencyInjection;
///
-/// Configures the ImageSharp middleware options.
+/// Configures the ImageSharp middleware options.
///
///
public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions
@@ -20,7 +23,7 @@ public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The ImageSharp configuration.
/// The Umbraco imaging settings.
@@ -35,6 +38,7 @@ public void Configure(ImageSharpMiddlewareOptions options)
{
options.Configuration = _configuration;
+ options.HMACSecretKey = _imagingSettings.HMACSecretKey;
options.BrowserMaxAge = _imagingSettings.Cache.BrowserMaxAge;
options.CacheMaxAge = _imagingSettings.Cache.CacheMaxAge;
options.CacheHashLength = _imagingSettings.Cache.CacheHashLength;
@@ -42,22 +46,19 @@ public void Configure(ImageSharpMiddlewareOptions options)
// Use configurable maximum width and height
options.OnParseCommandsAsync = context =>
{
- if (context.Commands.Count == 0)
+ if (context.Commands.Count == 0 || _imagingSettings.HMACSecretKey?.Length > 0)
{
+ // Nothing to parse or using HMAC authentication
return Task.CompletedTask;
}
- var width = context.Parser.ParseValue(
- context.Commands.GetValueOrDefault(ResizeWebProcessor.Width),
- context.Culture);
+ int width = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture);
if (width <= 0 || width > _imagingSettings.Resize.MaxWidth)
{
context.Commands.Remove(ResizeWebProcessor.Width);
}
- var height = context.Parser.ParseValue(
- context.Commands.GetValueOrDefault(ResizeWebProcessor.Height),
- context.Culture);
+ int height = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), context.Culture);
if (height <= 0 || height > _imagingSettings.Resize.MaxHeight)
{
context.Commands.Remove(ResizeWebProcessor.Height);
@@ -73,11 +74,16 @@ public void Configure(ImageSharpMiddlewareOptions options)
{
ResponseHeaders headers = context.Response.GetTypedHeaders();
- CacheControlHeaderValue cacheControl =
- headers.CacheControl ?? new CacheControlHeaderValue { Public = true };
- cacheControl.MustRevalidate = false; // ImageSharp enables this by default
+ CacheControlHeaderValue cacheControl = headers.CacheControl ?? new CacheControlHeaderValue()
+ {
+ Public = true
+ };
+
+ // ImageSharp enables cache revalidation by default, so disable and add immutable directive
+ cacheControl.MustRevalidate = false;
cacheControl.Extensions.Add(new NameValueHeaderValue("immutable"));
+ // Set updated value
headers.CacheControl = cacheControl;
}
diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs
index e5e642f4a3f7..f67858c4b7de 100644
--- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs
+++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs
@@ -15,28 +15,23 @@ namespace Umbraco.Extensions;
public static partial class UmbracoBuilderExtensions
{
///
- /// Adds Image Sharp with Umbraco settings
+ /// Adds ImageSharp with Umbraco settings.
///
public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder)
{
builder.Services.AddSingleton();
+ // Add ImageSharp, replace default image provider and add custom processors
builder.Services.AddImageSharp()
-
- // Replace default image provider
.ClearProviders()
.AddProvider()
-
- // Add custom processors
.AddProcessor();
// Configure middleware
- builder.Services
- .AddTransient, ConfigureImageSharpMiddlewareOptions>();
+ builder.Services.AddTransient, ConfigureImageSharpMiddlewareOptions>();
// Configure cache options
- builder.Services
- .AddTransient, ConfigurePhysicalFileSystemCacheOptions>();
+ builder.Services.AddTransient, ConfigurePhysicalFileSystemCacheOptions>();
return builder.Services;
}
diff --git a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs
index 78a01dca2d00..676b05317e51 100644
--- a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs
+++ b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs
@@ -558,7 +558,7 @@ public static class ImageCropperTemplateCoreExtensions
}
var cacheBusterValue =
- cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null;
+ cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString("x", CultureInfo.InvariantCulture) : null;
return GetCropUrl(
mediaItemUrl,
diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs
index b1b96ddc1008..d442bee97111 100644
--- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs
+++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs
@@ -1,39 +1,57 @@
using System.Globalization;
using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Web;
using SixLabors.ImageSharp.Web.Processors;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Cms.Web.Common.ImageProcessors;
using static Umbraco.Cms.Core.Models.ImageUrlGenerationOptions;
namespace Umbraco.Cms.Web.Common.Media;
///
-/// Exposes a method that generates an image URL based on the specified options that can be processed by ImageSharp.
+/// Exposes a method that generates an image URL based on the specified options that can be processed by ImageSharp.
///
///
public class ImageSharpImageUrlGenerator : IImageUrlGenerator
{
+ private readonly ImageSharpRequestAuthorizationUtilities? _requestAuthorizationUtilities;
+
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The ImageSharp configuration.
+ /// Contains helpers that allow authorization of image requests.
+ public ImageSharpImageUrlGenerator(Configuration configuration, ImageSharpRequestAuthorizationUtilities? requestAuthorizationUtilities)
+ : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray(), requestAuthorizationUtilities)
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The ImageSharp configuration.
+ [Obsolete("Use ctor with all params - This will be removed in Umbraco 12.")]
public ImageSharpImageUrlGenerator(Configuration configuration)
- : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray())
- {
- }
+ : this(configuration, StaticServiceProvider.Instance.GetService())
+ { }
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The supported image file types/extensions.
+ /// Contains helpers that allow authorization of image requests.
///
- /// This constructor is only used for testing.
+ /// This constructor is only used for testing.
///
- internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes) =>
+ internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes, ImageSharpRequestAuthorizationUtilities? requestAuthorizationUtilities = null)
+ {
SupportedImageFileTypes = supportedImageFileTypes;
+ _requestAuthorizationUtilities = requestAuthorizationUtilities;
+ }
///
public IEnumerable SupportedImageFileTypes { get; }
@@ -49,47 +67,44 @@ internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes
var queryString = new Dictionary();
Dictionary furtherOptions = QueryHelpers.ParseQuery(options.FurtherOptions);
- if (options.Crop is not null)
+ if (options.Crop is CropCoordinates crop)
{
- CropCoordinates? crop = options.Crop;
- queryString.Add(
- CropWebProcessor.Coordinates,
- FormattableString.Invariant($"{crop.Left},{crop.Top},{crop.Right},{crop.Bottom}"));
+ queryString.Add(CropWebProcessor.Coordinates, FormattableString.Invariant($"{crop.Left},{crop.Top},{crop.Right},{crop.Bottom}"));
}
- if (options.FocalPoint is not null)
+ if (options.FocalPoint is FocalPointPosition focalPoint)
{
- queryString.Add(ResizeWebProcessor.Xy, FormattableString.Invariant($"{options.FocalPoint.Left},{options.FocalPoint.Top}"));
+ queryString.Add(ResizeWebProcessor.Xy, FormattableString.Invariant($"{focalPoint.Left},{focalPoint.Top}"));
}
- if (options.ImageCropMode is not null)
+ if (options.ImageCropMode is ImageCropMode imageCropMode)
{
- queryString.Add(ResizeWebProcessor.Mode, options.ImageCropMode.ToString()?.ToLowerInvariant());
+ queryString.Add(ResizeWebProcessor.Mode, imageCropMode.ToString().ToLowerInvariant());
}
- if (options.ImageCropAnchor is not null)
+ if (options.ImageCropAnchor is ImageCropAnchor imageCropAnchor)
{
- queryString.Add(ResizeWebProcessor.Anchor, options.ImageCropAnchor.ToString()?.ToLowerInvariant());
+ queryString.Add(ResizeWebProcessor.Anchor, imageCropAnchor.ToString().ToLowerInvariant());
}
- if (options.Width is not null)
+ if (options.Width is int width)
{
- queryString.Add(ResizeWebProcessor.Width, options.Width?.ToString(CultureInfo.InvariantCulture));
+ queryString.Add(ResizeWebProcessor.Width, width.ToString(CultureInfo.InvariantCulture));
}
- if (options.Height is not null)
+ if (options.Height is int height)
{
- queryString.Add(ResizeWebProcessor.Height, options.Height?.ToString(CultureInfo.InvariantCulture));
+ queryString.Add(ResizeWebProcessor.Height, height.ToString(CultureInfo.InvariantCulture));
}
-
+
if (furtherOptions.Remove(FormatWebProcessor.Format, out StringValues format))
{
- queryString.Add(FormatWebProcessor.Format, format[0]);
+ queryString.Add(FormatWebProcessor.Format, format.ToString());
}
- if (options.Quality is not null)
+ if (options.Quality is int quality)
{
- queryString.Add(QualityWebProcessor.Quality, options.Quality?.ToString(CultureInfo.InvariantCulture));
+ queryString.Add(QualityWebProcessor.Quality, quality.ToString(CultureInfo.InvariantCulture));
}
foreach (KeyValuePair kvp in furtherOptions)
@@ -97,9 +112,18 @@ internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes
queryString.Add(kvp.Key, kvp.Value);
}
- if (options.CacheBusterValue is not null && !string.IsNullOrWhiteSpace(options.CacheBusterValue))
+ if (options.CacheBusterValue is string cacheBusterValue && !string.IsNullOrEmpty(cacheBusterValue))
+ {
+ queryString.Add("v", cacheBusterValue);
+ }
+
+ if (_requestAuthorizationUtilities is not null)
{
- queryString.Add("rnd", options.CacheBusterValue);
+ var uri = QueryHelpers.AddQueryString(options.ImageUrl, queryString);
+ if (_requestAuthorizationUtilities.ComputeHMAC(uri, CommandHandling.Sanitize) is string token && !string.IsNullOrEmpty(token))
+ {
+ queryString.Add(ImageSharpRequestAuthorizationUtilities.TokenCommand, token);
+ }
}
return QueryHelpers.AddQueryString(options.ImageUrl, queryString);
diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj
index 8f7ea5f93578..db9ab1897d6d 100644
--- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj
+++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js
index e98a597e764b..14de3bb1c4a7 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js
@@ -313,7 +313,7 @@ function mediaHelper(umbRequestHelper, $http, $log) {
var thumbnailUrl = umbRequestHelper.getApiUrl(
"imagesApiBaseUrl",
"GetBigThumbnail",
- [{ originalImagePath: imagePath }]) + '&rnd=' + Math.random();
+ [{ originalImagePath: imagePath }]);
return thumbnailUrl;
},
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umbBlockCard.component.js b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umbBlockCard.component.js
index bb4c8c2a0877..1a8cfa9a38ab 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umbBlockCard.component.js
+++ b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umbBlockCard.component.js
@@ -14,7 +14,7 @@
}
});
- function BlockCardController($scope, umbRequestHelper) {
+ function BlockCardController($scope, umbRequestHelper, mediaHelper) {
const vm = this;
vm.styleBackgroundImage = "none";
@@ -45,8 +45,10 @@
var path = umbRequestHelper.convertVirtualToAbsolutePath(vm.blockConfigModel.thumbnail);
if (path.toLowerCase().endsWith(".svg") === false) {
- path += "?width=400";
+
+ path = mediaHelper.getThumbnailFromPath(path);
}
+
vm.styleBackgroundImage = `url('${path}')`;
};
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js
index b4d59c683c73..5d4776b8f426 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js
@@ -53,16 +53,8 @@
// they contain different data structures so if we need to query against it we need to be aware of this.
mediaHelper.registerFileResolver("Umbraco.UploadField", function (property, entity, thumbnail) {
if (thumbnail) {
- if (mediaHelper.detectIfImageByExtension(property.value)) {
- //get default big thumbnail from image processor
- var thumbnailUrl = property.value + "?width=500&rnd=" + moment(entity.updateDate).format("YYYYMMDDHHmmss");
- return thumbnailUrl;
- }
- else {
- return null;
- }
- }
- else {
+ return mediaHelper.getThumbnailFromPath(property.value);
+ } else {
return property.value;
}
});
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js
index 81a548a1169a..71519c524544 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js
@@ -1,10 +1,10 @@
angular.module("umbraco")
.controller("Umbraco.PropertyEditors.Grid.MediaController",
- function ($scope, userService, editorService, localizationService) {
+ function ($scope, userService, editorService, localizationService, mediaHelper) {
$scope.control.icon = $scope.control.icon || 'icon-picture';
- $scope.thumbnailUrl = getThumbnailUrl();
+ updateThumbnailUrl();
if (!$scope.model.config.startNodeId) {
if ($scope.model.config.ignoreUserStartNodes === true) {
@@ -61,40 +61,31 @@ angular.module("umbraco")
/**
*
*/
- function getThumbnailUrl() {
-
+ function updateThumbnailUrl() {
if ($scope.control.value && $scope.control.value.image) {
- var url = $scope.control.value.image;
-
- if ($scope.control.editor.config && $scope.control.editor.config.size){
- if ($scope.control.value.coordinates) {
- // New way, crop by percent must come before width/height.
- var coords = $scope.control.value.coordinates;
- url += `?cc=${coords.x1},${coords.y1},${coords.x2},${coords.y2}`;
- } else {
- // Here in order not to break existing content where focalPoint were used.
- if ($scope.control.value.focalPoint) {
- url += `?rxy=${$scope.control.value.focalPoint.left},${$scope.control.value.focalPoint.top}`;
- } else {
- // Prevent black padding and no crop when focal point not set / changed from default
- url += '?rxy=0.5,0.5';
- }
- }
-
- url += '&width=' + $scope.control.editor.config.size.width;
- url += '&height=' + $scope.control.editor.config.size.height;
+ var options = {
+ width: 800
+ };
+
+ if ($scope.control.value.coordinates) {
+ // Use crop
+ options.crop = $scope.control.value.coordinates;
+ } else if ($scope.control.value.focalPoint) {
+ // Otherwise use focal point
+ options.focalPoint = $scope.control.value.focalPoint;
}
- // set default size if no crop present (moved from the view)
- if (url.includes('?') === false)
- {
- url += '?width=800'
+ if ($scope.control.editor.config && $scope.control.editor.config.size) {
+ options.width = $scope.control.editor.config.size.width;
+ options.height = $scope.control.editor.config.size.height;
}
- return url;
+ mediaHelper.getProcessedImageUrl($scope.control.value.image, options).then(imageUrl => {
+ $scope.thumbnailUrl = imageUrl;
+ });
+ } else {
+ $scope.thumbnailUrl = null;
}
-
- return null;
}
/**
@@ -113,6 +104,7 @@ angular.module("umbraco")
caption: selectedImage.caption,
altText: selectedImage.altText
};
- $scope.thumbnailUrl = getThumbnailUrl();
+
+ updateThumbnailUrl();
}
});
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js
index b5131e993875..453347bc1bb6 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js
@@ -236,9 +236,8 @@ angular.module('umbraco')
if (property.value && property.value.src) {
if (thumbnail === true) {
- return property.value.src + "?width=500";
- }
- else {
+ return mediaHelper.getThumbnailFromPath(property.value.src);
+ } else {
return property.value.src;
}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs
index 093e9b075c27..09aee65b2384 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs
@@ -1,7 +1,15 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Linq;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
using NUnit.Framework;
+using SixLabors.ImageSharp.Web;
+using SixLabors.ImageSharp.Web.Commands;
+using SixLabors.ImageSharp.Web.Commands.Converters;
+using SixLabors.ImageSharp.Web.Middleware;
+using SixLabors.ImageSharp.Web.Processors;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Web.Common.Media;
@@ -11,56 +19,62 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Media;
public class ImageSharpImageUrlGeneratorTests
{
private const string MediaPath = "/media/1005/img_0671.jpg";
-
- private static readonly ImageUrlGenerationOptions.CropCoordinates s_crop = new(0.58729977382575338m, 0.055768992440203169m, 0m, 0.32457553600198386m);
-
- private static readonly ImageUrlGenerationOptions.FocalPointPosition s_focus1 = new(0.96m, 0.80827067669172936m);
- private static readonly ImageUrlGenerationOptions.FocalPointPosition s_focus2 = new(0.4275m, 0.41m);
- private static readonly ImageSharpImageUrlGenerator s_generator = new(new string[0]);
+ private static readonly ImageUrlGenerationOptions.CropCoordinates _crop = new ImageUrlGenerationOptions.CropCoordinates(0.58729977382575338m, 0.055768992440203169m, 0m, 0.32457553600198386m);
+ private static readonly ImageUrlGenerationOptions.FocalPointPosition _focus1 = new ImageUrlGenerationOptions.FocalPointPosition(0.96m, 0.80827067669172936m);
+ private static readonly ImageUrlGenerationOptions.FocalPointPosition _focus2 = new ImageUrlGenerationOptions.FocalPointPosition(0.4275m, 0.41m);
+ private static readonly ImageSharpImageUrlGenerator _generator = new ImageSharpImageUrlGenerator(new string[0]);
[Test]
public void GetImageUrl_CropAliasTest()
{
- var urlString =
- s_generator.GetImageUrl(
- new ImageUrlGenerationOptions(MediaPath) { Crop = s_crop, Width = 100, Height = 100 });
- Assert.AreEqual(
- MediaPath + "?cc=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&width=100&height=100",
- urlString);
+ var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ {
+ Crop = _crop,
+ Width = 100,
+ Height = 100,
+ });
+
+ Assert.AreEqual(MediaPath + "?cc=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&width=100&height=100", urlString);
}
[Test]
public void GetImageUrl_WidthHeightTest()
{
- var urlString =
- s_generator.GetImageUrl(
- new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_focus1, Width = 200, Height = 300 });
+ var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ {
+ FocalPoint = _focus1,
+ Width = 200,
+ Height = 300,
+ });
+
Assert.AreEqual(MediaPath + "?rxy=0.96,0.80827067669172936&width=200&height=300", urlString);
}
[Test]
public void GetImageUrl_FocalPointTest()
{
- var urlString =
- s_generator.GetImageUrl(
- new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_focus1, Width = 100, Height = 100 });
+ var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ {
+ FocalPoint = _focus1,
+ Width = 100,
+ Height = 100,
+ });
+
Assert.AreEqual(MediaPath + "?rxy=0.96,0.80827067669172936&width=100&height=100", urlString);
}
[Test]
public void GetImageUrlFurtherOptionsTest()
{
- var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
{
- FocalPoint = s_focus1,
+ FocalPoint = _focus1,
Width = 200,
Height = 300,
FurtherOptions = "&filter=comic&roundedcorners=radius-26|bgcolor-fff",
});
- Assert.AreEqual(
- MediaPath +
- "?rxy=0.96,0.80827067669172936&width=200&height=300&filter=comic&roundedcorners=radius-26%7Cbgcolor-fff",
- urlString);
+
+ Assert.AreEqual(MediaPath + "?rxy=0.96,0.80827067669172936&width=200&height=300&filter=comic&roundedcorners=radius-26%7Cbgcolor-fff", urlString);
}
[Test]
@@ -91,90 +105,91 @@ public void GetImageUrlFurtherOptionsWithModeAndQualityTest()
}
///
- /// Test that if options is null, the generated image URL is also null.
+ /// Test that if options is null, the generated image URL is also null.
///
[Test]
public void GetImageUrlNullOptionsTest()
{
- var urlString = s_generator.GetImageUrl(null);
+ var urlString = _generator.GetImageUrl(null);
Assert.AreEqual(null, urlString);
}
///
- /// Test that if the image URL is null, the generated image URL is also null.
+ /// Test that if the image URL is null, the generated image URL is also null.
///
[Test]
public void GetImageUrlNullTest()
{
- var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(null));
+ var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(null));
Assert.AreEqual(null, urlString);
}
///
- /// Test that if the image URL is empty, the generated image URL is empty.
+ /// Test that if the image URL is empty, the generated image URL is empty.
///
[Test]
public void GetImageUrlEmptyTest()
{
- var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty));
+ var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty));
Assert.AreEqual(string.Empty, urlString);
}
///
- /// Test the GetImageUrl method on the ImageCropDataSet Model
+ /// Test the GetImageUrl method on the ImageCropDataSet Model
///
[Test]
public void GetBaseCropUrlFromModelTest()
{
- var urlString =
- s_generator.GetImageUrl(
- new ImageUrlGenerationOptions(string.Empty) { Crop = s_crop, Width = 100, Height = 100 });
- Assert.AreEqual(
- "?cc=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&width=100&height=100",
- urlString);
+ var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty)
+ {
+ Crop = _crop,
+ Width = 100,
+ Height = 100,
+ });
+
+ Assert.AreEqual("?cc=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&width=100&height=100", urlString);
}
///
- /// Test that if Crop mode is specified as anything other than Crop the image doesn't use the crop
+ /// Test that if Crop mode is specified as anything other than Crop the image doesn't use the crop
///
[Test]
public void GetImageUrl_SpecifiedCropModeTest()
{
- var urlStringMin =
- s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
- {
- ImageCropMode = ImageCropMode.Min,
- Width = 300,
- Height = 150,
- });
- var urlStringBoxPad =
- s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
- {
- ImageCropMode = ImageCropMode.BoxPad,
- Width = 300,
- Height = 150,
- });
- var urlStringPad =
- s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
- {
- ImageCropMode = ImageCropMode.Pad,
- Width = 300,
- Height = 150,
- });
- var urlStringMax =
- s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
- {
- ImageCropMode = ImageCropMode.Max,
- Width = 300,
- Height = 150,
- });
- var urlStringStretch =
- s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
- {
- ImageCropMode = ImageCropMode.Stretch,
- Width = 300,
- Height = 150,
- });
+ var urlStringMin = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ {
+ ImageCropMode = ImageCropMode.Min,
+ Width = 300,
+ Height = 150,
+ });
+
+ var urlStringBoxPad = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ {
+ ImageCropMode = ImageCropMode.BoxPad,
+ Width = 300,
+ Height = 150,
+ });
+
+ var urlStringPad = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ {
+ ImageCropMode = ImageCropMode.Pad,
+ Width = 300,
+ Height = 150,
+ });
+
+ var urlStringMax = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ {
+ ImageCropMode = ImageCropMode.Max,
+ Width = 300,
+ Height = 150,
+ });
+
+ var urlStringStretch = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ {
+ ImageCropMode = ImageCropMode.Stretch,
+ Width = 300,
+ Height = 150,
+ });
Assert.AreEqual(MediaPath + "?rmode=min&width=300&height=150", urlStringMin);
Assert.AreEqual(MediaPath + "?rmode=boxpad&width=300&height=150", urlStringBoxPad);
@@ -184,76 +199,136 @@ public void GetImageUrl_SpecifiedCropModeTest()
}
///
- /// Test for upload property type
+ /// Test for upload property type
///
[Test]
public void GetImageUrl_UploadTypeTest()
{
- var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
{
ImageCropMode = ImageCropMode.Crop,
ImageCropAnchor = ImageCropAnchor.Center,
Width = 100,
Height = 270,
});
+
Assert.AreEqual(MediaPath + "?rmode=crop&ranchor=center&width=100&height=270", urlString);
}
///
- /// Test for preferFocalPoint when focal point is centered
+ /// Test for preferFocalPoint when focal point is centered
///
[Test]
public void GetImageUrl_PreferFocalPointCenter()
{
- var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Width = 300, Height = 150 });
+ var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ {
+ Width = 300,
+ Height = 150,
+ });
+
Assert.AreEqual(MediaPath + "?width=300&height=150", urlString);
}
///
- /// Test to check if crop ratio is ignored if useCropDimensions is true
+ /// Test to check if crop ratio is ignored if useCropDimensions is true
///
[Test]
public void GetImageUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore()
{
- var urlString =
- s_generator.GetImageUrl(
- new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_focus2, Width = 270, Height = 161 });
+ var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ {
+ FocalPoint = _focus2,
+ Width = 270,
+ Height = 161,
+ });
+
Assert.AreEqual(MediaPath + "?rxy=0.4275,0.41&width=270&height=161", urlString);
}
///
- /// Test to check result when only a width parameter is passed, effectivly a resize only
+ /// Test to check result when only a width parameter is passed, effectivly a resize only
///
[Test]
public void GetImageUrl_WidthOnlyParameter()
{
- var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Width = 200 });
+ var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ {
+ Width = 200,
+ });
+
Assert.AreEqual(MediaPath + "?width=200", urlString);
}
///
- /// Test to check result when only a height parameter is passed, effectivly a resize only
+ /// Test to check result when only a height parameter is passed, effectivly a resize only
///
[Test]
public void GetImageUrl_HeightOnlyParameter()
{
- var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Height = 200 });
+ var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ {
+ Height = 200,
+ });
+
Assert.AreEqual(MediaPath + "?height=200", urlString);
}
///
- /// Test to check result when using a background color with padding
+ /// Test to check result when using a background color with padding
///
[Test]
public void GetImageUrl_BackgroundColorParameter()
{
- var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
+ var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath)
{
ImageCropMode = ImageCropMode.Pad,
Width = 400,
Height = 400,
FurtherOptions = "&bgcolor=fff",
});
+
Assert.AreEqual(MediaPath + "?rmode=pad&width=400&height=400&bgcolor=fff", urlString);
}
+
+ ///
+ /// Test to check result when using a HMAC security key.
+ ///
+ [Test]
+ public void GetImageUrl_HMACSecurityKey()
+ {
+ var requestAuthorizationUtilities = new ImageSharpRequestAuthorizationUtilities(
+ Options.Create(new ImageSharpMiddlewareOptions()
+ {
+ HMACSecretKey = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 }
+ }),
+ new QueryCollectionRequestParser(),
+ new[]
+ {
+ new ResizeWebProcessor()
+ },
+ new CommandParser(Enumerable.Empty()),
+ new ServiceCollection().BuildServiceProvider());
+
+ var generator = new ImageSharpImageUrlGenerator(new string[0], requestAuthorizationUtilities);
+ var options = new ImageUrlGenerationOptions(MediaPath)
+ {
+ Width = 400,
+ Height = 400,
+ };
+
+ Assert.AreEqual(MediaPath + "?width=400&height=400&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2", generator.GetImageUrl(options));
+
+ // CacheBusterValue isn't included in HMAC generation
+ options.CacheBusterValue = "not-included-in-hmac";
+ Assert.AreEqual(MediaPath + "?width=400&height=400&v=not-included-in-hmac&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2", generator.GetImageUrl(options));
+
+ // Removing height should generate a different HMAC
+ options.Height = null;
+ Assert.AreEqual(MediaPath + "?width=400&v=not-included-in-hmac&hmac=5bd24a05de5ea068533579863773ddac9269482ad515575be4aace7e9e50c88c", generator.GetImageUrl(options));
+
+ // But adding it again using FurtherOptions should include it (and produce the same HMAC as before)
+ options.FurtherOptions = "height=400";
+ Assert.AreEqual(MediaPath + "?width=400&height=400&v=not-included-in-hmac&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2", generator.GetImageUrl(options));
+ }
}