From e9883ea063e7dbd41ff311c9e16e8406f275ae33 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 13 Sep 2022 09:11:46 +0200 Subject: [PATCH] Add custom PrependBasePathFileProvider to handle media files with special characters (#12936) --- .../UmbracoApplicationBuilder.cs | 4 +- .../Media/MediaPrependBasePathFileProvider.cs | 94 +++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Web.Common/Media/MediaPrependBasePathFileProvider.cs diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs index 8bf36264eb..2212dec425 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs @@ -1,4 +1,3 @@ -using Dazinator.Extensions.FileProviders.PrependBasePath; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; @@ -8,6 +7,7 @@ using SixLabors.ImageSharp.Web.DependencyInjection; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Media; using Umbraco.Extensions; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; @@ -96,7 +96,7 @@ public class UmbracoApplicationBuilder : IUmbracoApplicationBuilder, IUmbracoEnd { webHostEnvironment.WebRootFileProvider = webHostEnvironment.WebRootFileProvider.ConcatComposite( - new PrependBasePathFileProvider(mediaRequestPath, mediaFileProvider)); + new MediaPrependBasePathFileProvider(mediaRequestPath, mediaFileProvider)); } } diff --git a/src/Umbraco.Web.Common/Media/MediaPrependBasePathFileProvider.cs b/src/Umbraco.Web.Common/Media/MediaPrependBasePathFileProvider.cs new file mode 100644 index 0000000000..c6ce59456d --- /dev/null +++ b/src/Umbraco.Web.Common/Media/MediaPrependBasePathFileProvider.cs @@ -0,0 +1,94 @@ +using Dazinator.Extensions.FileProviders; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; + +namespace Umbraco.Cms.Web.Common.Media; + +/// +/// Prepends a base path to files / directories from an underlying file provider. +/// +/// +/// This is a clone-and-own of PrependBasePathFileProvider from the Dazinator project, cleaned up and tweaked to work +/// for serving media files with special characters. +/// Reference issue: https://github.com/umbraco/Umbraco-CMS/issues/12903 +/// A PR has been submitted to the Dazinator project: https://github.com/dazinator/Dazinator.Extensions.FileProviders/pull/53 +/// If that PR is accepted, the Dazinator dependency should be updated and this class should be removed. +/// +internal class MediaPrependBasePathFileProvider : IFileProvider +{ + private readonly PathString _basePath; + private readonly IFileProvider _underlyingFileProvider; + private readonly IFileInfo _baseDirectoryFileInfo; + private static readonly char[] _splitChar = { '/' }; + + public MediaPrependBasePathFileProvider(string? basePath, IFileProvider underlyingFileProvider) + { + _basePath = new PathString(basePath); + _baseDirectoryFileInfo = new DirectoryFileInfo(_basePath.ToString().TrimStart(_splitChar)); + _underlyingFileProvider = underlyingFileProvider; + } + + protected virtual bool TryMapSubPath(string originalSubPath, out PathString newSubPath) + { + if (!string.IsNullOrEmpty(originalSubPath)) + { + PathString originalPathString; + originalPathString = originalSubPath[0] != '/' ? new PathString('/' + originalSubPath) : new PathString(originalSubPath); + + if (originalPathString.HasValue && originalPathString.StartsWithSegments(_basePath, out PathString remaining)) + { + // var childPath = originalPathString.Remove(0, _basePath.Value.Length); + newSubPath = remaining; + return true; + } + } + + newSubPath = null; + return false; + } + + public IDirectoryContents GetDirectoryContents(string subpath) + { + if (string.IsNullOrEmpty(subpath)) + { + // return root / base directory. + return new EnumerableDirectoryContents(_baseDirectoryFileInfo); + } + + if (TryMapSubPath(subpath, out PathString newPath)) + { + IDirectoryContents? contents = _underlyingFileProvider.GetDirectoryContents(newPath); + return contents; + } + + return new NotFoundDirectoryContents(); + } + + public IFileInfo GetFileInfo(string subpath) + { + if (TryMapSubPath(subpath, out PathString newPath)) + { + // KJA changed: use explicit newPath.Value instead of implicit newPath string operator (which calls ToString()) + IFileInfo? result = _underlyingFileProvider.GetFileInfo(newPath.Value); + return result; + } + + return new NotFoundFileInfo(subpath); + } + + public IChangeToken Watch(string filter) + { + // We check if the pattern starts with the base path, and remove it if necessary. + // otherwise we just pass the pattern through unaltered. + if (TryMapSubPath(filter, out PathString newPath)) + { + // KJA changed: use explicit newPath.Value instead of implicit newPath string operator (which calls ToString()) + IChangeToken? result = _underlyingFileProvider.Watch(newPath.Value); + return result; + } + + return _underlyingFileProvider.Watch(newPath); + } +} +