From ff153cdc408114542ae56816813bbb93c5df41f4 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 28 Mar 2022 00:08:54 +0200 Subject: [PATCH 01/37] Update to ImageSharp 2.1.0 and ImageSharp.Web 2.0.0-alpha.0.23 --- NuGet.config | 6 ++++++ src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj | 2 +- .../DependencyInjection/UmbracoBuilder.ImageSharp.cs | 2 +- .../ImageProcessors/CropWebProcessor.cs | 9 ++++++--- src/Umbraco.Web.Common/Umbraco.Web.Common.csproj | 2 +- .../ImageProcessors/CropWebProcessorTests.cs | 2 +- 6 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 NuGet.config 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.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 8bc4956c2f3d..40cda9903844 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -48,7 +48,7 @@ - + diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 30331fd812fd..82a517cc9cce 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -32,7 +32,7 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build // options.Configuration is set using ImageSharpConfigurationOptions below options.BrowserMaxAge = imagingSettings.Cache.BrowserMaxAge; options.CacheMaxAge = imagingSettings.Cache.CacheMaxAge; - options.CachedNameLength = imagingSettings.Cache.CachedNameLength; + options.CacheHashLength = imagingSettings.Cache.CachedNameLength; // Use configurable maximum width and height (overwrite ImageSharps default) options.OnParseCommandsAsync = context => diff --git a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs index 7b3cc817f232..5d6d8062b594 100644 --- a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs +++ b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs @@ -27,7 +27,7 @@ public class CropWebProcessor : IImageWebProcessor }; /// - public FormattedImage Process(FormattedImage image, ILogger logger, IDictionary commands, CommandParser parser, CultureInfo culture) + public FormattedImage Process(FormattedImage image, ILogger logger, CommandCollection commands, CommandParser parser, CultureInfo culture) { RectangleF? coordinates = GetCoordinates(commands, parser, culture); if (coordinates != null) @@ -41,14 +41,17 @@ public FormattedImage Process(FormattedImage image, ILogger logger, IDictionary< int height = (int)MathF.Round(coordinates.Value.Height * sourceHeight); var cropRectangle = new Rectangle(x, y, width, height); - + image.Image.Mutate(x => x.Crop(cropRectangle)); } return image; } - private static RectangleF? GetCoordinates(IDictionary commands, CommandParser parser, CultureInfo culture) + /// + public bool RequiresTrueColorPixelFormat(CommandCollection commands, CommandParser parser, CultureInfo culture) => false; + + private static RectangleF? GetCoordinates(CommandCollection commands, CommandParser parser, CultureInfo culture) { float[] coordinates = parser.ParseValue(commands.GetValueOrDefault(Coordinates), culture); diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index b023b5ecdc81..545bb05308ae 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -34,7 +34,7 @@ - + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs index 2c508d97d200..a5811c068165 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs @@ -31,7 +31,7 @@ public void CropWebProcessor_CropsImage() var parser = new CommandParser(converters); CultureInfo culture = CultureInfo.InvariantCulture; - var commands = new Dictionary + var commands = new CommandCollection { { CropWebProcessor.Coordinates, "0.1,0.2,0.1,0.4" }, // left, top, right, bottom }; From e8d82af8d29cd69ea62af14887fef288cbb444f0 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 28 Mar 2022 00:15:58 +0200 Subject: [PATCH 02/37] Rename CachedNameLength to CacheHashLength and add CacheFolderDepth setting --- .../Models/ImagingCacheSettings.cs | 19 +++++++++++++------ .../UmbracoBuilder.ImageSharp.cs | 10 ++++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs index cd7d2fda1bc7..b3bdddc211d3 100644 --- a/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ImagingCacheSettings.cs @@ -14,8 +14,9 @@ public class ImagingCacheSettings { internal const string StaticBrowserMaxAge = "7.00:00:00"; internal const string StaticCacheMaxAge = "365.00:00:00"; - internal const int StaticCachedNameLength = 8; - internal const string StaticCacheFolder = Constants.SystemDirectories.TempData + "/MediaCache"; + internal const int StaticCacheHashLength = 12; + internal const int StaticCacheFolderDepth = 8; + internal const string StaticCacheFolder = Constants.SystemDirectories.TempData + "/MediaCache"; /// /// Gets or sets a value for the browser image cache maximum age. @@ -30,13 +31,19 @@ public class ImagingCacheSettings public TimeSpan CacheMaxAge { get; set; } = TimeSpan.Parse(StaticCacheMaxAge); /// - /// Gets or sets a value for length of the cached name. + /// Gets or sets a value for the image cache hash length. /// - [DefaultValue(StaticCachedNameLength)] - public uint CachedNameLength { get; set; } = StaticCachedNameLength; + [DefaultValue(StaticCacheHashLength)] + public uint CacheHashLength { get; set; } = StaticCacheHashLength; /// - /// Gets or sets a value for the cache folder. + /// Gets or sets a value for the image cache folder depth. + /// + [DefaultValue(StaticCacheFolderDepth)] + public uint CacheFolderDepth { get; set; } = StaticCacheFolderDepth; + + /// + /// Gets or sets a value for the image cache folder. /// [DefaultValue(StaticCacheFolder)] public string CacheFolder { get; set; } = StaticCacheFolder; diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 82a517cc9cce..7f47aad30ac8 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -32,7 +32,7 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build // options.Configuration is set using ImageSharpConfigurationOptions below options.BrowserMaxAge = imagingSettings.Cache.BrowserMaxAge; options.CacheMaxAge = imagingSettings.Cache.CacheMaxAge; - options.CacheHashLength = imagingSettings.Cache.CachedNameLength; + options.CacheHashLength = imagingSettings.Cache.CacheHashLength; // Use configurable maximum width and height (overwrite ImageSharps default) options.OnParseCommandsAsync = context => @@ -71,7 +71,13 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build return Task.CompletedTask; }; }) - .Configure(options => options.CacheFolder = builder.BuilderHostingEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder)) + // Configure cache options + .Configure(options => + { + options.CacheFolder = builder.BuilderHostingEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder); + options.CacheFolderDepth = imagingSettings.Cache.CacheFolderDepth; + }) + // Add custom processors .AddProcessor(); // Configure middleware to use the registered/shared ImageSharp configuration From 5fa02e936dcd80f7998747a1818773ca71a82d07 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 28 Mar 2022 00:17:21 +0200 Subject: [PATCH 03/37] Replace PhysicalFileSystemProvider with WebRootImageProvider --- .../UmbracoBuilder.ImageSharp.cs | 4 ++ .../ImageProviders/FileInfoImageResolver.cs | 34 ++++++++++ .../FileProviderImageProvider.cs | 62 +++++++++++++++++++ .../ImageProviders/WebRootImageProvider.cs | 20 ++++++ 4 files changed, 120 insertions(+) create mode 100644 src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs create mode 100644 src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs create mode 100644 src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 7f47aad30ac8..c05344d7d140 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -10,10 +10,12 @@ using SixLabors.ImageSharp.Web.DependencyInjection; using SixLabors.ImageSharp.Web.Middleware; using SixLabors.ImageSharp.Web.Processors; +using SixLabors.ImageSharp.Web.Providers; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.ImageProcessors; +using Umbraco.Cms.Web.Common.ImageProviders; namespace Umbraco.Extensions { @@ -71,6 +73,8 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build return Task.CompletedTask; }; }) + // Replace default image provider + .RemoveProvider().AddProvider() // Configure cache options .Configure(options => { diff --git a/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs b/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs new file mode 100644 index 000000000000..3e6050b4c5e2 --- /dev/null +++ b/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs @@ -0,0 +1,34 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.FileProviders; +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Resolvers; + +namespace Umbraco.Cms.Web.Common.ImageProviders +{ + /// + /// Provides means to manage image buffers from an instance. + /// + public class FileInfoImageResolver : IImageResolver + { + private readonly IFileInfo _fileInfo; + private readonly ImageMetadata _metadata; + + /// + /// Initializes a new instance of the class. + /// + /// The file info. + /// The image metadata associated with this file. + public FileInfoImageResolver(IFileInfo fileInfo, in ImageMetadata metadata) + { + _fileInfo = fileInfo; + _metadata = metadata; + } + + /// + public Task GetMetaDataAsync() => Task.FromResult(_metadata); + + /// + public Task OpenReadAsync() => Task.FromResult(_fileInfo.CreateReadStream()); + } +} diff --git a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs new file mode 100644 index 000000000000..1b5002b9ded4 --- /dev/null +++ b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.FileProviders; +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Providers; +using SixLabors.ImageSharp.Web.Resolvers; + +namespace Umbraco.Cms.Web.Common.ImageProviders +{ + /// + /// Returns images from an abstraction. + /// + public class FileProviderImageProvider : IImageProvider + { + /// + /// The file provider abstraction. + /// + private readonly IFileProvider _fileProvider; + + /// + /// Contains various format helper methods based on the current configuration. + /// + private readonly FormatUtilities _formatUtilities; + + /// + /// Initializes a new instance of the class. + /// + /// The file provider. + /// Contains various format helper methods based on the current configuration. + public FileProviderImageProvider(IFileProvider fileProvider, FormatUtilities formatUtilities) + { + _fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider)); + _formatUtilities = formatUtilities ?? throw new ArgumentNullException(nameof(formatUtilities)); + } + + /// + public ProcessingBehavior ProcessingBehavior { get; protected set; } = ProcessingBehavior.CommandOnly; + + /// + public Func Match { get; set; } = _ => true; + + /// + public virtual bool IsValidRequest(HttpContext context) + => _formatUtilities.TryGetExtensionFromUri(context.Request.GetDisplayUrl(), out _); + + /// + public virtual Task GetAsync(HttpContext context) + { + IFileInfo fileInfo = _fileProvider.GetFileInfo(context.Request.Path); + if (!fileInfo.Exists) + { + return Task.FromResult(null); + } + + var metadata = new ImageMetadata(fileInfo.LastModified.UtcDateTime, fileInfo.Length); + + return Task.FromResult(new FileInfoImageResolver(fileInfo, metadata)); + } + } +} diff --git a/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs new file mode 100644 index 000000000000..a21762e81ef6 --- /dev/null +++ b/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using SixLabors.ImageSharp.Web; + +namespace Umbraco.Cms.Web.Common.ImageProviders +{ + /// + /// Returns images from the web root file provider. + /// + public sealed class WebRootImageProvider : FileProviderImageProvider + { + /// + /// Initializes a new instance of the class. + /// + /// The web host environment. + /// Contains various format helper methods based on the current configuration. + public WebRootImageProvider(IWebHostEnvironment environment, FormatUtilities formatUtilities) + : base(environment?.WebRootFileProvider, formatUtilities) + { } + } +} From da34c0906164f0a2c1317e26aac2312df4ed3d32 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 28 Mar 2022 10:56:02 +0200 Subject: [PATCH 04/37] Support EXIF-orientation in image dimention extractor --- .../Media/ImageSharpDimensionExtractor.cs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs b/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs index 227d9a653ce4..27f49b89558d 100644 --- a/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs +++ b/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs @@ -1,5 +1,7 @@ +using System; using System.IO; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; using Umbraco.Cms.Core.Media; using Size = System.Drawing.Size; @@ -30,10 +32,40 @@ public ImageSharpDimensionExtractor(Configuration configuration) IImageInfo imageInfo = Image.Identify(_configuration, stream); if (imageInfo != null) { - size = new Size(imageInfo.Width, imageInfo.Height); + size = IsExifOrientationRotated(imageInfo) + ? new Size(imageInfo.Height, imageInfo.Width) + : new Size(imageInfo.Width, imageInfo.Height); } return size; } + + private static bool IsExifOrientationRotated(IImageInfo imageInfo) + => GetExifOrientation(imageInfo) switch + { + ExifOrientationMode.LeftTop + or ExifOrientationMode.RightTop + or ExifOrientationMode.RightBottom + or ExifOrientationMode.LeftBottom => true, + _ => false, + }; + + private static ushort GetExifOrientation(IImageInfo imageInfo) + { + IExifValue orientation = imageInfo.Metadata.ExifProfile?.GetValue(ExifTag.Orientation); + if (orientation is not null) + { + if (orientation.DataType == ExifDataType.Short) + { + return orientation.Value; + } + else + { + return Convert.ToUInt16(orientation.Value); + } + } + + return ExifOrientationMode.Unknown; + } } } From 91fbdb27b88f3ab3085f9cd58a0d761666f09e88 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 31 Mar 2022 00:31:11 +0200 Subject: [PATCH 05/37] Remove virtual methods on FileProviderImageProvider --- .../ImageProviders/FileProviderImageProvider.cs | 4 ++-- src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs index 1b5002b9ded4..44a2795a00d9 100644 --- a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs +++ b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs @@ -42,11 +42,11 @@ public FileProviderImageProvider(IFileProvider fileProvider, FormatUtilities for public Func Match { get; set; } = _ => true; /// - public virtual bool IsValidRequest(HttpContext context) + public bool IsValidRequest(HttpContext context) => _formatUtilities.TryGetExtensionFromUri(context.Request.GetDisplayUrl(), out _); /// - public virtual Task GetAsync(HttpContext context) + public Task GetAsync(HttpContext context) { IFileInfo fileInfo = _fileProvider.GetFileInfo(context.Request.Path); if (!fileInfo.Exists) diff --git a/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs index a21762e81ef6..73605a6b7cc1 100644 --- a/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs +++ b/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs @@ -11,10 +11,10 @@ public sealed class WebRootImageProvider : FileProviderImageProvider /// /// Initializes a new instance of the class. /// - /// The web host environment. + /// The web hosting environment. /// Contains various format helper methods based on the current configuration. public WebRootImageProvider(IWebHostEnvironment environment, FormatUtilities formatUtilities) - : base(environment?.WebRootFileProvider, formatUtilities) + : base(environment.WebRootFileProvider, formatUtilities) { } } } From 827ae60ec6c4929e8560a3ae8b39c46c325320fb Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 31 Mar 2022 00:33:08 +0200 Subject: [PATCH 06/37] Simplify FileInfoImageResolver --- .../ImageProviders/FileInfoImageResolver.cs | 11 +++-------- .../ImageProviders/FileProviderImageProvider.cs | 4 +--- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs b/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs index 3e6050b4c5e2..4275cd137f09 100644 --- a/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs +++ b/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs @@ -12,21 +12,16 @@ namespace Umbraco.Cms.Web.Common.ImageProviders public class FileInfoImageResolver : IImageResolver { private readonly IFileInfo _fileInfo; - private readonly ImageMetadata _metadata; /// /// Initializes a new instance of the class. /// /// The file info. - /// The image metadata associated with this file. - public FileInfoImageResolver(IFileInfo fileInfo, in ImageMetadata metadata) - { - _fileInfo = fileInfo; - _metadata = metadata; - } + public FileInfoImageResolver(IFileInfo fileInfo) + => _fileInfo = fileInfo; /// - public Task GetMetaDataAsync() => Task.FromResult(_metadata); + public Task GetMetaDataAsync() => Task.FromResult(new ImageMetadata(_fileInfo.LastModified.UtcDateTime, _fileInfo.Length)); /// public Task OpenReadAsync() => Task.FromResult(_fileInfo.CreateReadStream()); diff --git a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs index 44a2795a00d9..f41c6ba8502b 100644 --- a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs +++ b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs @@ -54,9 +54,7 @@ public Task GetAsync(HttpContext context) return Task.FromResult(null); } - var metadata = new ImageMetadata(fileInfo.LastModified.UtcDateTime, fileInfo.Length); - - return Task.FromResult(new FileInfoImageResolver(fileInfo, metadata)); + return Task.FromResult(new FileInfoImageResolver(fileInfo)); } } } From 8efb5269f98c1170eee0baa2882d8a91d508fc7d Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 4 Apr 2022 23:34:06 +0200 Subject: [PATCH 07/37] Update to SixLabors.ImageSharp.Web 2.0.0-alpha.0.25 and remove custom providers --- .../UmbracoBuilder.ImageSharp.cs | 3 +- .../ImageProviders/FileInfoImageResolver.cs | 29 --------- .../FileProviderImageProvider.cs | 60 ------------------- .../ImageProviders/WebRootImageProvider.cs | 20 ------- .../Umbraco.Web.Common.csproj | 2 +- 5 files changed, 2 insertions(+), 112 deletions(-) delete mode 100644 src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs delete mode 100644 src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs delete mode 100644 src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index c05344d7d140..25dfe7e5cd03 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -15,7 +15,6 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.ImageProcessors; -using Umbraco.Cms.Web.Common.ImageProviders; namespace Umbraco.Extensions { @@ -74,7 +73,7 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build }; }) // Replace default image provider - .RemoveProvider().AddProvider() + .ClearProviders().AddProvider() // Configure cache options .Configure(options => { diff --git a/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs b/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs deleted file mode 100644 index 4275cd137f09..000000000000 --- a/src/Umbraco.Web.Common/ImageProviders/FileInfoImageResolver.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using Microsoft.Extensions.FileProviders; -using SixLabors.ImageSharp.Web; -using SixLabors.ImageSharp.Web.Resolvers; - -namespace Umbraco.Cms.Web.Common.ImageProviders -{ - /// - /// Provides means to manage image buffers from an instance. - /// - public class FileInfoImageResolver : IImageResolver - { - private readonly IFileInfo _fileInfo; - - /// - /// Initializes a new instance of the class. - /// - /// The file info. - public FileInfoImageResolver(IFileInfo fileInfo) - => _fileInfo = fileInfo; - - /// - public Task GetMetaDataAsync() => Task.FromResult(new ImageMetadata(_fileInfo.LastModified.UtcDateTime, _fileInfo.Length)); - - /// - public Task OpenReadAsync() => Task.FromResult(_fileInfo.CreateReadStream()); - } -} diff --git a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs deleted file mode 100644 index f41c6ba8502b..000000000000 --- a/src/Umbraco.Web.Common/ImageProviders/FileProviderImageProvider.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.FileProviders; -using SixLabors.ImageSharp.Web; -using SixLabors.ImageSharp.Web.Providers; -using SixLabors.ImageSharp.Web.Resolvers; - -namespace Umbraco.Cms.Web.Common.ImageProviders -{ - /// - /// Returns images from an abstraction. - /// - public class FileProviderImageProvider : IImageProvider - { - /// - /// The file provider abstraction. - /// - private readonly IFileProvider _fileProvider; - - /// - /// Contains various format helper methods based on the current configuration. - /// - private readonly FormatUtilities _formatUtilities; - - /// - /// Initializes a new instance of the class. - /// - /// The file provider. - /// Contains various format helper methods based on the current configuration. - public FileProviderImageProvider(IFileProvider fileProvider, FormatUtilities formatUtilities) - { - _fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider)); - _formatUtilities = formatUtilities ?? throw new ArgumentNullException(nameof(formatUtilities)); - } - - /// - public ProcessingBehavior ProcessingBehavior { get; protected set; } = ProcessingBehavior.CommandOnly; - - /// - public Func Match { get; set; } = _ => true; - - /// - public bool IsValidRequest(HttpContext context) - => _formatUtilities.TryGetExtensionFromUri(context.Request.GetDisplayUrl(), out _); - - /// - public Task GetAsync(HttpContext context) - { - IFileInfo fileInfo = _fileProvider.GetFileInfo(context.Request.Path); - if (!fileInfo.Exists) - { - return Task.FromResult(null); - } - - return Task.FromResult(new FileInfoImageResolver(fileInfo)); - } - } -} diff --git a/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs b/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs deleted file mode 100644 index 73605a6b7cc1..000000000000 --- a/src/Umbraco.Web.Common/ImageProviders/WebRootImageProvider.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using SixLabors.ImageSharp.Web; - -namespace Umbraco.Cms.Web.Common.ImageProviders -{ - /// - /// Returns images from the web root file provider. - /// - public sealed class WebRootImageProvider : FileProviderImageProvider - { - /// - /// Initializes a new instance of the class. - /// - /// The web hosting environment. - /// Contains various format helper methods based on the current configuration. - public WebRootImageProvider(IWebHostEnvironment environment, FormatUtilities formatUtilities) - : base(environment.WebRootFileProvider, formatUtilities) - { } - } -} diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 545bb05308ae..2ad624e9e3be 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -34,7 +34,7 @@ - + From d35dfc6d68d40b39768ffe8c58ed5b90fb821c1a Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 4 Apr 2022 23:35:04 +0200 Subject: [PATCH 08/37] Make CropWebProcessor EXIF orientation-aware --- .../ImageProcessors/CropWebProcessor.cs | 67 +++++++++++++------ 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs index 5d6d8062b594..519e524b196b 100644 --- a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs +++ b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Numerics; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Web; using SixLabors.ImageSharp.Web.Commands; @@ -20,29 +22,25 @@ public class CropWebProcessor : IImageWebProcessor /// public const string Coordinates = "cc"; - /// + /// + /// The command constant for the resize orientation handling mode. + /// + public const string Orient = "orient"; + + /// public IEnumerable Commands { get; } = new[] { - Coordinates + Coordinates, + Orient }; - /// + /// public FormattedImage Process(FormattedImage image, ILogger logger, CommandCollection commands, CommandParser parser, CultureInfo culture) { - RectangleF? coordinates = GetCoordinates(commands, parser, culture); - if (coordinates != null) + Rectangle? cropRectangle = GetCropRectangle(image, commands, parser, culture); + if (cropRectangle.HasValue) { - // Convert the coordinates to a pixel based rectangle - int sourceWidth = image.Image.Width; - int sourceHeight = image.Image.Height; - int x = (int)MathF.Round(coordinates.Value.X * sourceWidth); - int y = (int)MathF.Round(coordinates.Value.Y * sourceHeight); - int width = (int)MathF.Round(coordinates.Value.Width * sourceWidth); - int height = (int)MathF.Round(coordinates.Value.Height * sourceHeight); - - var cropRectangle = new Rectangle(x, y, width, height); - - image.Image.Mutate(x => x.Crop(cropRectangle)); + image.Image.Mutate(x => x.Crop(cropRectangle.Value)); } return image; @@ -51,17 +49,44 @@ public FormattedImage Process(FormattedImage image, ILogger logger, CommandColle /// public bool RequiresTrueColorPixelFormat(CommandCollection commands, CommandParser parser, CultureInfo culture) => false; - private static RectangleF? GetCoordinates(CommandCollection commands, CommandParser parser, CultureInfo culture) + private static Rectangle? GetCropRectangle(FormattedImage image, CommandCollection commands, CommandParser parser, CultureInfo culture) { float[] coordinates = parser.ParseValue(commands.GetValueOrDefault(Coordinates), culture); - - if (coordinates.Length != 4) + if (coordinates.Length != 4 || + (coordinates[0] == 0 && coordinates[1] == 0 && coordinates[2] == 0 && coordinates[3] == 0)) { return null; } - // The right and bottom values are actually the distance from those sides, so convert them into real coordinates - return RectangleF.FromLTRB(coordinates[0], coordinates[1], 1 - coordinates[2], 1 - coordinates[3]); + // The right and bottom values are actually the distance from those sides, so convert them into real coordinates and transform to correct orientation + float left = Math.Clamp(coordinates[0], 0, 1); + float top = Math.Clamp(coordinates[1], 0, 1); + float right = Math.Clamp(1 - coordinates[2], 0, 1); + float bottom = Math.Clamp(1 - coordinates[3], 0, 1); + ushort orientation = GetExifOrientation(image, commands, parser, culture); + Vector2 xy1 = ExifOrientationUtilities.Transform(new Vector2(left, top), Vector2.Zero, Vector2.One, orientation); + Vector2 xy2 = ExifOrientationUtilities.Transform(new Vector2(right, bottom), Vector2.Zero, Vector2.One, orientation); + + // Scale points to a pixel based rectangle + Size size = image.Image.Size(); + int x = (int)MathF.Round(MathF.Min(xy1.X, xy2.X) * size.Width); + int y = (int)MathF.Round(MathF.Min(xy1.Y, xy2.Y) * size.Height); + int width = (int)MathF.Round(MathF.Max(xy1.X, xy2.X) * size.Width) - x; + int height = (int)MathF.Round(MathF.Max(xy1.Y, xy2.Y) * size.Height) - y; + + return new Rectangle(x, y, width, height); + } + + private static ushort GetExifOrientation(FormattedImage image, CommandCollection commands, CommandParser parser, CultureInfo culture) + { + if (commands.Contains(Orient) && !parser.ParseValue(commands.GetValueOrDefault(Orient), culture)) + { + return ExifOrientationMode.Unknown; + } + + image.TryGetExifOrientation(out ushort orientation); + + return orientation; } } } From ba491ec33f92916c614d7ae120d4df393a124d74 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 4 Apr 2022 23:36:53 +0200 Subject: [PATCH 09/37] Improve width/height sanitization --- .../DependencyInjection/UmbracoBuilder.ImageSharp.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 25dfe7e5cd03..3d2852799126 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -43,11 +43,15 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build return Task.CompletedTask; } - uint width = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture); - uint height = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), context.Culture); - if (width > imagingSettings.Resize.MaxWidth || height > imagingSettings.Resize.MaxHeight) + int width = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture); + if (width <= 0 || width > imagingSettings.Resize.MaxWidth) { context.Commands.Remove(ResizeWebProcessor.Width); + } + + int height = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), context.Culture); + if (height <= 0 || height > imagingSettings.Resize.MaxHeight) + { context.Commands.Remove(ResizeWebProcessor.Height); } From 5b973a9c45de08a3339fd69bc68f2fde9bbcb893 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 4 Apr 2022 23:37:54 +0200 Subject: [PATCH 10/37] Also use 'v' as cache buster value --- .../DependencyInjection/UmbracoBuilder.ImageSharp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 3d2852799126..b0c9c48e7b21 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -62,7 +62,7 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build options.OnPrepareResponseAsync = context => { // Change Cache-Control header when cache buster value is present - if (context.Request.Query.ContainsKey("rnd")) + if (context.Request.Query.ContainsKey("rnd") || context.Request.Query.ContainsKey("v")) { var headers = context.Response.GetTypedHeaders(); From 2cbef09aaa77de39abdcc5d64251ed6988973a11 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 4 Apr 2022 23:38:23 +0200 Subject: [PATCH 11/37] Add WebP to supported image file types --- src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs index 990b3c61cbae..2e109fe31009 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentImagingSettings.cs @@ -23,7 +23,7 @@ public class ContentImagingSettings } }; - internal const string StaticImageFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif"; + internal const string StaticImageFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp"; /// /// Gets or sets a value for the collection of accepted image file extensions. From e7f74dcb21732aad7c97652125fc633815483c70 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 5 Apr 2022 16:18:59 +0200 Subject: [PATCH 12/37] Update to SixLabors.ImageSharp.Web 2.0.0-alpha.0.27 and fix test --- .../Umbraco.Web.Common.csproj | 2 +- .../ImageProcessors/CropWebProcessorTests.cs | 31 ++----------------- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 2ad624e9e3be..3ab3c56e32ce 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -34,7 +34,7 @@ - + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs index a5811c068165..3bad2051b82c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Reflection; using NUnit.Framework; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; @@ -24,8 +23,8 @@ public void CropWebProcessor_CropsImage() { var converters = new List { - CreateArrayConverterOfFloat(), - CreateSimpleCommandConverterOfFloat(), + new ArrayConverter(), + CreateSimpleCommandConverterOfFloat() }; var parser = new CommandParser(converters); @@ -37,22 +36,13 @@ public void CropWebProcessor_CropsImage() }; using var image = new Image(50, 80); - using FormattedImage formatted = CreateFormattedImage(image, PngFormat.Instance); + using FormattedImage formatted = new FormattedImage(image, PngFormat.Instance); new CropWebProcessor().Process(formatted, null, commands, parser, culture); Assert.AreEqual(40, image.Width); // Cropped 5 pixels from each side. Assert.AreEqual(32, image.Height); // Cropped 16 pixels from the top and 32 from the bottom. } - private static ICommandConverter CreateArrayConverterOfFloat() - { - // ImageSharp.Web's ArrayConverter is internal, so we need to use reflection to instantiate. - var type = Type.GetType("SixLabors.ImageSharp.Web.Commands.Converters.ArrayConverter`1, SixLabors.ImageSharp.Web"); - Type[] typeArgs = { typeof(float) }; - Type genericType = type.MakeGenericType(typeArgs); - return (ICommandConverter)Activator.CreateInstance(genericType); - } - private static ICommandConverter CreateSimpleCommandConverterOfFloat() { // ImageSharp.Web's SimpleCommandConverter is internal, so we need to use reflection to instantiate. @@ -61,20 +51,5 @@ private static ICommandConverter CreateSimpleCommandConverterOfFloat() Type genericType = type.MakeGenericType(typeArgs); return (ICommandConverter)Activator.CreateInstance(genericType); } - - private FormattedImage CreateFormattedImage(Image image, PngFormat format) - { - // Again, the constructor of FormattedImage useful for tests is internal, so we need to use reflection. - Type type = typeof(FormattedImage); - var instance = type.Assembly.CreateInstance( - type.FullName, - false, - BindingFlags.Instance | BindingFlags.NonPublic, - null, - new object[] { image, format }, - null, - null); - return (FormattedImage)instance; - } } } From 70e2437db0ff7490a2c348f7a4c0fb844775612e Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Sat, 9 Apr 2022 13:24:31 +0200 Subject: [PATCH 13/37] Fix rounding error and add test cases --- .../ImageProcessors/CropWebProcessor.cs | 10 ++-- .../ImageProcessors/CropWebProcessorTests.cs | 46 +++++++++++-------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs index 519e524b196b..85ecf2d844d2 100644 --- a/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs +++ b/src/Umbraco.Web.Common/ImageProcessors/CropWebProcessor.cs @@ -69,12 +69,12 @@ public FormattedImage Process(FormattedImage image, ILogger logger, CommandColle // Scale points to a pixel based rectangle Size size = image.Image.Size(); - int x = (int)MathF.Round(MathF.Min(xy1.X, xy2.X) * size.Width); - int y = (int)MathF.Round(MathF.Min(xy1.Y, xy2.Y) * size.Height); - int width = (int)MathF.Round(MathF.Max(xy1.X, xy2.X) * size.Width) - x; - int height = (int)MathF.Round(MathF.Max(xy1.Y, xy2.Y) * size.Height) - y; - return new Rectangle(x, y, width, height); + return Rectangle.Round(RectangleF.FromLTRB( + MathF.Min(xy1.X, xy2.X) * size.Width, + MathF.Min(xy1.Y, xy2.Y) * size.Height, + MathF.Max(xy1.X, xy2.X) * size.Width, + MathF.Max(xy1.Y, xy2.Y) * size.Height)); } private static ushort GetExifOrientation(FormattedImage image, CommandCollection commands, CommandParser parser, CultureInfo culture) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs index 3bad2051b82c..d9892177d02c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs @@ -2,8 +2,8 @@ // See LICENSE for more details. using System; -using System.Collections.Generic; using System.Globalization; +using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; @@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Web; using SixLabors.ImageSharp.Web.Commands; using SixLabors.ImageSharp.Web.Commands.Converters; +using SixLabors.ImageSharp.Web.Middleware; using Umbraco.Cms.Web.Common.ImageProcessors; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.ImageProcessors @@ -19,36 +20,45 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.ImageProcessors public class CropWebProcessorTests { [Test] - public void CropWebProcessor_CropsImage() + // Coordinates are percentages to crop from the left, top, right and bottom sides + [TestCase("0,0,0,0", 50, 90)] + [TestCase("0.1,0.0,0.0,0.0", 45, 90)] + [TestCase("0.0,0.1,0.0,0.0", 50, 81)] + [TestCase("0.0,0.0,0.1,0.0", 45, 90)] + [TestCase("0.0,0.0,0.0,0.1", 50, 81)] + [TestCase("0.1,0.0,0.1,0.0", 40, 90)] + [TestCase("0.0,0.1,0.0,0.1", 50, 72)] + [TestCase("0.1,0.1,0.1,0.1", 40, 72)] + [TestCase("0.25,0.25,0.25,0.25", 25, 45)] + public void CropWebProcessor_CropsImage(string coordinates, int width, int height) { - var converters = new List - { - new ArrayConverter(), - CreateSimpleCommandConverterOfFloat() - }; - - var parser = new CommandParser(converters); - CultureInfo culture = CultureInfo.InvariantCulture; + using var image = new Image(50, 90); + using var formattedImage = new FormattedImage(image, PngFormat.Instance); + var logger = new NullLogger(); var commands = new CommandCollection { - { CropWebProcessor.Coordinates, "0.1,0.2,0.1,0.4" }, // left, top, right, bottom + { CropWebProcessor.Coordinates, coordinates }, }; + var parser = new CommandParser(new[] + { + new ArrayConverter(), + CreateSimpleCommandConverterOfFloat() + }); + var culture = CultureInfo.InvariantCulture; - using var image = new Image(50, 80); - using FormattedImage formatted = new FormattedImage(image, PngFormat.Instance); - new CropWebProcessor().Process(formatted, null, commands, parser, culture); + new CropWebProcessor().Process(formattedImage, logger, commands, parser, culture); - Assert.AreEqual(40, image.Width); // Cropped 5 pixels from each side. - Assert.AreEqual(32, image.Height); // Cropped 16 pixels from the top and 32 from the bottom. + Assert.AreEqual(width, image.Width); + Assert.AreEqual(height, image.Height); } private static ICommandConverter CreateSimpleCommandConverterOfFloat() { // ImageSharp.Web's SimpleCommandConverter is internal, so we need to use reflection to instantiate. var type = Type.GetType("SixLabors.ImageSharp.Web.Commands.Converters.SimpleCommandConverter`1, SixLabors.ImageSharp.Web"); - Type[] typeArgs = { typeof(float) }; - Type genericType = type.MakeGenericType(typeArgs); + var genericType = type.MakeGenericType(typeof(float)); + return (ICommandConverter)Activator.CreateInstance(genericType); } } From 67b8dbc003f5ecea3f0129bb82dbcf2b4cb3ffc6 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 25 Apr 2022 10:44:15 +0200 Subject: [PATCH 14/37] Update to newest and stable releases --- NuGet.config | 6 ------ src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj | 2 +- src/Umbraco.Web.Common/Umbraco.Web.Common.csproj | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 NuGet.config diff --git a/NuGet.config b/NuGet.config deleted file mode 100644 index 6aa8697b8fe9..000000000000 --- a/NuGet.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 40cda9903844..73f748827692 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -48,7 +48,7 @@ - + diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 3ab3c56e32ce..64d0c9aa8e5b 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -34,7 +34,7 @@ - + From cac54022d68512ace0891f85c553d8b4bb58fb15 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 10:56:53 +0200 Subject: [PATCH 15/37] Move ImageSharpImageUrlGenerator to Umbraco.Web.Common --- .../UmbracoBuilder.CoreServices.cs | 1 - .../Media/ImageSharpImageUrlGenerator.cs | 107 ------------------ .../UmbracoBuilder.ImageSharp.cs | 6 +- .../Media/ImageSharpImageUrlGenerator.cs | 100 ++++++++++++++++ .../Media/ImageSharpImageUrlGeneratorTests.cs | 4 +- 5 files changed, 106 insertions(+), 112 deletions(-) delete mode 100644 src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs create mode 100644 src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs rename tests/Umbraco.Tests.UnitTests/{Umbraco.Infrastructure => Umbraco.Web.Common}/Media/ImageSharpImageUrlGeneratorTests.cs (98%) diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 2a429fc6c560..44e898fcc823 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -200,7 +200,6 @@ public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builde // Add default ImageSharp configuration and service implementations builder.Services.AddSingleton(SixLabors.ImageSharp.Configuration.Default); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs deleted file mode 100644 index cfca16601ed4..000000000000 --- a/src/Umbraco.Infrastructure/Media/ImageSharpImageUrlGenerator.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using SixLabors.ImageSharp; -using Umbraco.Cms.Core.Media; -using Umbraco.Cms.Core.Models; - -namespace Umbraco.Cms.Infrastructure.Media -{ - /// - /// Exposes a method that generates an image URL based on the specified options that can be processed by ImageSharp. - /// - /// - public class ImageSharpImageUrlGenerator : IImageUrlGenerator - { - /// - public IEnumerable SupportedImageFileTypes { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The ImageSharp configuration. - public ImageSharpImageUrlGenerator(Configuration configuration) - : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray()) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The supported image file types/extensions. - /// - /// This constructor is only used for testing. - /// - internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes) => SupportedImageFileTypes = supportedImageFileTypes; - - /// - public string? GetImageUrl(ImageUrlGenerationOptions options) - { - if (options == null) - { - return null; - } - - var imageUrl = new StringBuilder(options.ImageUrl); - - bool queryStringHasStarted = false; - void AppendQueryString(string value) - { - imageUrl.Append(queryStringHasStarted ? '&' : '?'); - queryStringHasStarted = true; - - imageUrl.Append(value); - } - void AddQueryString(string key, params IConvertible[] values) - => AppendQueryString(key + '=' + string.Join(",", values.Select(x => x.ToString(CultureInfo.InvariantCulture)))); - - if (options.Crop != null) - { - AddQueryString("cc", options.Crop.Left, options.Crop.Top, options.Crop.Right, options.Crop.Bottom); - } - - if (options.FocalPoint != null) - { - AddQueryString("rxy", options.FocalPoint.Left, options.FocalPoint.Top); - } - - if (options.ImageCropMode.HasValue) - { - AddQueryString("rmode", options.ImageCropMode.Value.ToString().ToLowerInvariant()); - } - - if (options.ImageCropAnchor.HasValue) - { - AddQueryString("ranchor", options.ImageCropAnchor.Value.ToString().ToLowerInvariant()); - } - - if (options.Width.HasValue) - { - AddQueryString("width", options.Width.Value); - } - - if (options.Height.HasValue) - { - AddQueryString("height", options.Height.Value); - } - - if (options.Quality.HasValue) - { - AddQueryString("quality", options.Quality.Value); - } - - if (string.IsNullOrWhiteSpace(options.FurtherOptions) == false) - { - AppendQueryString(options.FurtherOptions.TrimStart('?', '&')); - } - - if (string.IsNullOrWhiteSpace(options.CacheBusterValue) == false) - { - AddQueryString("rnd", options.CacheBusterValue); - } - - return imageUrl.ToString(); - } - } -} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index dbb06b1cb932..a1a8de5bb048 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using SixLabors.ImageSharp.Web.Caching; @@ -13,9 +12,10 @@ using SixLabors.ImageSharp.Web.Providers; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Extensions; +using Umbraco.Cms.Core.Media; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.ImageProcessors; +using Umbraco.Cms.Web.Common.Media; namespace Umbraco.Extensions { @@ -26,6 +26,8 @@ public static partial class UmbracoBuilderExtensions /// public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder) { + builder.Services.AddSingleton(); + ImagingSettings imagingSettings = builder.Config.GetSection(Cms.Core.Constants.Configuration.ConfigImaging) .Get() ?? new ImagingSettings(); diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs new file mode 100644 index 000000000000..02aed2ee9636 --- /dev/null +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Web.Processors; +using Umbraco.Cms.Core.Media; +using Umbraco.Cms.Core.Models; +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. + /// + /// + public class ImageSharpImageUrlGenerator : IImageUrlGenerator + { + /// + public IEnumerable SupportedImageFileTypes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The ImageSharp configuration. + public ImageSharpImageUrlGenerator(Configuration configuration) + : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray()) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The supported image file types/extensions. + /// + /// This constructor is only used for testing. + /// + internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes) => SupportedImageFileTypes = supportedImageFileTypes; + + /// + public string? GetImageUrl(ImageUrlGenerationOptions options) + { + if (options == null || string.IsNullOrEmpty(options.ImageUrl)) + { + return null; + } + + var queryString = new Dictionary(); + + if (options.Crop is CropCoordinates crop) + { + queryString.Add(CropWebProcessor.Coordinates, FormattableString.Invariant($"{crop.Left},{crop.Top},{crop.Right},{crop.Bottom}")); + } + + if (options.FocalPoint is FocalPointPosition focalPoint) + { + queryString.Add(ResizeWebProcessor.Xy, FormattableString.Invariant($"{focalPoint.Left},{focalPoint.Top}")); + } + + if (options.ImageCropMode is ImageCropMode imageCropMode) + { + queryString.Add(ResizeWebProcessor.Mode, imageCropMode.ToString().ToLowerInvariant()); + } + + if (options.ImageCropAnchor is ImageCropAnchor imageCropAnchor) + { + queryString.Add(ResizeWebProcessor.Anchor, imageCropAnchor.ToString().ToLowerInvariant()); + } + + if (options.Width is int width) + { + queryString.Add(ResizeWebProcessor.Width, width.ToString(CultureInfo.InvariantCulture)); + } + + if (options.Height is int height) + { + queryString.Add(ResizeWebProcessor.Height, height.ToString(CultureInfo.InvariantCulture)); + } + + if (options.Quality is int quality) + { + queryString.Add(QualityWebProcessor.Quality, quality.ToString(CultureInfo.InvariantCulture)); + } + + foreach (KeyValuePair kvp in QueryHelpers.ParseQuery(options.FurtherOptions)) + { + queryString.Add(kvp.Key, kvp.Value); + } + + if (options.CacheBusterValue is string cacheBusterValue && !string.IsNullOrWhiteSpace(cacheBusterValue)) + { + queryString.Add("rnd", cacheBusterValue); + } + + return QueryHelpers.AddQueryString(options.ImageUrl, queryString); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Media/ImageSharpImageUrlGeneratorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs similarity index 98% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Media/ImageSharpImageUrlGeneratorTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs index a531aa6bbd01..3098a523508f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Media/ImageSharpImageUrlGeneratorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs @@ -3,9 +3,9 @@ using NUnit.Framework; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Infrastructure.Media; +using Umbraco.Cms.Web.Common.Media; -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Media +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Media { [TestFixture] public class ImageSharpImageUrlGeneratorTests From 3cae534f264a301ded64982aa3e3544829a92c09 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 11:11:29 +0200 Subject: [PATCH 16/37] Use IConfigureOptions to configure ImageSharp options --- .../ConfigureImageSharpMiddlewareOptions.cs | 88 +++++++++++++++++++ ...ConfigurePhysicalFileSystemCacheOptions.cs | 36 ++++++++ .../ImageSharpConfigurationOptions.cs | 30 ------- .../UmbracoBuilder.ImageSharp.cs | 78 ++-------------- 4 files changed, 132 insertions(+), 100 deletions(-) create mode 100644 src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs create mode 100644 src/Umbraco.Web.Common/DependencyInjection/ConfigurePhysicalFileSystemCacheOptions.cs delete mode 100644 src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs diff --git a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs new file mode 100644 index 000000000000..69b37cd7da2d --- /dev/null +++ b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Headers; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Web.Commands; +using SixLabors.ImageSharp.Web.Middleware; +using SixLabors.ImageSharp.Web.Processors; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Web.Common.DependencyInjection +{ + /// + /// Configures the ImageSharp middleware options. + /// + /// + public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions + { + private readonly Configuration _configuration; + private readonly ImagingSettings _imagingSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The ImageSharp configuration. + /// The Umbraco imaging settings. + public ConfigureImageSharpMiddlewareOptions(Configuration configuration, IOptions imagingSettings) + { + _configuration = configuration; + _imagingSettings = imagingSettings.Value; + } + + /// + public void Configure(ImageSharpMiddlewareOptions options) + { + options.Configuration = _configuration; + + options.BrowserMaxAge = _imagingSettings.Cache.BrowserMaxAge; + options.CacheMaxAge = _imagingSettings.Cache.CacheMaxAge; + options.CacheHashLength = _imagingSettings.Cache.CacheHashLength; + + // Use configurable maximum width and height + options.OnParseCommandsAsync = context => + { + if (context.Commands.Count == 0) + { + return Task.CompletedTask; + } + + int width = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture); + if (width <= 0 || width > _imagingSettings.Resize.MaxWidth) + { + context.Commands.Remove(ResizeWebProcessor.Width); + } + + int height = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), context.Culture); + if (height <= 0 || height > _imagingSettings.Resize.MaxHeight) + { + context.Commands.Remove(ResizeWebProcessor.Height); + } + + return Task.CompletedTask; + }; + + // Change Cache-Control header when cache buster value is present + options.OnPrepareResponseAsync = context => + { + if (context.Request.Query.ContainsKey("rnd") || context.Request.Query.ContainsKey("v")) + { + ResponseHeaders headers = context.Response.GetTypedHeaders(); + + CacheControlHeaderValue cacheControl = headers.CacheControl ?? new CacheControlHeaderValue() + { + Public = true + }; + cacheControl.MustRevalidate = false; // ImageSharp enables this by default + cacheControl.Extensions.Add(new NameValueHeaderValue("immutable")); + + headers.CacheControl = cacheControl; + } + + return Task.CompletedTask; + }; + } + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/ConfigurePhysicalFileSystemCacheOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ConfigurePhysicalFileSystemCacheOptions.cs new file mode 100644 index 000000000000..16f247618957 --- /dev/null +++ b/src/Umbraco.Web.Common/DependencyInjection/ConfigurePhysicalFileSystemCacheOptions.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using SixLabors.ImageSharp.Web.Caching; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Extensions; + +namespace Umbraco.Cms.Web.Common.DependencyInjection +{ + /// + /// Configures the ImageSharp physical file system cache options. + /// + /// + public sealed class ConfigurePhysicalFileSystemCacheOptions : IConfigureOptions + { + private readonly ImagingSettings _imagingSettings; + private readonly IHostEnvironment _hostEnvironment; + + /// + /// Initializes a new instance of the class. + /// + /// The Umbraco imaging settings. + /// The host environment. + public ConfigurePhysicalFileSystemCacheOptions(IOptions imagingSettings, IHostEnvironment hostEnvironment) + { + _imagingSettings = imagingSettings.Value; + _hostEnvironment = hostEnvironment; + } + + /// + public void Configure(PhysicalFileSystemCacheOptions options) + { + options.CacheFolder = _hostEnvironment.MapPathContentRoot(_imagingSettings.Cache.CacheFolder); + options.CacheFolderDepth = _imagingSettings.Cache.CacheFolderDepth; + } + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs deleted file mode 100644 index f8897e522cd6..000000000000 --- a/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.Extensions.Options; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Web.Middleware; - -namespace Umbraco.Cms.Web.Common.DependencyInjection -{ - /// - /// Configures the ImageSharp middleware options to use the registered configuration. - /// - /// - public sealed class ImageSharpConfigurationOptions : IConfigureOptions - { - /// - /// The ImageSharp configuration. - /// - private readonly Configuration _configuration; - - /// - /// Initializes a new instance of the class. - /// - /// The ImageSharp configuration. - public ImageSharpConfigurationOptions(Configuration configuration) => _configuration = configuration; - - /// - /// Invoked to configure an instance. - /// - /// The options instance to configure. - public void Configure(ImageSharpMiddlewareOptions options) => options.Configuration = _configuration; - } -} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index a1a8de5bb048..cfba33d0ae7b 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -1,16 +1,9 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; using SixLabors.ImageSharp.Web.Caching; -using SixLabors.ImageSharp.Web.Commands; using SixLabors.ImageSharp.Web.DependencyInjection; using SixLabors.ImageSharp.Web.Middleware; -using SixLabors.ImageSharp.Web.Processors; using SixLabors.ImageSharp.Web.Providers; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Web.Common.DependencyInjection; @@ -28,73 +21,18 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build { builder.Services.AddSingleton(); - ImagingSettings imagingSettings = builder.Config.GetSection(Cms.Core.Constants.Configuration.ConfigImaging) - .Get() ?? new ImagingSettings(); - - builder.Services.AddImageSharp(options => - { - // options.Configuration is set using ImageSharpConfigurationOptions below - options.BrowserMaxAge = imagingSettings.Cache.BrowserMaxAge; - options.CacheMaxAge = imagingSettings.Cache.CacheMaxAge; - options.CacheHashLength = imagingSettings.Cache.CacheHashLength; - - // Use configurable maximum width and height (overwrite ImageSharps default) - options.OnParseCommandsAsync = context => - { - if (context.Commands.Count == 0) - { - return Task.CompletedTask; - } - - int width = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Width), context.Culture); - if (width <= 0 || width > imagingSettings.Resize.MaxWidth) - { - context.Commands.Remove(ResizeWebProcessor.Width); - } - - int height = context.Parser.ParseValue(context.Commands.GetValueOrDefault(ResizeWebProcessor.Height), context.Culture); - if (height <= 0 || height > imagingSettings.Resize.MaxHeight) - { - context.Commands.Remove(ResizeWebProcessor.Height); - } - - return Task.CompletedTask; - }; - options.OnBeforeSaveAsync = _ => Task.CompletedTask; - options.OnProcessedAsync = _ => Task.CompletedTask; - options.OnPrepareResponseAsync = context => - { - // Change Cache-Control header when cache buster value is present - if (context.Request.Query.ContainsKey("rnd") || context.Request.Query.ContainsKey("v")) - { - var headers = context.Response.GetTypedHeaders(); - - var cacheControl = headers.CacheControl; - if (cacheControl is not null) - { - cacheControl.MustRevalidate = false; - cacheControl.Extensions.Add(new NameValueHeaderValue("immutable")); - } - - headers.CacheControl = cacheControl; - } - - return Task.CompletedTask; - }; - }) + builder.Services.AddImageSharp() // Replace default image provider - .ClearProviders().AddProvider() - // Configure cache options - .Configure(options => - { - options.CacheFolder = builder.BuilderHostingEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder); - options.CacheFolderDepth = imagingSettings.Cache.CacheFolderDepth; - }) + .ClearProviders() + .AddProvider() // Add custom processors .AddProcessor(); - // Configure middleware to use the registered/shared ImageSharp configuration - builder.Services.AddTransient, ImageSharpConfigurationOptions>(); + // Configure middleware + builder.Services.AddTransient, ConfigureImageSharpMiddlewareOptions>(); + + // Configure cache options + builder.Services.AddTransient, ConfigurePhysicalFileSystemCacheOptions>(); return builder.Services; } From 10eb181c4fa5e68f525b04c48230d809d20d5f6b Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 11:53:27 +0200 Subject: [PATCH 17/37] Implement IEquatable on ImageUrlGenerationOptions classes --- .../Models/ImageUrlGenerationOptions.cs | 62 ++++++++++++++++++- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs index 855c7c00bcd9..876b2bfddb7e 100644 --- a/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs +++ b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs @@ -1,9 +1,12 @@ +using System; +using System.Collections.Generic; + namespace Umbraco.Cms.Core.Models { /// /// These are options that are passed to the IImageUrlGenerator implementation to determine the URL that is generated. /// - public class ImageUrlGenerationOptions + public class ImageUrlGenerationOptions : IEquatable { public ImageUrlGenerationOptions(string? imageUrl) => ImageUrl = imageUrl; @@ -27,10 +30,43 @@ public class ImageUrlGenerationOptions public string? FurtherOptions { get; set; } + public override bool Equals(object? obj) => Equals(obj as ImageUrlGenerationOptions); + + public bool Equals(ImageUrlGenerationOptions? other) + => other != null && + ImageUrl == other.ImageUrl && + Width == other.Width && + Height == other.Height && + Quality == other.Quality && + ImageCropMode == other.ImageCropMode && + ImageCropAnchor == other.ImageCropAnchor && + EqualityComparer.Default.Equals(FocalPoint, other.FocalPoint) && + EqualityComparer.Default.Equals(Crop, other.Crop) && + CacheBusterValue == other.CacheBusterValue && + FurtherOptions == other.FurtherOptions; + + public override int GetHashCode() + { + var hash = new HashCode(); + + hash.Add(ImageUrl); + hash.Add(Width); + hash.Add(Height); + hash.Add(Quality); + hash.Add(ImageCropMode); + hash.Add(ImageCropAnchor); + hash.Add(FocalPoint); + hash.Add(Crop); + hash.Add(CacheBusterValue); + hash.Add(FurtherOptions); + + return hash.ToHashCode(); + } + /// /// The focal point position, in whatever units the registered IImageUrlGenerator uses, typically a percentage of the total image from 0.0 to 1.0. /// - public class FocalPointPosition + public class FocalPointPosition : IEquatable { public FocalPointPosition(decimal left, decimal top) { @@ -41,12 +77,21 @@ public FocalPointPosition(decimal left, decimal top) public decimal Left { get; } public decimal Top { get; } + + public override bool Equals(object? obj) => Equals(obj as FocalPointPosition); + + public bool Equals(FocalPointPosition? other) + => other != null && + Left == other.Left && + Top == other.Top; + + public override int GetHashCode() => HashCode.Combine(Left, Top); } /// /// The bounds of the crop within the original image, in whatever units the registered IImageUrlGenerator uses, typically a percentage between 0.0 and 1.0. /// - public class CropCoordinates + public class CropCoordinates : IEquatable { public CropCoordinates(decimal left, decimal top, decimal right, decimal bottom) { @@ -63,6 +108,17 @@ public CropCoordinates(decimal left, decimal top, decimal right, decimal bottom) public decimal Right { get; } public decimal Bottom { get; } + + public override bool Equals(object? obj) => Equals(obj as CropCoordinates); + + public bool Equals(CropCoordinates? other) + => other != null && + Left == other.Left && + Top == other.Top && + Right == other.Right && + Bottom == other.Bottom; + + public override int GetHashCode() => HashCode.Combine(Left, Top, Right, Bottom); } } } From 2de55205bef8935e457146849453465aacb379df Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 12:10:51 +0200 Subject: [PATCH 18/37] Fix empty/null values in image URL generation and corresponding tests --- .../Media/ImageSharpImageUrlGenerator.cs | 2 +- .../Media/ImageSharpImageUrlGeneratorTests.cs | 44 ++++++++++++------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs index 02aed2ee9636..1addc76abb93 100644 --- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs @@ -42,7 +42,7 @@ public ImageSharpImageUrlGenerator(Configuration configuration) /// public string? GetImageUrl(ImageUrlGenerationOptions options) { - if (options == null || string.IsNullOrEmpty(options.ImageUrl)) + if (options?.ImageUrl == null) { return null; } 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 3098a523508f..f529d17bd0ad 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs @@ -17,60 +17,70 @@ public class ImageSharpImageUrlGeneratorTests private static readonly ImageSharpImageUrlGenerator s_generator = new ImageSharpImageUrlGenerator(new string[0]); [Test] - public void GetCropUrl_CropAliasTest() + 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); } [Test] - public void GetCropUrl_WidthHeightTest() + public void GetImageUrl_WidthHeightTest() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_focus1, Width = 200, Height = 300 }); Assert.AreEqual(MediaPath + "?rxy=0.96,0.80827067669172936&width=200&height=300", urlString); } [Test] - public void GetCropUrl_FocalPointTest() + public void GetImageUrl_FocalPointTest() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_focus1, Width = 100, Height = 100 }); Assert.AreEqual(MediaPath + "?rxy=0.96,0.80827067669172936&width=100&height=100", urlString); } [Test] - public void GetCropUrlFurtherOptionsTest() + public void GetImageUrlFurtherOptionsTest() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_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|bgcolor-fff", urlString); + Assert.AreEqual(MediaPath + "?rxy=0.96,0.80827067669172936&width=200&height=300&filter=comic&roundedcorners=radius-26%7Cbgcolor-fff", urlString); } /// /// Test that if options is null, the generated image URL is also null. /// [Test] - public void GetCropUrlNullTest() + public void GetImageUrlNullOptionsTest() { var urlString = s_generator.GetImageUrl(null); Assert.AreEqual(null, urlString); } /// - /// Test that if the image URL is null, the generated image URL is empty. + /// Test that if the image URL is null, the generated image URL is also null. /// [Test] - public void GetCropUrlEmptyTest() + public void GetImageUrlNullTest() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(null)); + Assert.AreEqual(null, urlString); + } + + /// + /// 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)); Assert.AreEqual(string.Empty, urlString); } /// - /// Test the GetCropUrl method on the ImageCropDataSet Model + /// Test the GetImageUrl method on the ImageCropDataSet Model /// [Test] public void GetBaseCropUrlFromModelTest() { - var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(null) { Crop = s_crop, Width = 100, Height = 100 }); + 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); } @@ -78,7 +88,7 @@ public void GetBaseCropUrlFromModelTest() /// Test that if Crop mode is specified as anything other than Crop the image doesn't use the crop /// [Test] - public void GetCropUrl_SpecifiedCropModeTest() + 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 }); @@ -97,7 +107,7 @@ public void GetCropUrl_SpecifiedCropModeTest() /// Test for upload property type /// [Test] - public void GetCropUrl_UploadTypeTest() + public void GetImageUrl_UploadTypeTest() { var urlString = s_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); @@ -107,7 +117,7 @@ public void GetCropUrl_UploadTypeTest() /// Test for preferFocalPoint when focal point is centered /// [Test] - public void GetCropUrl_PreferFocalPointCenter() + public void GetImageUrl_PreferFocalPointCenter() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Width = 300, Height = 150 }); Assert.AreEqual(MediaPath + "?width=300&height=150", urlString); @@ -117,7 +127,7 @@ public void GetCropUrl_PreferFocalPointCenter() /// Test to check if crop ratio is ignored if useCropDimensions is true /// [Test] - public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore() + public void GetImageUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = s_focus2, Width = 270, Height = 161 }); Assert.AreEqual(MediaPath + "?rxy=0.4275,0.41&width=270&height=161", urlString); @@ -127,7 +137,7 @@ public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore() /// Test to check result when only a width parameter is passed, effectivly a resize only /// [Test] - public void GetCropUrl_WidthOnlyParameter() + public void GetImageUrl_WidthOnlyParameter() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Width = 200 }); Assert.AreEqual(MediaPath + "?width=200", urlString); @@ -137,7 +147,7 @@ public void GetCropUrl_WidthOnlyParameter() /// Test to check result when only a height parameter is passed, effectivly a resize only /// [Test] - public void GetCropUrl_HeightOnlyParameter() + public void GetImageUrl_HeightOnlyParameter() { var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Height = 200 }); Assert.AreEqual(MediaPath + "?height=200", urlString); @@ -147,7 +157,7 @@ public void GetCropUrl_HeightOnlyParameter() /// Test to check result when using a background color with padding /// [Test] - public void GetCropUrl_BackgroundColorParameter() + public void GetImageUrl_BackgroundColorParameter() { var urlString = s_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); From 5898b0c87ae44747895fc72c7b64e0d582a04b56 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 13:18:05 +0200 Subject: [PATCH 19/37] Use IsSupportedImageFormat extension method --- src/Umbraco.Web.BackOffice/Controllers/MediaController.cs | 4 ++-- src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index 6f43e90acf62..ce0bb9846bca 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -840,7 +840,7 @@ public async Task PostAddFile([FromForm] string path, [FromForm] { var fileName = formFile.FileName.Trim(Constants.CharArrays.DoubleQuote).TrimEnd(); var safeFileName = fileName.ToSafeFileName(ShortStringHelper); - var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower(); + var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLowerInvariant(); if (!_contentSettings.IsFileAllowedForUpload(ext)) { @@ -885,7 +885,7 @@ public async Task PostAddFile([FromForm] string path, [FromForm] } // If media type is still File then let's check if it's an image. - if (mediaTypeAlias == Constants.Conventions.MediaTypes.File && _imageUrlGenerator.SupportedImageFileTypes.Contains(ext)) + if (mediaTypeAlias == Constants.Conventions.MediaTypes.File && _imageUrlGenerator.IsSupportedImageFormat(ext)) { mediaTypeAlias = Constants.Conventions.MediaTypes.Image; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs index cc4bc82ad628..1b067e71c207 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -75,9 +75,9 @@ public async Task UploadImage(List file) // var file = result.FileData[0]; var fileName = formFile.FileName.Trim(new[] { '\"' }).TrimEnd(); var safeFileName = fileName.ToSafeFileName(_shortStringHelper); - var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower(); + var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLowerInvariant(); - if (_contentSettings.IsFileAllowedForUpload(ext) == false || _imageUrlGenerator.SupportedImageFileTypes.Contains(ext) == false) + if (_contentSettings.IsFileAllowedForUpload(ext) == false || _imageUrlGenerator.IsSupportedImageFormat(ext) == false) { // Throw some error - to say can't upload this IMG type return new UmbracoProblemResult("This is not an image filetype extension that is approved", HttpStatusCode.BadRequest); From 5cb9a9b1341a713f730e33aac3fd067de4a840af Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 13:41:08 +0200 Subject: [PATCH 20/37] Remove unneeded reflection --- .../ImageProcessors/CropWebProcessorTests.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs index d9892177d02c..7a16ff9abfca 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageProcessors/CropWebProcessorTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using System.Globalization; using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; @@ -40,10 +39,10 @@ public void CropWebProcessor_CropsImage(string coordinates, int width, int heigh { { CropWebProcessor.Coordinates, coordinates }, }; - var parser = new CommandParser(new[] + var parser = new CommandParser(new ICommandConverter[] { new ArrayConverter(), - CreateSimpleCommandConverterOfFloat() + new SimpleCommandConverter() }); var culture = CultureInfo.InvariantCulture; @@ -52,14 +51,5 @@ public void CropWebProcessor_CropsImage(string coordinates, int width, int heigh Assert.AreEqual(width, image.Width); Assert.AreEqual(height, image.Height); } - - private static ICommandConverter CreateSimpleCommandConverterOfFloat() - { - // ImageSharp.Web's SimpleCommandConverter is internal, so we need to use reflection to instantiate. - var type = Type.GetType("SixLabors.ImageSharp.Web.Commands.Converters.SimpleCommandConverter`1, SixLabors.ImageSharp.Web"); - var genericType = type.MakeGenericType(typeof(float)); - - return (ICommandConverter)Activator.CreateInstance(genericType); - } } } From bd4bb547416edf9d5cdd018afab5df51a833f1cd Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 20:16:50 +0200 Subject: [PATCH 21/37] Add HMACSecretKey setting and add token when generating image URLs --- .../Configuration/Models/ImagingSettings.cs | 5 ++++ .../ConfigureImageSharpMiddlewareOptions.cs | 3 ++- .../Media/ImageSharpImageUrlGenerator.cs | 25 +++++++++++++++--- .../Media/ImageSharpImageUrlGeneratorTests.cs | 26 ++++++++++++++++++- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs index fde303343c9c..60658ae916c8 100644 --- a/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs @@ -9,6 +9,11 @@ namespace Umbraco.Cms.Core.Configuration.Models [UmbracoOptions(Constants.Configuration.ConfigImaging)] public class ImagingSettings { + /// + /// Gets or sets a value for the HMAC security key. + /// + public byte[]? HMACSecretKey { get; set; } + /// /// Gets or sets a value for imaging cache settings. /// diff --git a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs index 69b37cd7da2d..e64c19ea685e 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs @@ -37,6 +37,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; @@ -44,7 +45,7 @@ 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) { return Task.CompletedTask; } diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs index 1addc76abb93..d310a419c89a 100644 --- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs @@ -2,10 +2,14 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Web; using SixLabors.ImageSharp.Web.Processors; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.ImageProcessors; @@ -19,6 +23,8 @@ namespace Umbraco.Cms.Web.Common.Media /// public class ImageSharpImageUrlGenerator : IImageUrlGenerator { + private readonly byte[]? _hmacSecretKey; + /// public IEnumerable SupportedImageFileTypes { get; } @@ -26,18 +32,24 @@ public class ImageSharpImageUrlGenerator : IImageUrlGenerator /// Initializes a new instance of the class. /// /// The ImageSharp configuration. - public ImageSharpImageUrlGenerator(Configuration configuration) - : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray()) + /// The Umbraco imaging settings. + public ImageSharpImageUrlGenerator(Configuration configuration, IOptions imagingSettings) + : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray(), imagingSettings.Value.HMACSecretKey) { } /// /// Initializes a new instance of the class. /// /// The supported image file types/extensions. + /// The HMAC security key. /// /// This constructor is only used for testing. /// - internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes) => SupportedImageFileTypes = supportedImageFileTypes; + internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes, byte[]? hmacSecretKey) + { + SupportedImageFileTypes = supportedImageFileTypes; + _hmacSecretKey = hmacSecretKey; + } /// public string? GetImageUrl(ImageUrlGenerationOptions options) @@ -89,6 +101,13 @@ public ImageSharpImageUrlGenerator(Configuration configuration) queryString.Add(kvp.Key, kvp.Value); } + if (_hmacSecretKey is byte[] secret && secret.Length > 0) + { + string value = CaseHandlingUriBuilder.BuildRelative(CaseHandlingUriBuilder.CaseHandling.LowerInvariant, null, options.ImageUrl, QueryString.Create(queryString)); + + queryString.Add(HMACUtilities.TokenCommand, HMACUtilities.ComputeHMACSHA256(value, secret)); + } + if (options.CacheBusterValue is string cacheBusterValue && !string.IsNullOrWhiteSpace(cacheBusterValue)) { queryString.Add("rnd", cacheBusterValue); 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 f529d17bd0ad..3d0376157136 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs @@ -14,7 +14,7 @@ public class ImageSharpImageUrlGeneratorTests private static readonly ImageUrlGenerationOptions.CropCoordinates s_crop = new ImageUrlGenerationOptions.CropCoordinates(0.58729977382575338m, 0.055768992440203169m, 0m, 0.32457553600198386m); private static readonly ImageUrlGenerationOptions.FocalPointPosition s_focus1 = new ImageUrlGenerationOptions.FocalPointPosition(0.96m, 0.80827067669172936m); private static readonly ImageUrlGenerationOptions.FocalPointPosition s_focus2 = new ImageUrlGenerationOptions.FocalPointPosition(0.4275m, 0.41m); - private static readonly ImageSharpImageUrlGenerator s_generator = new ImageSharpImageUrlGenerator(new string[0]); + private static readonly ImageSharpImageUrlGenerator s_generator = new ImageSharpImageUrlGenerator(new string[0], null); [Test] public void GetImageUrl_CropAliasTest() @@ -162,5 +162,29 @@ public void GetImageUrl_BackgroundColorParameter() var urlString = s_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 generator = new ImageSharpImageUrlGenerator(new string[0], 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 }); + 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&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2&rnd=not-included-in-hmac", generator.GetImageUrl(options)); + + // Removing height should generate a different HMAC + options.Height = null; + Assert.AreEqual(MediaPath + "?width=400&hmac=5bd24a05de5ea068533579863773ddac9269482ad515575be4aace7e9e50c88c&rnd=not-included-in-hmac", 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&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2&rnd=not-included-in-hmac", generator.GetImageUrl(options)); + } } } From 3dd078aef65442fb91f1db99225c3124a98e6e2e Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 28 Apr 2022 20:21:23 +0200 Subject: [PATCH 22/37] Ensure backoffice image URLs are generated by the server (and include a correct HMAC token) --- .../Controllers/ImagesController.cs | 14 +++++++------- .../src/common/services/mediahelper.service.js | 2 +- .../fileupload/fileupload.controller.js | 12 ++---------- .../imagecropper/imagecropper.controller.js | 5 ++--- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs index 2ec30456ca4c..0a9821cf8c39 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -1,11 +1,11 @@ using System; +using System.Globalization; using System.IO; using System.Web; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -59,7 +59,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 @@ -88,13 +87,14 @@ 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 rnd = imageLastModified.HasValue ? imageLastModified.Value.ToFileTime().ToString(CultureInfo.InvariantCulture) : null; var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(encodedImagePath) { Width = width, ImageCropMode = ImageCropMode.Max, CacheBusterValue = rnd }); + if (Url.IsLocalUrl(imageUrl)) { return new LocalRedirectResult(imageUrl, false); @@ -118,18 +118,18 @@ 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. /// - public string? GetProcessedImageUrl(string imagePath, + public string? GetProcessedImageUrl( + string imagePath, int? width = null, int? height = null, decimal? focalPointLeft = null, decimal? focalPointTop = null, ImageCropMode mode = ImageCropMode.Max, - string cacheBusterValue = "", + string? cacheBusterValue = null, decimal? cropX1 = null, decimal? cropX2 = null, decimal? cropY1 = null, - decimal? cropY2 = null - ) + decimal? cropY2 = null) { var options = new ImageUrlGenerationOptions(imagePath) { 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/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/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js index 8bb50a07dc01..cce9395c01af 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 @@ -234,9 +234,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; } From 8bd54f26c72c40c7c7e0a3a57ee9523483ac3289 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 29 Apr 2022 13:07:59 +0200 Subject: [PATCH 23/37] Abstract HMAC generation to IImageUrlTokenGenerator --- .../Configuration/Models/ImagingSettings.cs | 2 +- .../Media/IImageUrlTokenGenerator.cs | 20 +++++ .../ConfigureImageSharpMiddlewareOptions.cs | 14 ++- .../UmbracoBuilder.ImageSharp.cs | 1 + .../Media/ImageSharpImageUrlGenerator.cs | 27 +++--- .../Media/ImageSharpImageUrlTokenGenerator.cs | 89 +++++++++++++++++++ .../Media/ImageSharpImageUrlGeneratorTests.cs | 4 +- 7 files changed, 137 insertions(+), 20 deletions(-) create mode 100644 src/Umbraco.Core/Media/IImageUrlTokenGenerator.cs create mode 100644 src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs diff --git a/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs b/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs index 60658ae916c8..e749774236c8 100644 --- a/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ImagingSettings.cs @@ -10,7 +10,7 @@ namespace Umbraco.Cms.Core.Configuration.Models public class ImagingSettings { /// - /// Gets or sets a value for the HMAC security key. + /// Gets or sets a value for the Hash-based Message Authentication Code (HMAC) secret key for request authentication. /// public byte[]? HMACSecretKey { get; set; } diff --git a/src/Umbraco.Core/Media/IImageUrlTokenGenerator.cs b/src/Umbraco.Core/Media/IImageUrlTokenGenerator.cs new file mode 100644 index 000000000000..95fa43d81515 --- /dev/null +++ b/src/Umbraco.Core/Media/IImageUrlTokenGenerator.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Umbraco.Cms.Core.Media +{ + /// + /// Exposes a method that generates an image URL token for request authentication. + /// + public interface IImageUrlTokenGenerator + { + /// + /// Gets the image URL token for request authentication. + /// + /// The image URL. + /// The commands to include. + /// + /// The generated image URL token. + /// + string? GetImageUrlToken(string imageUrl, IEnumerable> commands); + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs index e64c19ea685e..66896965fef8 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Headers; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; @@ -9,6 +10,7 @@ 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 { @@ -20,16 +22,18 @@ public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions /// Initializes a new instance of the class. /// /// The ImageSharp configuration. /// The Umbraco imaging settings. - public ConfigureImageSharpMiddlewareOptions(Configuration configuration, IOptions imagingSettings) + public ConfigureImageSharpMiddlewareOptions(Configuration configuration, IOptions imagingSettings, IImageUrlTokenGenerator imageUrlTokenGenerator) { _configuration = configuration; _imagingSettings = imagingSettings.Value; + _imageUrlTokenGenerator = imageUrlTokenGenerator; } /// @@ -42,6 +46,14 @@ public void Configure(ImageSharpMiddlewareOptions options) options.CacheMaxAge = _imagingSettings.Cache.CacheMaxAge; options.CacheHashLength = _imagingSettings.Cache.CacheHashLength; + // Use our own image URL token generator + options.OnComputeHMACAsync = (context, secret) => + { + string imageUrl = UriHelper.BuildRelative(context.Context.Request.PathBase, context.Context.Request.Path); + + return Task.FromResult(_imageUrlTokenGenerator.GetImageUrlToken(imageUrl, context.Commands)); + }; + // Use configurable maximum width and height options.OnParseCommandsAsync = context => { diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index cfba33d0ae7b..edbf16c4706e 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -20,6 +20,7 @@ public static partial class UmbracoBuilderExtensions public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder) { builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddImageSharp() // Replace default image provider diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs index d310a419c89a..1afd9ca5e839 100644 --- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs @@ -2,14 +2,11 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Web; using SixLabors.ImageSharp.Web.Processors; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.ImageProcessors; @@ -23,7 +20,7 @@ namespace Umbraco.Cms.Web.Common.Media /// public class ImageSharpImageUrlGenerator : IImageUrlGenerator { - private readonly byte[]? _hmacSecretKey; + private readonly IImageUrlTokenGenerator _imageUrlTokenGenerator; /// public IEnumerable SupportedImageFileTypes { get; } @@ -32,26 +29,26 @@ public class ImageSharpImageUrlGenerator : IImageUrlGenerator /// Initializes a new instance of the class. /// /// The ImageSharp configuration. - /// The Umbraco imaging settings. - public ImageSharpImageUrlGenerator(Configuration configuration, IOptions imagingSettings) - : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray(), imagingSettings.Value.HMACSecretKey) + /// The image URL token generator. + public ImageSharpImageUrlGenerator(Configuration configuration, IImageUrlTokenGenerator imageUrlTokenGenerator) + : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray(), imageUrlTokenGenerator) { } /// /// Initializes a new instance of the class. /// /// The supported image file types/extensions. - /// The HMAC security key. + /// The image URL token generator. /// /// This constructor is only used for testing. /// - internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes, byte[]? hmacSecretKey) + internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes, IImageUrlTokenGenerator imageUrlTokenGenerator) { SupportedImageFileTypes = supportedImageFileTypes; - _hmacSecretKey = hmacSecretKey; + _imageUrlTokenGenerator = imageUrlTokenGenerator ?? throw new ArgumentNullException(nameof(imageUrlTokenGenerator)); } - /// + /// public string? GetImageUrl(ImageUrlGenerationOptions options) { if (options?.ImageUrl == null) @@ -101,14 +98,12 @@ internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes queryString.Add(kvp.Key, kvp.Value); } - if (_hmacSecretKey is byte[] secret && secret.Length > 0) + if (_imageUrlTokenGenerator.GetImageUrlToken(options.ImageUrl, queryString) is string token && !string.IsNullOrEmpty(token)) { - string value = CaseHandlingUriBuilder.BuildRelative(CaseHandlingUriBuilder.CaseHandling.LowerInvariant, null, options.ImageUrl, QueryString.Create(queryString)); - - queryString.Add(HMACUtilities.TokenCommand, HMACUtilities.ComputeHMACSHA256(value, secret)); + queryString.Add(HMACUtilities.TokenCommand, token); } - if (options.CacheBusterValue is string cacheBusterValue && !string.IsNullOrWhiteSpace(cacheBusterValue)) + if (options.CacheBusterValue is string cacheBusterValue && !string.IsNullOrEmpty(cacheBusterValue)) { queryString.Add("rnd", cacheBusterValue); } diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs new file mode 100644 index 000000000000..00d1df4dc289 --- /dev/null +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using SixLabors.ImageSharp.Web; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Media; + +namespace Umbraco.Cms.Web.Common.Media +{ + /// + /// Exposes a method that generates an image URL token for request authentication based . + /// + /// + public class ImageSharpImageUrlTokenGenerator : IImageUrlTokenGenerator + { + private readonly byte[]? _hmacSecretKey; + private readonly CaseHandlingUriBuilder.CaseHandling _caseHandling; + + /// + /// Initializes a new instance of the class. + /// + /// The Umbraco imaging settings. + public ImageSharpImageUrlTokenGenerator(IOptions imagingSettings) + : this(imagingSettings.Value.HMACSecretKey, CaseHandlingUriBuilder.CaseHandling.LowerInvariant) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The HMAC security key. + /// + /// This constructor is only used for testing. + /// + internal ImageSharpImageUrlTokenGenerator(byte[]? hmacSecretKey) + : this(hmacSecretKey, CaseHandlingUriBuilder.CaseHandling.LowerInvariant) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The HMAC security key. + /// Determines case handling for the result. + protected ImageSharpImageUrlTokenGenerator(byte[]? hmacSecretKey, CaseHandlingUriBuilder.CaseHandling caseHandling) + { + _hmacSecretKey = hmacSecretKey; + _caseHandling = caseHandling; + } + + /// + public string? GetImageUrlToken(string imageUrl, IEnumerable> commands) + { + if (_hmacSecretKey == null || _hmacSecretKey.Length == 0) + { + return null; + } + + QueryString queryString = CreateImageUrlTokenQueryString(commands); + string value = CaseHandlingUriBuilder.BuildRelative(_caseHandling, null, imageUrl, queryString); + + return ComputeImageUrlToken(value, _hmacSecretKey); + } + + /// + /// Creates the query string that is included in the image URL token. + /// + /// The commands to include. + /// + /// The query string. + /// + /// + /// The ImageSharp middleware computes the token using only known commands, + /// so if you've removed default processors or add unknown commands using the FurtherOptions, + /// you can use this method to filter out any unsupported/unknown commands. + /// + protected virtual QueryString CreateImageUrlTokenQueryString(IEnumerable> commands) + => QueryString.Create(commands); + + /// + /// Computes the image URL token by hashing the using the specified . + /// + /// The value to hash. + /// The secret key. + /// + /// The computed image URL token. + /// + protected virtual string ComputeImageUrlToken(string value, byte[] secret) + => HMACUtilities.ComputeHMACSHA256(value, secret); + } +} 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 3d0376157136..8bd7d5c87562 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs @@ -14,7 +14,7 @@ public class ImageSharpImageUrlGeneratorTests private static readonly ImageUrlGenerationOptions.CropCoordinates s_crop = new ImageUrlGenerationOptions.CropCoordinates(0.58729977382575338m, 0.055768992440203169m, 0m, 0.32457553600198386m); private static readonly ImageUrlGenerationOptions.FocalPointPosition s_focus1 = new ImageUrlGenerationOptions.FocalPointPosition(0.96m, 0.80827067669172936m); private static readonly ImageUrlGenerationOptions.FocalPointPosition s_focus2 = new ImageUrlGenerationOptions.FocalPointPosition(0.4275m, 0.41m); - private static readonly ImageSharpImageUrlGenerator s_generator = new ImageSharpImageUrlGenerator(new string[0], null); + private static readonly ImageSharpImageUrlGenerator s_generator = new ImageSharpImageUrlGenerator(new string[0], new ImageSharpImageUrlTokenGenerator((byte[])null)); [Test] public void GetImageUrl_CropAliasTest() @@ -169,7 +169,7 @@ public void GetImageUrl_BackgroundColorParameter() [Test] public void GetImageUrl_HMACSecurityKey() { - var generator = new ImageSharpImageUrlGenerator(new string[0], 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 }); + var generator = new ImageSharpImageUrlGenerator(new string[0], new ImageSharpImageUrlTokenGenerator(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 })); var options = new ImageUrlGenerationOptions(MediaPath) { Width = 400, Height = 400 }; Assert.AreEqual(MediaPath + "?width=400&height=400&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2", generator.GetImageUrl(options)); From e16940e0e847145a0ca6b4dd9cf96fd75f804fb0 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Sun, 1 May 2022 14:12:08 +0200 Subject: [PATCH 24/37] Change cache buster value to 'v' and use hexadecimal timestamp --- src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs | 2 +- .../Extensions/ImageCropperTemplateCoreExtensions.cs | 2 +- src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs | 2 +- .../Media/ImageSharpImageUrlGeneratorTests.cs | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs index 0a9821cf8c39..6b1ce581f36b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -87,7 +87,7 @@ public IActionResult GetResized(string imagePath, int width) // so ignore and we won't set a last modified date. } - var rnd = imageLastModified.HasValue ? imageLastModified.Value.ToFileTime().ToString(CultureInfo.InvariantCulture) : null; + var rnd = imageLastModified.HasValue ? imageLastModified.Value.ToFileTime().ToString("x", CultureInfo.InvariantCulture) : null; var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(encodedImagePath) { Width = width, diff --git a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs index 57779d20822e..dd9fe61cc36d 100644 --- a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs @@ -273,7 +273,7 @@ public static class ImageCropperTemplateCoreExtensions } } - var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null; + var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString("x", CultureInfo.InvariantCulture) : null; return GetCropUrl( mediaItemUrl, imageUrlGenerator, localCrops, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs index 1afd9ca5e839..fb05764baa4e 100644 --- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs @@ -105,7 +105,7 @@ internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes if (options.CacheBusterValue is string cacheBusterValue && !string.IsNullOrEmpty(cacheBusterValue)) { - queryString.Add("rnd", cacheBusterValue); + queryString.Add("v", cacheBusterValue); } return QueryHelpers.AddQueryString(options.ImageUrl, queryString); 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 8bd7d5c87562..eb5c977bd51c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs @@ -176,15 +176,15 @@ public void GetImageUrl_HMACSecurityKey() // CacheBusterValue isn't included in HMAC generation options.CacheBusterValue = "not-included-in-hmac"; - Assert.AreEqual(MediaPath + "?width=400&height=400&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2&rnd=not-included-in-hmac", generator.GetImageUrl(options)); + Assert.AreEqual(MediaPath + "?width=400&height=400&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2&v=not-included-in-hmac", generator.GetImageUrl(options)); // Removing height should generate a different HMAC options.Height = null; - Assert.AreEqual(MediaPath + "?width=400&hmac=5bd24a05de5ea068533579863773ddac9269482ad515575be4aace7e9e50c88c&rnd=not-included-in-hmac", generator.GetImageUrl(options)); + Assert.AreEqual(MediaPath + "?width=400&hmac=5bd24a05de5ea068533579863773ddac9269482ad515575be4aace7e9e50c88c&v=not-included-in-hmac", 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&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2&rnd=not-included-in-hmac", generator.GetImageUrl(options)); + Assert.AreEqual(MediaPath + "?width=400&height=400&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2&v=not-included-in-hmac", generator.GetImageUrl(options)); } } } From 845cadc1af29f78ed5676fa0af0ea26fb348a50c Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Sun, 1 May 2022 14:22:27 +0200 Subject: [PATCH 25/37] Update comments --- .../ConfigureImageSharpMiddlewareOptions.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs index 66896965fef8..aa2535cc9b63 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs @@ -46,8 +46,8 @@ public void Configure(ImageSharpMiddlewareOptions options) options.CacheMaxAge = _imagingSettings.Cache.CacheMaxAge; options.CacheHashLength = _imagingSettings.Cache.CacheHashLength; - // Use our own image URL token generator - options.OnComputeHMACAsync = (context, secret) => + // Use the image URL token generator to compute the HMAC + options.OnComputeHMACAsync = (context, _) => { string imageUrl = UriHelper.BuildRelative(context.Context.Request.PathBase, context.Context.Request.Path); @@ -59,6 +59,7 @@ public void Configure(ImageSharpMiddlewareOptions options) { if (context.Commands.Count == 0 || _imagingSettings.HMACSecretKey?.Length > 0) { + // Nothing to parse or using HMAC authentication return Task.CompletedTask; } @@ -88,9 +89,12 @@ public void Configure(ImageSharpMiddlewareOptions options) { Public = true }; - cacheControl.MustRevalidate = false; // ImageSharp enables this by default + + // 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; } From 9e7141fcabca6e4b9460744b8cabfd6cf4418986 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 6 May 2022 14:23:06 +0200 Subject: [PATCH 26/37] Fix backoffice thumbnail URL generation --- .../blockcard/umbBlockCard.component.js | 6 ++- .../grid/editors/media.controller.js | 48 ++++++++----------- 2 files changed, 24 insertions(+), 30 deletions(-) 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/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index 81a548a1169a..4ab6eba035c2 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; + var options = { + width: 800 + }; - if ($scope.control.editor.config && $scope.control.editor.config.size){ + 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}`; + // Use crop + options.crop = $scope.control.value.coordinates; } 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'; - } + // Otherwise use focal point + options.focalPoint = $scope.control.value.focalPoint; } - url += '&width=' + $scope.control.editor.config.size.width; - url += '&height=' + $scope.control.editor.config.size.height; - } - - // set default size if no crop present (moved from the view) - if (url.includes('?') === false) - { - url += '?width=800' + 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(); } }); From c9401c91d1526858cd95dcb54de654ac7021431c Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 6 May 2022 14:32:05 +0200 Subject: [PATCH 27/37] Update grid media thumbnail URL generation --- .../grid/editors/media.controller.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 4ab6eba035c2..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 @@ -67,15 +67,15 @@ angular.module("umbraco") width: 800 }; - if ($scope.control.editor.config && $scope.control.editor.config.size) { - if ($scope.control.value.coordinates) { - // Use crop - options.crop = $scope.control.value.coordinates; - } else { - // Otherwise use focal point - options.focalPoint = $scope.control.value.focalPoint; - } + 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; + } + 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; } From d9879ffa74e5c2fc9792616a60b00cadebb7b6df Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 30 Jun 2022 09:59:44 +0200 Subject: [PATCH 28/37] Remove breaking changes --- .../Controllers/ImagesController.cs | 8 +++---- .../ConfigureImageSharpMiddlewareOptions.cs | 15 +++++++++++-- .../UmbracoBuilder.ImageSharp.cs | 2 +- .../Media/ImageSharpImageUrlGenerator.cs | 22 ++++++++++++++----- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs index cab091691221..ed98c4da18ba 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -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,12 +83,12 @@ public IActionResult GetResized(string imagePath, int width) // so ignore and we won't set a last modified date. } - var rnd = imageLastModified.HasValue ? imageLastModified.Value.ToFileTime().ToString("x", CultureInfo.InvariantCulture) : 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)) diff --git a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs index fe76500262a2..f861d2a1e127 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs @@ -1,6 +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; @@ -13,7 +14,7 @@ namespace Umbraco.Cms.Web.Common.DependencyInjection; /// -/// Configures the ImageSharp middleware options. +/// Configures the ImageSharp middleware options. /// /// public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions @@ -23,7 +24,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 +36,16 @@ public ConfigureImageSharpMiddlewareOptions(Configuration configuration, IOption _imageUrlTokenGenerator = imageUrlTokenGenerator; } + /// + /// Initializes a new instance of the class. + /// + /// The ImageSharp configuration. + /// The Umbraco imaging settings. + [Obsolete("Use ctor with all params - This will be removed in Umbraco 12.")] + public ConfigureImageSharpMiddlewareOptions(Configuration configuration, IOptions imagingSettings) + : this(configuration, imagingSettings, StaticServiceProvider.Instance.GetRequiredService()) + { } + /// public void Configure(ImageSharpMiddlewareOptions options) { diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index fddc27248b38..e59227b7036a 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -15,7 +15,7 @@ namespace Umbraco.Extensions; public static partial class UmbracoBuilderExtensions { /// - /// Adds ImageSharp with Umbraco settings. + /// Adds ImageSharp with Umbraco settings. /// public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder) { diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs index 501f820ac6bb..64bbdbfe3213 100644 --- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs @@ -1,18 +1,20 @@ 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 @@ -20,22 +22,30 @@ public class ImageSharpImageUrlGenerator : IImageUrlGenerator private readonly IImageUrlTokenGenerator _imageUrlTokenGenerator; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The ImageSharp configuration. /// The image URL token generator. public ImageSharpImageUrlGenerator(Configuration configuration, IImageUrlTokenGenerator imageUrlTokenGenerator) : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray(), imageUrlTokenGenerator) - { - } + { } + + /// + /// 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, StaticServiceProvider.Instance.GetRequiredService()) + { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The supported image file types/extensions. /// The image URL token generator. /// - /// This constructor is only used for testing. + /// This constructor is only used for testing. /// internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes, IImageUrlTokenGenerator imageUrlTokenGenerator) { From 577f8661077e5d4105634509e3249a4bc8c97df9 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 30 Jun 2022 10:45:35 +0200 Subject: [PATCH 29/37] Strip unknown commands from image URL token --- .../Media/ImageSharpImageUrlTokenGenerator.cs | 50 ++++++---- .../Media/ImageSharpImageUrlGeneratorTests.cs | 98 +++++++++++++------ 2 files changed, 102 insertions(+), 46 deletions(-) diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs index 00d1df4dc289..3a40dddde156 100644 --- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs @@ -1,48 +1,55 @@ -using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Processors; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Media; namespace Umbraco.Cms.Web.Common.Media { /// - /// Exposes a method that generates an image URL token for request authentication based . + /// Exposes a method that generates an image URL token for request authentication. /// /// public class ImageSharpImageUrlTokenGenerator : IImageUrlTokenGenerator { private readonly byte[]? _hmacSecretKey; + private readonly Lazy?> _knownCommands; private readonly CaseHandlingUriBuilder.CaseHandling _caseHandling; /// /// Initializes a new instance of the class. /// /// The Umbraco imaging settings. - public ImageSharpImageUrlTokenGenerator(IOptions imagingSettings) - : this(imagingSettings.Value.HMACSecretKey, CaseHandlingUriBuilder.CaseHandling.LowerInvariant) + /// "The image web processors to retrieve the known commands, used to strip out unsupported commands from the generated image URL token. + public ImageSharpImageUrlTokenGenerator(IOptions imagingSettings, Lazy> imageWebProcessors) + : this(imagingSettings.Value.HMACSecretKey, imageWebProcessors, CaseHandlingUriBuilder.CaseHandling.LowerInvariant) { } /// /// Initializes a new instance of the class. /// /// The HMAC security key. + /// "The known commands, used to strip out unsupported commands from the generated image URL token. /// /// This constructor is only used for testing. /// - internal ImageSharpImageUrlTokenGenerator(byte[]? hmacSecretKey) - : this(hmacSecretKey, CaseHandlingUriBuilder.CaseHandling.LowerInvariant) - { } + internal ImageSharpImageUrlTokenGenerator(byte[]? hmacSecretKey, IList? knownCommands = null) + : this(hmacSecretKey, null, CaseHandlingUriBuilder.CaseHandling.LowerInvariant) + { + _knownCommands = new Lazy?>(knownCommands); + } /// /// Initializes a new instance of the class. /// /// The HMAC security key. + /// "The image web processors to retrieve the known commands, used to strip out unsupported commands from the generated image URL token. /// Determines case handling for the result. - protected ImageSharpImageUrlTokenGenerator(byte[]? hmacSecretKey, CaseHandlingUriBuilder.CaseHandling caseHandling) + protected ImageSharpImageUrlTokenGenerator(byte[]? hmacSecretKey, Lazy>? imageWebProcessors, CaseHandlingUriBuilder.CaseHandling caseHandling) { _hmacSecretKey = hmacSecretKey; + _knownCommands = new Lazy?>(() => imageWebProcessors?.Value.SelectMany(x => x.Commands).Distinct().ToList()); _caseHandling = caseHandling; } @@ -54,26 +61,32 @@ protected ImageSharpImageUrlTokenGenerator(byte[]? hmacSecretKey, CaseHandlingUr return null; } - QueryString queryString = CreateImageUrlTokenQueryString(commands); + var queryString = QueryString.Create(StripCommands(commands)); string value = CaseHandlingUriBuilder.BuildRelative(_caseHandling, null, imageUrl, queryString); return ComputeImageUrlToken(value, _hmacSecretKey); } /// - /// Creates the query string that is included in the image URL token. + /// Strips out any commands from being included in the image URL token. /// - /// The commands to include. + /// The commands. /// - /// The query string. + /// The commands used when generating and validating the image URL token. /// /// - /// The ImageSharp middleware computes the token using only known commands, - /// so if you've removed default processors or add unknown commands using the FurtherOptions, - /// you can use this method to filter out any unsupported/unknown commands. + /// Unknown commands will already be removed by ImageSharp when generating the image URL token, so this ensures it's also done when validating. + /// This can be overwritten to strip out additional commands, enabling a way to whitelist specific commands. /// - protected virtual QueryString CreateImageUrlTokenQueryString(IEnumerable> commands) - => QueryString.Create(commands); + protected virtual IEnumerable> StripCommands(IEnumerable> commands) + { + if (_knownCommands.Value is IList knownCommands) + { + return commands.Where(x => knownCommands.Contains(x.Key)); + } + + return commands; + } /// /// Computes the image URL token by hashing the using the specified . @@ -83,6 +96,9 @@ protected virtual QueryString CreateImageUrlTokenQueryString(IEnumerable /// The computed image URL token. /// + /// + /// This is used for both generating and validating the image URL token, therefore can be overwritten to change the hashing algorithm. + /// protected virtual string ComputeImageUrlToken(string value, byte[] secret) => HMACUtilities.ComputeHMACSHA256(value, secret); } 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 bf76a51403bb..a5d81b6099b9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Collections.Generic; using NUnit.Framework; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.Media; @@ -11,17 +12,17 @@ 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 ImageUrlGenerationOptions.CropCoordinates(0.58729977382575338m, 0.055768992440203169m, 0m, 0.32457553600198386m); - private static readonly ImageUrlGenerationOptions.FocalPointPosition s_focus1 = new ImageUrlGenerationOptions.FocalPointPosition(0.96m, 0.80827067669172936m); - private static readonly ImageUrlGenerationOptions.FocalPointPosition s_focus2 = new ImageUrlGenerationOptions.FocalPointPosition(0.4275m, 0.41m); - private static readonly ImageSharpImageUrlGenerator s_generator = new ImageSharpImageUrlGenerator(new string[0], new ImageSharpImageUrlTokenGenerator((byte[])null)); + 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], new ImageSharpImageUrlTokenGenerator((byte[])null)); [Test] public void GetImageUrl_CropAliasTest() { - var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) + var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { - Crop = s_crop, + Crop = _crop, Width = 100, Height = 100, }); @@ -32,9 +33,9 @@ public void GetImageUrl_CropAliasTest() [Test] public void GetImageUrl_WidthHeightTest() { - var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) + var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { - FocalPoint = s_focus1, + FocalPoint = _focus1, Width = 200, Height = 300, }); @@ -45,9 +46,9 @@ public void GetImageUrl_WidthHeightTest() [Test] public void GetImageUrl_FocalPointTest() { - var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) + var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { - FocalPoint = s_focus1, + FocalPoint = _focus1, Width = 100, Height = 100, }); @@ -58,9 +59,9 @@ public void GetImageUrl_FocalPointTest() [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", @@ -75,7 +76,7 @@ public void GetImageUrlFurtherOptionsTest() [Test] public void GetImageUrlNullOptionsTest() { - var urlString = s_generator.GetImageUrl(null); + var urlString = _generator.GetImageUrl(null); Assert.AreEqual(null, urlString); } @@ -85,7 +86,7 @@ public void GetImageUrlNullOptionsTest() [Test] public void GetImageUrlNullTest() { - var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(null)); + var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(null)); Assert.AreEqual(null, urlString); } @@ -95,7 +96,7 @@ public void GetImageUrlNullTest() [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); } @@ -105,9 +106,9 @@ public void GetImageUrlEmptyTest() [Test] public void GetBaseCropUrlFromModelTest() { - var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty) + var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(string.Empty) { - Crop = s_crop, + Crop = _crop, Width = 100, Height = 100, }); @@ -121,35 +122,35 @@ public void GetBaseCropUrlFromModelTest() [Test] public void GetImageUrl_SpecifiedCropModeTest() { - var urlStringMin = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) + var urlStringMin = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = ImageCropMode.Min, Width = 300, Height = 150, }); - var urlStringBoxPad = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) + var urlStringBoxPad = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = ImageCropMode.BoxPad, Width = 300, Height = 150, }); - var urlStringPad = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) + var urlStringPad = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = ImageCropMode.Pad, Width = 300, Height = 150, }); - var urlStringMax = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) + var urlStringMax = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = ImageCropMode.Max, Width = 300, Height = 150, }); - var urlStringStretch = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) + var urlStringStretch = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = ImageCropMode.Stretch, Width = 300, @@ -169,7 +170,7 @@ public void GetImageUrl_SpecifiedCropModeTest() [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, @@ -186,7 +187,7 @@ public void GetImageUrl_UploadTypeTest() [Test] public void GetImageUrl_PreferFocalPointCenter() { - var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) + var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Width = 300, Height = 150, @@ -201,9 +202,9 @@ public void GetImageUrl_PreferFocalPointCenter() [Test] public void GetImageUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore() { - var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) + var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { - FocalPoint = s_focus2, + FocalPoint = _focus2, Width = 270, Height = 161, }); @@ -217,7 +218,7 @@ public void GetImageUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore( [Test] public void GetImageUrl_WidthOnlyParameter() { - var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) + var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Width = 200, }); @@ -231,7 +232,7 @@ public void GetImageUrl_WidthOnlyParameter() [Test] public void GetImageUrl_HeightOnlyParameter() { - var urlString = s_generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) + var urlString = _generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Height = 200, }); @@ -245,7 +246,7 @@ public void GetImageUrl_HeightOnlyParameter() [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, @@ -283,4 +284,43 @@ public void GetImageUrl_HMACSecurityKey() options.FurtherOptions = "height=400"; Assert.AreEqual(MediaPath + "?width=400&height=400&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2&v=not-included-in-hmac", generator.GetImageUrl(options)); } + + /// + /// Test to check result when using a HMAC security key and custom known commands. + /// + [Test] + public void GetImageUrl_HMACSecurityKeyKnownCommands() + { + var knownCommands = new List() + { + "width", + "height", + "v" // The cache buster value is never included in the HMAC + }; + + var generator = new ImageSharpImageUrlGenerator(new string[0], new ImageSharpImageUrlTokenGenerator(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 }, knownCommands)); + 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 = "never-included-in-hmac"; + Assert.AreEqual(MediaPath + "?width=400&height=400&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2&v=never-included-in-hmac", generator.GetImageUrl(options)); + + // Removing height should generate a different HMAC + options.Height = null; + Assert.AreEqual(MediaPath + "?width=400&hmac=5bd24a05de5ea068533579863773ddac9269482ad515575be4aace7e9e50c88c&v=never-included-in-hmac", generator.GetImageUrl(options)); + + // Adding an unknown command shouldn't chnage the HMAC + options.FurtherOptions = "whitelisted=whatever"; + Assert.AreEqual(MediaPath + "?width=400&whitelisted=whatever&hmac=5bd24a05de5ea068533579863773ddac9269482ad515575be4aace7e9e50c88c&v=never-included-in-hmac", generator.GetImageUrl(options)); + + // But adding the height again using FurtherOptions should produce the same HMAC as before + options.FurtherOptions = "height=400"; + Assert.AreEqual(MediaPath + "?width=400&height=400&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2&v=never-included-in-hmac", generator.GetImageUrl(options)); + } } From c627a90ac971d55d178c9d601a91f18972c622c3 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 30 Jun 2022 11:53:51 +0200 Subject: [PATCH 30/37] Remove HMAC whitelisting possibility (not supported by ImageSharp) --- .../Media/IImageUrlTokenGenerator.cs | 2 -- .../Media/ImageSharpImageUrlTokenGenerator.cs | 34 +++++++------------ 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Core/Media/IImageUrlTokenGenerator.cs b/src/Umbraco.Core/Media/IImageUrlTokenGenerator.cs index 95fa43d81515..3feec4e45682 100644 --- a/src/Umbraco.Core/Media/IImageUrlTokenGenerator.cs +++ b/src/Umbraco.Core/Media/IImageUrlTokenGenerator.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - namespace Umbraco.Cms.Core.Media { /// diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs index 3a40dddde156..657a27181ee2 100644 --- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Commands; using SixLabors.ImageSharp.Web.Processors; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Media; @@ -61,31 +62,20 @@ protected ImageSharpImageUrlTokenGenerator(byte[]? hmacSecretKey, Lazy - /// Strips out any commands from being included in the image URL token. - /// - /// The commands. - /// - /// The commands used when generating and validating the image URL token. - /// - /// - /// Unknown commands will already be removed by ImageSharp when generating the image URL token, so this ensures it's also done when validating. - /// This can be overwritten to strip out additional commands, enabling a way to whitelist specific commands. - /// - protected virtual IEnumerable> StripCommands(IEnumerable> commands) - { - if (_knownCommands.Value is IList knownCommands) + QueryString queryString; + if (commands is not CommandCollection && _knownCommands.Value is IList knownCommands) + { + // Commands are of type CommandCollection when validating the HMAC and already filtered, so optimize for that + queryString = QueryString.Create(commands.Where(x => knownCommands.Contains(x.Key))); + } + else { - return commands.Where(x => knownCommands.Contains(x.Key)); + queryString = QueryString.Create(commands); } - return commands; + string value = CaseHandlingUriBuilder.BuildRelative(_caseHandling, null, imageUrl, queryString); + + return ComputeImageUrlToken(value, _hmacSecretKey); } /// From e19b2d2d74f30214e1243b0e1cd00c15af1dacac Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 30 Jun 2022 13:52:14 +0200 Subject: [PATCH 31/37] Update to SixLabors.ImageSharp 2.1.3 --- .../Umbraco.Infrastructure.csproj | 2 +- .../Controllers/ImagesController.cs | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index fc41bec6c55b..7d48468a96e8 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -48,7 +48,7 @@ - + diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs index ed98c4da18ba..b0b9e7a1aaea 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -115,17 +115,17 @@ 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. /// public string? GetProcessedImageUrl( - string imagePath, - int? width = null, - int? height = null, - decimal? focalPointLeft = null, - decimal? focalPointTop = null, - ImageCropMode mode = ImageCropMode.Max, - string cacheBusterValue = "", - decimal? cropX1 = null, - decimal? cropX2 = null, - decimal? cropY1 = null, - decimal? cropY2 = null) + string imagePath, + int? width = null, + int? height = null, + decimal? focalPointLeft = null, + decimal? focalPointTop = null, + ImageCropMode mode = ImageCropMode.Max, + string cacheBusterValue = "", // TODO Change to: string? cacheBusterValue = null + decimal? cropX1 = null, + decimal? cropX2 = null, + decimal? cropY1 = null, + decimal? cropY2 = null) { var options = new ImageUrlGenerationOptions(imagePath) { From 1748b787cc377507d34a7df1aef97be722161ff7 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 5 Jul 2022 13:18:57 +0200 Subject: [PATCH 32/37] Add comment to internal constructor --- src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs index 657a27181ee2..add9d68c7572 100644 --- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs @@ -38,6 +38,7 @@ public ImageSharpImageUrlTokenGenerator(IOptions imagingSetting internal ImageSharpImageUrlTokenGenerator(byte[]? hmacSecretKey, IList? knownCommands = null) : this(hmacSecretKey, null, CaseHandlingUriBuilder.CaseHandling.LowerInvariant) { + // Overwrite with the list explicitly passed into this overload _knownCommands = new Lazy?>(knownCommands); } From 69ab5af58fa413ce47982867bee4e13ad4dd2fe3 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 5 Jul 2022 13:30:47 +0200 Subject: [PATCH 33/37] Fix to support absolute image URLs --- .../Media/ImageSharpImageUrlTokenGenerator.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs index add9d68c7572..4a8f26536c9e 100644 --- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs @@ -63,15 +63,21 @@ protected ImageSharpImageUrlTokenGenerator(byte[]? hmacSecretKey, Lazy knownCommands) + if (commands is CommandCollection || _knownCommands.Value is not IList knownCommands) { - // Commands are of type CommandCollection when validating the HMAC and already filtered, so optimize for that - queryString = QueryString.Create(commands.Where(x => knownCommands.Contains(x.Key))); + // Commands are of type CommandCollection when validating the HMAC and already filtered (so optimize for that) + queryString = QueryString.Create(commands); } else { - queryString = QueryString.Create(commands); + queryString = QueryString.Create(commands.Where(x => knownCommands.Contains(x.Key))); } string value = CaseHandlingUriBuilder.BuildRelative(_caseHandling, null, imageUrl, queryString); From dd184d069fd58b8bf2d6746772246027c7992c7c Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 25 Aug 2022 00:08:29 +0200 Subject: [PATCH 34/37] Update to SixLabors.ImageSharp.Web 2.0.3-alpha.0.3 --- Directory.Build.props | 4 ++++ src/Umbraco.Web.Common/Umbraco.Web.Common.csproj | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 8ae2e0baec45..67727df0cd7d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,4 +15,8 @@ all + + + https://www.myget.org/F/sixlabors/api/v3/index.json + diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index c75ddfe98fd8..cd179aae9f4e 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -40,7 +40,7 @@ - + From 40f694d1fe2e22c0a51aee6f299ab2ab528b13ec Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 25 Aug 2022 00:09:47 +0200 Subject: [PATCH 35/37] Remove IImageUrlTokenGenerator and use ImageSharpRequestAuthorizationUtilities --- .../Media/IImageUrlTokenGenerator.cs | 18 ---- .../ConfigureImageSharpMiddlewareOptions.cs | 23 +--- .../UmbracoBuilder.ImageSharp.cs | 1 - .../Media/ImageSharpImageUrlGenerator.cs | 28 ++--- .../Media/ImageSharpImageUrlTokenGenerator.cs | 102 ------------------ .../Media/ImageSharpImageUrlGeneratorTests.cs | 71 +++++------- 6 files changed, 43 insertions(+), 200 deletions(-) delete mode 100644 src/Umbraco.Core/Media/IImageUrlTokenGenerator.cs delete mode 100644 src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs diff --git a/src/Umbraco.Core/Media/IImageUrlTokenGenerator.cs b/src/Umbraco.Core/Media/IImageUrlTokenGenerator.cs deleted file mode 100644 index 3feec4e45682..000000000000 --- a/src/Umbraco.Core/Media/IImageUrlTokenGenerator.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Umbraco.Cms.Core.Media -{ - /// - /// Exposes a method that generates an image URL token for request authentication. - /// - public interface IImageUrlTokenGenerator - { - /// - /// Gets the image URL token for request authentication. - /// - /// The image URL. - /// The commands to include. - /// - /// The generated image URL token. - /// - string? GetImageUrlToken(string imageUrl, IEnumerable> commands); - } -} diff --git a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs index f861d2a1e127..64e6ca19a68e 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ConfigureImageSharpMiddlewareOptions.cs @@ -21,31 +21,18 @@ public sealed class ConfigureImageSharpMiddlewareOptions : IConfigureOptions /// Initializes a new instance of the class. /// /// The ImageSharp configuration. /// The Umbraco imaging settings. - /// The image URL token generator. - public ConfigureImageSharpMiddlewareOptions(Configuration configuration, IOptions imagingSettings, IImageUrlTokenGenerator imageUrlTokenGenerator) + public ConfigureImageSharpMiddlewareOptions(Configuration configuration, IOptions imagingSettings) { _configuration = configuration; _imagingSettings = imagingSettings.Value; - _imageUrlTokenGenerator = imageUrlTokenGenerator; } - /// - /// Initializes a new instance of the class. - /// - /// The ImageSharp configuration. - /// The Umbraco imaging settings. - [Obsolete("Use ctor with all params - This will be removed in Umbraco 12.")] - public ConfigureImageSharpMiddlewareOptions(Configuration configuration, IOptions imagingSettings) - : this(configuration, imagingSettings, StaticServiceProvider.Instance.GetRequiredService()) - { } - /// public void Configure(ImageSharpMiddlewareOptions options) { @@ -56,14 +43,6 @@ public void Configure(ImageSharpMiddlewareOptions options) options.CacheMaxAge = _imagingSettings.Cache.CacheMaxAge; options.CacheHashLength = _imagingSettings.Cache.CacheHashLength; - // Use the image URL token generator to compute the HMAC - options.OnComputeHMACAsync = (context, _) => - { - string imageUrl = UriHelper.BuildRelative(context.Context.Request.PathBase, context.Context.Request.Path); - - return Task.FromResult(_imageUrlTokenGenerator.GetImageUrlToken(imageUrl, context.Commands)); - }; - // Use configurable maximum width and height options.OnParseCommandsAsync = context => { diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index e59227b7036a..f67858c4b7de 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -20,7 +20,6 @@ public static partial class UmbracoBuilderExtensions public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder builder) { builder.Services.AddSingleton(); - builder.Services.AddSingleton(); // Add ImageSharp, replace default image provider and add custom processors builder.Services.AddImageSharp() diff --git a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs index 64bbdbfe3213..08152ee31353 100644 --- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs +++ b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlGenerator.cs @@ -19,15 +19,15 @@ namespace Umbraco.Cms.Web.Common.Media; /// public class ImageSharpImageUrlGenerator : IImageUrlGenerator { - private readonly IImageUrlTokenGenerator _imageUrlTokenGenerator; + private readonly ImageSharpRequestAuthorizationUtilities? _requestAuthorizationUtilities; /// /// Initializes a new instance of the class. /// /// The ImageSharp configuration. - /// The image URL token generator. - public ImageSharpImageUrlGenerator(Configuration configuration, IImageUrlTokenGenerator imageUrlTokenGenerator) - : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray(), imageUrlTokenGenerator) + /// Contains helpers that allow authorization of image requests. + public ImageSharpImageUrlGenerator(Configuration configuration, ImageSharpRequestAuthorizationUtilities? requestAuthorizationUtilities) + : this(configuration.ImageFormats.SelectMany(f => f.FileExtensions).ToArray(), requestAuthorizationUtilities) { } /// @@ -36,21 +36,21 @@ public ImageSharpImageUrlGenerator(Configuration configuration, IImageUrlTokenGe /// The ImageSharp configuration. [Obsolete("Use ctor with all params - This will be removed in Umbraco 12.")] public ImageSharpImageUrlGenerator(Configuration configuration) - : this(configuration, StaticServiceProvider.Instance.GetRequiredService()) + : this(configuration, StaticServiceProvider.Instance.GetService()) { } /// /// Initializes a new instance of the class. /// /// The supported image file types/extensions. - /// The image URL token generator. + /// Contains helpers that allow authorization of image requests. /// /// This constructor is only used for testing. /// - internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes, IImageUrlTokenGenerator imageUrlTokenGenerator) + internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes, ImageSharpRequestAuthorizationUtilities? requestAuthorizationUtilities = null) { SupportedImageFileTypes = supportedImageFileTypes; - _imageUrlTokenGenerator = imageUrlTokenGenerator; + _requestAuthorizationUtilities = requestAuthorizationUtilities; } /// @@ -106,14 +106,18 @@ internal ImageSharpImageUrlGenerator(IEnumerable supportedImageFileTypes queryString.Add(kvp.Key, kvp.Value); } - if (_imageUrlTokenGenerator.GetImageUrlToken(options.ImageUrl, queryString) is string token && !string.IsNullOrEmpty(token)) + if (options.CacheBusterValue is string cacheBusterValue && !string.IsNullOrEmpty(cacheBusterValue)) { - queryString.Add(HMACUtilities.TokenCommand, token); + queryString.Add("v", cacheBusterValue); } - if (options.CacheBusterValue is string cacheBusterValue && !string.IsNullOrEmpty(cacheBusterValue)) + if (_requestAuthorizationUtilities is not null) { - queryString.Add("v", 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/Media/ImageSharpImageUrlTokenGenerator.cs b/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs deleted file mode 100644 index 4a8f26536c9e..000000000000 --- a/src/Umbraco.Web.Common/Media/ImageSharpImageUrlTokenGenerator.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; -using SixLabors.ImageSharp.Web; -using SixLabors.ImageSharp.Web.Commands; -using SixLabors.ImageSharp.Web.Processors; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Media; - -namespace Umbraco.Cms.Web.Common.Media -{ - /// - /// Exposes a method that generates an image URL token for request authentication. - /// - /// - public class ImageSharpImageUrlTokenGenerator : IImageUrlTokenGenerator - { - private readonly byte[]? _hmacSecretKey; - private readonly Lazy?> _knownCommands; - private readonly CaseHandlingUriBuilder.CaseHandling _caseHandling; - - /// - /// Initializes a new instance of the class. - /// - /// The Umbraco imaging settings. - /// "The image web processors to retrieve the known commands, used to strip out unsupported commands from the generated image URL token. - public ImageSharpImageUrlTokenGenerator(IOptions imagingSettings, Lazy> imageWebProcessors) - : this(imagingSettings.Value.HMACSecretKey, imageWebProcessors, CaseHandlingUriBuilder.CaseHandling.LowerInvariant) - { } - - /// - /// Initializes a new instance of the class. - /// - /// The HMAC security key. - /// "The known commands, used to strip out unsupported commands from the generated image URL token. - /// - /// This constructor is only used for testing. - /// - internal ImageSharpImageUrlTokenGenerator(byte[]? hmacSecretKey, IList? knownCommands = null) - : this(hmacSecretKey, null, CaseHandlingUriBuilder.CaseHandling.LowerInvariant) - { - // Overwrite with the list explicitly passed into this overload - _knownCommands = new Lazy?>(knownCommands); - } - - /// - /// Initializes a new instance of the class. - /// - /// The HMAC security key. - /// "The image web processors to retrieve the known commands, used to strip out unsupported commands from the generated image URL token. - /// Determines case handling for the result. - protected ImageSharpImageUrlTokenGenerator(byte[]? hmacSecretKey, Lazy>? imageWebProcessors, CaseHandlingUriBuilder.CaseHandling caseHandling) - { - _hmacSecretKey = hmacSecretKey; - _knownCommands = new Lazy?>(() => imageWebProcessors?.Value.SelectMany(x => x.Commands).Distinct().ToList()); - _caseHandling = caseHandling; - } - - /// - public string? GetImageUrlToken(string imageUrl, IEnumerable> commands) - { - if (_hmacSecretKey == null || _hmacSecretKey.Length == 0) - { - return null; - } - - if (Uri.TryCreate(imageUrl, UriKind.Absolute, out Uri? imageUri)) - { - // Only use the path - imageUrl = imageUri.AbsolutePath; - } - - QueryString queryString; - if (commands is CommandCollection || _knownCommands.Value is not IList knownCommands) - { - // Commands are of type CommandCollection when validating the HMAC and already filtered (so optimize for that) - queryString = QueryString.Create(commands); - } - else - { - queryString = QueryString.Create(commands.Where(x => knownCommands.Contains(x.Key))); - } - - string value = CaseHandlingUriBuilder.BuildRelative(_caseHandling, null, imageUrl, queryString); - - return ComputeImageUrlToken(value, _hmacSecretKey); - } - - /// - /// Computes the image URL token by hashing the using the specified . - /// - /// The value to hash. - /// The secret key. - /// - /// The computed image URL token. - /// - /// - /// This is used for both generating and validating the image URL token, therefore can be overwritten to change the hashing algorithm. - /// - protected virtual string ComputeImageUrlToken(string value, byte[] secret) - => HMACUtilities.ComputeHMACSHA256(value, secret); - } -} 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 a5d81b6099b9..733a737c434b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Media/ImageSharpImageUrlGeneratorTests.cs @@ -1,8 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; +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; @@ -15,7 +22,7 @@ public class ImageSharpImageUrlGeneratorTests 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], new ImageSharpImageUrlTokenGenerator((byte[])null)); + private static readonly ImageSharpImageUrlGenerator _generator = new ImageSharpImageUrlGenerator(new string[0]); [Test] public void GetImageUrl_CropAliasTest() @@ -263,7 +270,20 @@ public void GetImageUrl_BackgroundColorParameter() [Test] public void GetImageUrl_HMACSecurityKey() { - var generator = new ImageSharpImageUrlGenerator(new string[0], new ImageSharpImageUrlTokenGenerator(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 })); + 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, @@ -274,53 +294,14 @@ public void GetImageUrl_HMACSecurityKey() // CacheBusterValue isn't included in HMAC generation options.CacheBusterValue = "not-included-in-hmac"; - Assert.AreEqual(MediaPath + "?width=400&height=400&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2&v=not-included-in-hmac", generator.GetImageUrl(options)); + 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&hmac=5bd24a05de5ea068533579863773ddac9269482ad515575be4aace7e9e50c88c&v=not-included-in-hmac", generator.GetImageUrl(options)); + 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&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2&v=not-included-in-hmac", generator.GetImageUrl(options)); - } - - /// - /// Test to check result when using a HMAC security key and custom known commands. - /// - [Test] - public void GetImageUrl_HMACSecurityKeyKnownCommands() - { - var knownCommands = new List() - { - "width", - "height", - "v" // The cache buster value is never included in the HMAC - }; - - var generator = new ImageSharpImageUrlGenerator(new string[0], new ImageSharpImageUrlTokenGenerator(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 }, knownCommands)); - 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 = "never-included-in-hmac"; - Assert.AreEqual(MediaPath + "?width=400&height=400&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2&v=never-included-in-hmac", generator.GetImageUrl(options)); - - // Removing height should generate a different HMAC - options.Height = null; - Assert.AreEqual(MediaPath + "?width=400&hmac=5bd24a05de5ea068533579863773ddac9269482ad515575be4aace7e9e50c88c&v=never-included-in-hmac", generator.GetImageUrl(options)); - - // Adding an unknown command shouldn't chnage the HMAC - options.FurtherOptions = "whitelisted=whatever"; - Assert.AreEqual(MediaPath + "?width=400&whitelisted=whatever&hmac=5bd24a05de5ea068533579863773ddac9269482ad515575be4aace7e9e50c88c&v=never-included-in-hmac", generator.GetImageUrl(options)); - - // But adding the height again using FurtherOptions should produce the same HMAC as before - options.FurtherOptions = "height=400"; - Assert.AreEqual(MediaPath + "?width=400&height=400&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2&v=never-included-in-hmac", generator.GetImageUrl(options)); + Assert.AreEqual(MediaPath + "?width=400&height=400&v=not-included-in-hmac&hmac=6335195986da0663e23eaadfb9bb32d537375aaeec253aae66b8f4388506b4b2", generator.GetImageUrl(options)); } } From 5f0f351c69f038448ea7fddd161636c0cce00920 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 25 Aug 2022 09:21:49 +0200 Subject: [PATCH 36/37] Move NuGet feed to config file --- Directory.Build.props | 4 ---- nuget.config | 6 ++++++ 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 nuget.config diff --git a/Directory.Build.props b/Directory.Build.props index 67727df0cd7d..8ae2e0baec45 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,8 +15,4 @@ all - - - https://www.myget.org/F/sixlabors/api/v3/index.json - diff --git a/nuget.config b/nuget.config new file mode 100644 index 000000000000..6aa8697b8fe9 --- /dev/null +++ b/nuget.config @@ -0,0 +1,6 @@ + + + + + + From b4296a5cfffd1ced0e7d7bfa24a2dc2be62839c1 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 28 Apr 2023 12:54:02 +0200 Subject: [PATCH 37/37] Update to ImageSharp v3 --- .../ConfigureImageSharpMiddlewareOptions.cs | 1 - .../ImageProcessors/CropWebProcessor.cs | 8 ++--- .../ImageSharpComposer.cs | 1 - .../Media/ImageSharpDimensionExtractor.cs | 32 +++++++++++-------- .../Media/ImageSharpImageUrlGenerator.cs | 1 - .../Umbraco.Cms.Imaging.ImageSharp.csproj | 7 ++-- .../UmbracoBuilderExtensions.cs | 1 - 7 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/ConfigureImageSharpMiddlewareOptions.cs b/src/Umbraco.Cms.Imaging.ImageSharp/ConfigureImageSharpMiddlewareOptions.cs index 8daa1b689b01..aaaade354442 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/ConfigureImageSharpMiddlewareOptions.cs +++ b/src/Umbraco.Cms.Imaging.ImageSharp/ConfigureImageSharpMiddlewareOptions.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Http.Headers; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.Web.Commands; using SixLabors.ImageSharp.Web.Middleware; using SixLabors.ImageSharp.Web.Processors; diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/ImageProcessors/CropWebProcessor.cs b/src/Umbraco.Cms.Imaging.ImageSharp/ImageProcessors/CropWebProcessor.cs index eda49fa9d013..0107ae7d204d 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/ImageProcessors/CropWebProcessor.cs +++ b/src/Umbraco.Cms.Imaging.ImageSharp/ImageProcessors/CropWebProcessor.cs @@ -1,10 +1,7 @@ using System.Globalization; using System.Numerics; using Microsoft.Extensions.Logging; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Web; using SixLabors.ImageSharp.Web.Commands; using SixLabors.ImageSharp.Web.Processors; @@ -48,7 +45,8 @@ public bool RequiresTrueColorPixelFormat(CommandCollection commands, CommandPars private static Rectangle? GetCropRectangle(FormattedImage image, CommandCollection commands, CommandParser parser, CultureInfo culture) { var coordinates = parser.ParseValue(commands.GetValueOrDefault(Coordinates), culture); - if (coordinates.Length != 4 || + if (coordinates is null || + coordinates.Length != 4 || (coordinates[0] == 0 && coordinates[1] == 0 && coordinates[2] == 0 && coordinates[3] == 0)) { return null; @@ -64,7 +62,7 @@ public bool RequiresTrueColorPixelFormat(CommandCollection commands, CommandPars Vector2 xy2 = ExifOrientationUtilities.Transform(new Vector2(right, bottom), Vector2.Zero, Vector2.One, orientation); // Scale points to a pixel based rectangle - Size size = image.Image.Size(); + Size size = image.Image.Size; return Rectangle.Round(RectangleF.FromLTRB( MathF.Min(xy1.X, xy2.X) * size.Width, diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/ImageSharpComposer.cs b/src/Umbraco.Cms.Imaging.ImageSharp/ImageSharpComposer.cs index 9a77bc28b294..357a12556234 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/ImageSharpComposer.cs +++ b/src/Umbraco.Cms.Imaging.ImageSharp/ImageSharpComposer.cs @@ -1,6 +1,5 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Extensions; namespace Umbraco.Cms.Imaging.ImageSharp; diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpDimensionExtractor.cs b/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpDimensionExtractor.cs index 409b6e272663..0ec90bb358cc 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpDimensionExtractor.cs +++ b/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpDimensionExtractor.cs @@ -1,4 +1,4 @@ -using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using Umbraco.Cms.Core.Media; using Size = System.Drawing.Size; @@ -28,31 +28,35 @@ public ImageSharpDimensionExtractor(Configuration configuration) { Size? size = null; - IImageInfo imageInfo = Image.Identify(_configuration, stream); - if (imageInfo != null) + if (stream is not null) { - size = IsExifOrientationRotated(imageInfo) - ? new Size(imageInfo.Height, imageInfo.Width) - : new Size(imageInfo.Width, imageInfo.Height); + DecoderOptions options = new() + { + Configuration = _configuration, + }; + + ImageInfo imageInfo = Image.Identify(options, stream); + if (imageInfo != null) + { + size = IsExifOrientationRotated(imageInfo) + ? new Size(imageInfo.Height, imageInfo.Width) + : new Size(imageInfo.Width, imageInfo.Height); + } } return size; } - private static bool IsExifOrientationRotated(IImageInfo imageInfo) + private static bool IsExifOrientationRotated(ImageInfo imageInfo) => GetExifOrientation(imageInfo) switch { - ExifOrientationMode.LeftTop - or ExifOrientationMode.RightTop - or ExifOrientationMode.RightBottom - or ExifOrientationMode.LeftBottom => true, + ExifOrientationMode.LeftTop or ExifOrientationMode.RightTop or ExifOrientationMode.RightBottom or ExifOrientationMode.LeftBottom => true, _ => false, }; - private static ushort GetExifOrientation(IImageInfo imageInfo) + private static ushort GetExifOrientation(ImageInfo imageInfo) { - IExifValue? orientation = imageInfo.Metadata.ExifProfile?.GetValue(ExifTag.Orientation); - if (orientation is not null) + if (imageInfo.Metadata.ExifProfile?.TryGetValue(ExifTag.Orientation, out IExifValue? orientation) == true) { if (orientation.DataType == ExifDataType.Short) { diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpImageUrlGenerator.cs index ad766031871e..d0f3b0d55a56 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpImageUrlGenerator.cs +++ b/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpImageUrlGenerator.cs @@ -1,7 +1,6 @@ using System.Globalization; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Primitives; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.Web.Processors; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj b/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj index 0eb87fbda776..e4eb1cd938a0 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj +++ b/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj @@ -2,13 +2,12 @@ Umbraco CMS - Imaging - ImageSharp Adds imaging support using ImageSharp/ImageSharp.Web to Umbraco CMS. - - false + true - - + + diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Imaging.ImageSharp/UmbracoBuilderExtensions.cs index 4bd50034abd9..7af10cdefa21 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Imaging.ImageSharp/UmbracoBuilderExtensions.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.Web.Caching; using SixLabors.ImageSharp.Web.DependencyInjection; using SixLabors.ImageSharp.Web.Middleware;