diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 97fb91b0ecbf..7e3e1a27006d 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -31,21 +31,19 @@ public class GlobalSettings internal const bool StaticSanitizeTinyMce = false; /// - /// Gets or sets a value for the reserved URLs. - /// It must end with a comma + /// Gets or sets a value for the reserved URLs (must end with a comma). /// [DefaultValue(StaticReservedUrls)] public string ReservedUrls { get; set; } = StaticReservedUrls; /// - /// Gets or sets a value for the reserved paths. - /// It must end with a comma + /// Gets or sets a value for the reserved paths (must end with a comma). /// [DefaultValue(StaticReservedPaths)] public string ReservedPaths { get; set; } = StaticReservedPaths; /// - /// Gets or sets a value for the timeout + /// Gets or sets a value for the back-office login timeout. /// [DefaultValue(StaticTimeOut)] public TimeSpan TimeOut { get; set; } = TimeSpan.Parse(StaticTimeOut); @@ -104,11 +102,19 @@ public class GlobalSettings public string UmbracoScriptsPath { get; set; } = StaticUmbracoScriptsPath; /// - /// Gets or sets a value for the Umbraco media path. + /// Gets or sets a value for the Umbraco media request path. /// [DefaultValue(StaticUmbracoMediaPath)] public string UmbracoMediaPath { get; set; } = StaticUmbracoMediaPath; + /// + /// Gets or sets a value for the physical Umbraco media root path (falls back to when empty). + /// + /// + /// If the value is a virtual path, it's resolved relative to the webroot. + /// + public string UmbracoMediaPhysicalRootPath { get; set; } + /// /// Gets or sets a value indicating whether to install the database when it is missing. /// @@ -131,6 +137,9 @@ public class GlobalSettings /// public string MainDomLock { get; set; } = string.Empty; + /// + /// Gets or sets the telemetry ID. + /// public string Id { get; set; } = string.Empty; /// @@ -164,19 +173,19 @@ public class GlobalSettings /// public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation); - /// Gets a value indicating whether TinyMCE scripting sanitization should be applied + /// + /// Gets a value indicating whether TinyMCE scripting sanitization should be applied. /// [DefaultValue(StaticSanitizeTinyMce)] public bool SanitizeTinyMce => StaticSanitizeTinyMce; /// - /// An int value representing the time in milliseconds to lock the database for a write operation + /// Gets a value representing the time in milliseconds to lock the database for a write operation. /// /// - /// The default value is 5000 milliseconds + /// The default value is 5000 milliseconds. /// - /// The timeout in milliseconds. [DefaultValue(StaticSqlWriteLockTimeOut)] public TimeSpan SqlWriteLockTimeOut { get; } = TimeSpan.Parse(StaticSqlWriteLockTimeOut); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Constants-SystemDirectories.cs b/src/Umbraco.Core/Constants-SystemDirectories.cs index 80b49781ecf9..bf34aab989c4 100644 --- a/src/Umbraco.Core/Constants-SystemDirectories.cs +++ b/src/Umbraco.Core/Constants-SystemDirectories.cs @@ -43,6 +43,8 @@ public static class SystemDirectories public const string AppPlugins = "/App_Plugins"; public static string AppPluginIcons => "/Backoffice/Icons"; + public const string CreatedPackages = "/created-packages"; + public const string MvcViews = "~/Views"; diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 6ef87464e851..ce2e4f230404 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -13,23 +13,25 @@ namespace Umbraco.Cms.Core.DependencyInjection public static partial class UmbracoBuilderExtensions { - private static IUmbracoBuilder AddUmbracoOptions(this IUmbracoBuilder builder) + private static IUmbracoBuilder AddUmbracoOptions(this IUmbracoBuilder builder, Action> configure = null) where TOptions : class { var umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute(); - if (umbracoOptionsAttribute is null) { - throw new ArgumentException("typeof(TOptions) do not have the UmbracoOptionsAttribute"); + throw new ArgumentException($"{typeof(TOptions)} do not have the UmbracoOptionsAttribute."); } - - builder.Services.AddOptions() - .Bind(builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey), - o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties) + var optionsBuilder = builder.Services.AddOptions() + .Bind( + builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey), + o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties + ) .ValidateDataAnnotations(); - return builder; + configure?.Invoke(optionsBuilder); + + return builder; } /// @@ -52,7 +54,13 @@ public static IUmbracoBuilder AddConfiguration(this IUmbracoBuilder builder) .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions() + .AddUmbracoOptions(optionsBuilder => optionsBuilder.PostConfigure(options => + { + if (string.IsNullOrEmpty(options.UmbracoMediaPhysicalRootPath)) + { + options.UmbracoMediaPhysicalRootPath = options.UmbracoMediaPath; + } + })) .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() diff --git a/src/Umbraco.Core/IO/FileSystemExtensions.cs b/src/Umbraco.Core/IO/FileSystemExtensions.cs index 23be195e4b16..c95d37e1c3c2 100644 --- a/src/Umbraco.Core/IO/FileSystemExtensions.cs +++ b/src/Umbraco.Core/IO/FileSystemExtensions.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading; +using Microsoft.Extensions.FileProviders; using Umbraco.Cms.Core.IO; namespace Umbraco.Extensions @@ -87,5 +88,24 @@ public static void CreateFolder(this IFileSystem fs, string folderPath) } fs.DeleteFile(tempFile); } + + /// + /// Creates a new from the file system. + /// + /// The file system. + /// When this method returns, contains an created from the file system. + /// + /// true if the was successfully created; otherwise, false. + /// + public static bool TryCreateFileProvider(this IFileSystem fileSystem, out IFileProvider fileProvider) + { + fileProvider = fileSystem switch + { + IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(), + _ => null + }; + + return fileProvider != null; + } } } diff --git a/src/Umbraco.Core/IO/IFileProviderFactory.cs b/src/Umbraco.Core/IO/IFileProviderFactory.cs new file mode 100644 index 000000000000..742467ccc843 --- /dev/null +++ b/src/Umbraco.Core/IO/IFileProviderFactory.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.FileProviders; + +namespace Umbraco.Cms.Core.IO +{ + /// + /// Factory for creating instances. + /// + public interface IFileProviderFactory + { + /// + /// Creates a new instance. + /// + /// + /// The newly created instance (or null if not supported). + /// + IFileProvider Create(); + } +} diff --git a/src/Umbraco.Core/IO/MediaFileManager.cs b/src/Umbraco.Core/IO/MediaFileManager.cs index 96680d3f845f..c769b9801e41 100644 --- a/src/Umbraco.Core/IO/MediaFileManager.cs +++ b/src/Umbraco.Core/IO/MediaFileManager.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; @@ -22,29 +21,37 @@ public sealed class MediaFileManager private readonly IShortStringHelper _shortStringHelper; private readonly IServiceProvider _serviceProvider; private MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly ContentSettings _contentSettings; - - /// - /// Gets the media filesystem. - /// - public IFileSystem FileSystem { get; } public MediaFileManager( IFileSystem fileSystem, IMediaPathScheme mediaPathScheme, ILogger logger, IShortStringHelper shortStringHelper, - IServiceProvider serviceProvider, - IOptions contentSettings) + IServiceProvider serviceProvider) { _mediaPathScheme = mediaPathScheme; _logger = logger; _shortStringHelper = shortStringHelper; _serviceProvider = serviceProvider; - _contentSettings = contentSettings.Value; FileSystem = fileSystem; } + [Obsolete("Use the ctr that doesn't include unused parameters.")] + public MediaFileManager( + IFileSystem fileSystem, + IMediaPathScheme mediaPathScheme, + ILogger logger, + IShortStringHelper shortStringHelper, + IServiceProvider serviceProvider, + IOptions contentSettings) + : this(fileSystem, mediaPathScheme, logger, shortStringHelper, serviceProvider) + { } + + /// + /// Gets the media filesystem. + /// + public IFileSystem FileSystem { get; } + /// /// Delete media files. /// diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index db10cae4162a..3da09a499c66 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -3,14 +3,17 @@ using System.IO; using System.Linq; using System.Threading; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; namespace Umbraco.Cms.Core.IO { - public interface IPhysicalFileSystem : IFileSystem {} - public class PhysicalFileSystem : IPhysicalFileSystem + public interface IPhysicalFileSystem : IFileSystem + { } + + public class PhysicalFileSystem : IPhysicalFileSystem, IFileProviderFactory { private readonly IIOHelper _ioHelper; private readonly ILogger _logger; @@ -28,7 +31,7 @@ public class PhysicalFileSystem : IPhysicalFileSystem // eg "" or "/Views" or "/Media" or "//Media" in case of a virtual path private readonly string _rootUrl; - public PhysicalFileSystem(IIOHelper ioHelper,IHostingEnvironment hostingEnvironment, ILogger logger, string rootPath, string rootUrl) + public PhysicalFileSystem(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILogger logger, string rootPath, string rootUrl) { _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -270,7 +273,7 @@ public string GetRelativePath(string fullPathOrUrl) return path.Substring(_rootUrl.Length).TrimStart(Constants.CharArrays.ForwardSlash); // unchanged - what else? - return path; + return path.TrimStart(Constants.CharArrays.ForwardSlash); } /// @@ -285,7 +288,7 @@ public string GetRelativePath(string fullPathOrUrl) public string GetFullPath(string path) { // normalize - var opath = path; + var originalPath = path; path = EnsureDirectorySeparatorChar(path); // FIXME: this part should go! @@ -318,7 +321,7 @@ public string GetFullPath(string path) // nothing prevents us to reach the file, security-wise, yet it is outside // this filesystem's root - throw - throw new UnauthorizedAccessException($"File original: [{opath}] full: [{path}] is outside this filesystem's root."); + throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root."); } /// @@ -450,6 +453,9 @@ protected void WithRetry(Action action) } } + /// + public IFileProvider Create() => new PhysicalFileProvider(_rootPath); + #endregion } } diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs index cda61cf7b50c..7bf315a5756f 100644 --- a/src/Umbraco.Core/IO/ShadowWrapper.cs +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -1,14 +1,15 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; namespace Umbraco.Cms.Core.IO { - internal class ShadowWrapper : IFileSystem + internal class ShadowWrapper : IFileSystem, IFileProviderFactory { private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs"; @@ -220,5 +221,8 @@ public void AddFile(string path, string physicalPath, bool overrideIfExists = tr { FileSystem.AddFile(path, physicalPath, overrideIfExists, copy); } + + /// + public IFileProvider Create() => _innerFileSystem.TryCreateFileProvider(out IFileProvider fileProvider) ? fileProvider : null; } } diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index 331034e78758..36b7a5d5d5fe 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -93,7 +93,7 @@ public PackagesRepository( _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles"; _packagesFolderPath = packagesFolderPath ?? Constants.SystemDirectories.Packages; - _mediaFolderPath = mediaFolderPath ?? globalSettings.Value.UmbracoMediaPath + "/created-packages"; + _mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages); _parser = new PackageDefinitionXmlParser(); _mediaService = mediaService; diff --git a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs index a9564712c332..6c45e4d96915 100644 --- a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs +++ b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs @@ -25,7 +25,7 @@ public void Handle(UmbracoApplicationStartingNotification notification) // ensure we have some essential directories // every other component can then initialize safely _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data)); - _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath)); + _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath)); _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews)); _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews)); _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials)); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index f36d7a8ee5c0..ee83d0677ac3 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -17,6 +17,7 @@ + diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs index 6582cfb0c6e6..f66991fb6858 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs @@ -49,7 +49,7 @@ internal static IUmbracoBuilder AddFileSystems(this IUmbracoBuilder builder) ILogger logger = factory.GetRequiredService>(); GlobalSettings globalSettings = factory.GetRequiredService>().Value; - var rootPath = hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPath); + var rootPath = hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath); var rootUrl = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath); return new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, rootPath, rootUrl); }); diff --git a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs index 0ad2271d7e05..1d228ebf9883 100644 --- a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs @@ -44,7 +44,7 @@ public FilePermissionHelper(IOptions globalSettings, IIOHelper i hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath), hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config), hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath), + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Preview) }; _packagesPermissionsDirs = new[] @@ -70,7 +70,7 @@ public bool RunFilePermissionTestSuite(out Dictionary x.Value.Count()) == 0; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs index 0c4f876bb182..be1a31c2c9d2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs @@ -76,7 +76,7 @@ public CreatedPackageSchemaRepository( _macroService = macroService; _contentTypeService = contentTypeService; _xmlParser = new PackageDefinitionXmlParser(); - _mediaFolderPath = mediaFolderPath ?? globalSettings.Value.UmbracoMediaPath + "/created-packages"; + _mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages); _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles"; } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs index 9d30551071e7..58f66a6fb8c5 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs @@ -1,10 +1,16 @@ using System; +using Dazinator.Extensions.FileProviders.PrependBasePath; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; using SixLabors.ImageSharp.Web.DependencyInjection; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Web.Common.ApplicationBuilder { @@ -22,12 +28,14 @@ public UmbracoApplicationBuilder(IApplicationBuilder appBuilder) { AppBuilder = appBuilder ?? throw new ArgumentNullException(nameof(appBuilder)); ApplicationServices = appBuilder.ApplicationServices; - RuntimeState = appBuilder.ApplicationServices.GetRequiredService(); + RuntimeState = appBuilder.ApplicationServices.GetRequiredService(); _umbracoPipelineStartupOptions = ApplicationServices.GetRequiredService>(); } public IServiceProvider ApplicationServices { get; } + public IRuntimeState RuntimeState { get; } + public IApplicationBuilder AppBuilder { get; } /// @@ -78,18 +86,32 @@ public void WithEndpoints(Action configureUmbrac } /// - /// Registers the default required middleware to run Umbraco + /// Registers the default required middleware to run Umbraco. /// - /// public void RegisterDefaultRequiredMiddleware() { UseUmbracoCoreMiddleware(); AppBuilder.UseStatusCodePages(); - // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. + // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. AppBuilder.UseImageSharp(); + + // Get media file provider and request path/URL + var mediaFileManager = AppBuilder.ApplicationServices.GetRequiredService(); + if (mediaFileManager.FileSystem.TryCreateFileProvider(out IFileProvider mediaFileProvider)) + { + GlobalSettings globalSettings = AppBuilder.ApplicationServices.GetRequiredService>().Value; + IHostingEnvironment hostingEnvironment = AppBuilder.ApplicationServices.GetService(); + string mediaRequestPath = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath); + + // Configure custom file provider for media + IWebHostEnvironment webHostEnvironment = AppBuilder.ApplicationServices.GetService(); + webHostEnvironment.WebRootFileProvider = webHostEnvironment.WebRootFileProvider.ConcatComposite(new PrependBasePathFileProvider(mediaRequestPath, mediaFileProvider)); + } + AppBuilder.UseStaticFiles(); + AppBuilder.UseUmbracoPluginsStaticFiles(); // UseRouting adds endpoint routing middleware, this means that middlewares registered after this one diff --git a/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs index 628345dcd67a..f8897e522cd6 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs @@ -7,7 +7,7 @@ namespace Umbraco.Cms.Web.Common.DependencyInjection /// /// Configures the ImageSharp middleware options to use the registered configuration. /// - /// + /// public sealed class ImageSharpConfigurationOptions : IConfigureOptions { /// @@ -22,7 +22,7 @@ public sealed class ImageSharpConfigurationOptions : IConfigureOptions _configuration = configuration; /// - /// Invoked to configure a instance. + /// 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 4d621d348cef..30331fd812fd 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -74,6 +74,7 @@ public static IServiceCollection AddUmbracoImageSharp(this IUmbracoBuilder build .Configure(options => options.CacheFolder = builder.BuilderHostingEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder)) .AddProcessor(); + // Configure middleware to use the registered/shared ImageSharp configuration builder.Services.AddTransient, ImageSharpConfigurationOptions>(); return builder.Services; diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index cc035d116b8d..fc6a2904dfbe 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -1,18 +1,20 @@ using System; using System.IO; +using Dazinator.Extensions.FileProviders.PrependBasePath; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Serilog.Context; using StackExchange.Profiling; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging.Serilog.Enrichers; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Common.Middleware; using Umbraco.Cms.Web.Common.Plugins; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Extensions { @@ -94,7 +96,8 @@ public static IApplicationBuilder UseUmbracoRequestLogging(this IApplicationBuil throw new ArgumentNullException(nameof(app)); } - if (!app.UmbracoCanBoot()) return app; + if (!app.UmbracoCanBoot()) + return app; app.UseMiddleware(); @@ -109,25 +112,21 @@ public static IApplicationBuilder UseUmbracoRequestLogging(this IApplicationBuil public static IApplicationBuilder UseUmbracoPluginsStaticFiles(this IApplicationBuilder app) { var hostingEnvironment = app.ApplicationServices.GetRequiredService(); - var umbracoPluginSettings = app.ApplicationServices.GetRequiredService>(); var pluginFolder = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins); + if (Directory.Exists(pluginFolder)) + { + var umbracoPluginSettings = app.ApplicationServices.GetRequiredService>(); - // Ensure the plugin folder exists - Directory.CreateDirectory(pluginFolder); - - var fileProvider = new UmbracoPluginPhysicalFileProvider( - pluginFolder, - umbracoPluginSettings); + var pluginFileProvider = new UmbracoPluginPhysicalFileProvider( + pluginFolder, + umbracoPluginSettings); - app.UseStaticFiles(new StaticFileOptions - { - FileProvider = fileProvider, - RequestPath = Constants.SystemDirectories.AppPlugins - }); + IWebHostEnvironment webHostEnvironment = app.ApplicationServices.GetService(); + webHostEnvironment.WebRootFileProvider = webHostEnvironment.WebRootFileProvider.ConcatComposite(new PrependBasePathFileProvider(Constants.SystemDirectories.AppPlugins, pluginFileProvider)); + } return app; } } - } diff --git a/src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs b/src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs new file mode 100644 index 000000000000..35a2882bdd5d --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs @@ -0,0 +1,19 @@ +using System.Linq; +using Microsoft.Extensions.FileProviders; + +namespace Umbraco.Extensions +{ + internal static class FileProviderExtensions + { + public static IFileProvider ConcatComposite(this IFileProvider fileProvider, params IFileProvider[] fileProviders) + { + var existingFileProviders = fileProvider switch + { + CompositeFileProvider compositeFileProvider => compositeFileProvider.FileProviders, + _ => new[] { fileProvider } + }; + + return new CompositeFileProvider(existingFileProviders.Concat(fileProviders)); + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs index 02709f7f84c2..4c6204ee4edb 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs @@ -53,7 +53,7 @@ public void PathTests() { var repository = new PartialViewRepository(fileSystems); - var partialView = new PartialView(PartialViewType.PartialView, "test-path-1.cshtml") { Content = "// partialView" }; + IPartialView partialView = new PartialView(PartialViewType.PartialView, "test-path-1.cshtml") { Content = "// partialView" }; repository.Save(partialView); Assert.IsTrue(_fileSystem.FileExists("test-path-1.cshtml")); Assert.AreEqual("test-path-1.cshtml", partialView.Path); @@ -62,10 +62,10 @@ public void PathTests() partialView = new PartialView(PartialViewType.PartialView, "path-2/test-path-2.cshtml") { Content = "// partialView" }; repository.Save(partialView); Assert.IsTrue(_fileSystem.FileExists("path-2/test-path-2.cshtml")); - Assert.AreEqual("path-2\\test-path-2.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); // fixed in 7.3 - 7.2.8 does not update the path + Assert.AreEqual("path-2\\test-path-2.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-2.cshtml", partialView.VirtualPath); - partialView = (PartialView)repository.Get("path-2/test-path-2.cshtml"); + partialView = repository.Get("path-2/test-path-2.cshtml"); Assert.IsNotNull(partialView); Assert.AreEqual("path-2\\test-path-2.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-2.cshtml", partialView.VirtualPath); @@ -76,26 +76,33 @@ public void PathTests() Assert.AreEqual("path-2\\test-path-3.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-3.cshtml", partialView.VirtualPath); - partialView = (PartialView)repository.Get("path-2/test-path-3.cshtml"); + partialView = repository.Get("path-2/test-path-3.cshtml"); Assert.IsNotNull(partialView); Assert.AreEqual("path-2\\test-path-3.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-3.cshtml", partialView.VirtualPath); - partialView = (PartialView)repository.Get("path-2\\test-path-3.cshtml"); + partialView = repository.Get("path-2\\test-path-3.cshtml"); Assert.IsNotNull(partialView); Assert.AreEqual("path-2\\test-path-3.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-3.cshtml", partialView.VirtualPath); - partialView = new PartialView(PartialViewType.PartialView, "\\test-path-4.cshtml") { Content = "// partialView" }; - Assert.Throws(() => // fixed in 7.3 - 7.2.8 used to strip the \ + partialView = new PartialView(PartialViewType.PartialView, "..\\test-path-4.cshtml") { Content = "// partialView" }; + Assert.Throws(() => repository.Save(partialView)); - partialView = (PartialView)repository.Get("missing.cshtml"); + partialView = new PartialView(PartialViewType.PartialView, "\\test-path-5.cshtml") { Content = "// partialView" }; + repository.Save(partialView); + + partialView = repository.Get("\\test-path-5.cshtml"); + Assert.IsNotNull(partialView); + Assert.AreEqual("test-path-5.cshtml", partialView.Path); + Assert.AreEqual("/Views/Partials/test-path-5.cshtml", partialView.VirtualPath); + + partialView = repository.Get("missing.cshtml"); Assert.IsNull(partialView); - // fixed in 7.3 - 7.2.8 used to... - Assert.Throws(() => partialView = (PartialView)repository.Get("\\test-path-4.cshtml")); - Assert.Throws(() => partialView = (PartialView)repository.Get("../../packages.config")); + Assert.Throws(() => partialView = repository.Get("..\\test-path-4.cshtml")); + Assert.Throws(() => partialView = repository.Get("../../packages.config")); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs index 28f9a9eff15e..4721af14e171 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs @@ -303,15 +303,22 @@ public void PathTests() Assert.AreEqual("path-2\\test-path-3.js".Replace("\\", $"{Path.DirectorySeparatorChar}"), script.Path); Assert.AreEqual("/scripts/path-2/test-path-3.js", script.VirtualPath); - script = new Script("\\test-path-4.js") { Content = "// script" }; - Assert.Throws(() => // fixed in 7.3 - 7.2.8 used to strip the \ + script = new Script("..\\test-path-4.js") { Content = "// script" }; + Assert.Throws(() => repository.Save(script)); + script = new Script("\\test-path-5.js") { Content = "// script" }; + repository.Save(script); + + script = repository.Get("\\test-path-5.js"); + Assert.IsNotNull(script); + Assert.AreEqual("test-path-5.js", script.Path); + Assert.AreEqual("/scripts/test-path-5.js", script.VirtualPath); + script = repository.Get("missing.js"); Assert.IsNull(script); - // fixed in 7.3 - 7.2.8 used to... - Assert.Throws(() => script = repository.Get("\\test-path-4.js")); + Assert.Throws(() => script = repository.Get("..\\test-path-4.js")); Assert.Throws(() => script = repository.Get("../packages.config")); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs index d9bde0bca2ec..a32638ed4dba 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs @@ -275,7 +275,7 @@ public void PathTests() repository.Save(stylesheet); Assert.IsTrue(_fileSystem.FileExists("path-2/test-path-2.css")); - Assert.AreEqual("path-2\\test-path-2.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path); // fixed in 7.3 - 7.2.8 does not update the path + Assert.AreEqual("path-2\\test-path-2.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path); Assert.AreEqual("/css/path-2/test-path-2.css", stylesheet.VirtualPath); stylesheet = repository.Get("path-2/test-path-2.css"); @@ -300,17 +300,24 @@ public void PathTests() Assert.AreEqual("path-2\\test-path-3.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path); Assert.AreEqual("/css/path-2/test-path-3.css", stylesheet.VirtualPath); - stylesheet = new Stylesheet("\\test-path-4.css") { Content = "body { color:#000; } .bold {font-weight:bold;}" }; - Assert.Throws(() => // fixed in 7.3 - 7.2.8 used to strip the \ + stylesheet = new Stylesheet("..\\test-path-4.css") { Content = "body { color:#000; } .bold {font-weight:bold;}" }; + Assert.Throws(() => repository.Save(stylesheet)); - // fixed in 7.3 - 7.2.8 used to throw + stylesheet = new Stylesheet("\\test-path-5.css") { Content = "body { color:#000; } .bold {font-weight:bold;}" }; + repository.Save(stylesheet); + + stylesheet = repository.Get("\\test-path-5.css"); + Assert.IsNotNull(stylesheet); + Assert.AreEqual("test-path-5.css", stylesheet.Path); + Assert.AreEqual("/css/test-path-5.css", stylesheet.VirtualPath); + stylesheet = repository.Get("missing.css"); Assert.IsNull(stylesheet); // #7713 changes behaviour to return null when outside the filesystem // to accomodate changing the CSS path and not flooding the backoffice with errors - stylesheet = repository.Get("\\test-path-4.css"); // outside the filesystem, does not exist + stylesheet = repository.Get("..\\test-path-4.css"); // outside the filesystem, does not exist Assert.IsNull(stylesheet); stylesheet = repository.Get("../packages.config"); // outside the filesystem, exists diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeFileSystemsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeFileSystemsTests.cs index e30d7bbf5542..7ea8e65edaea 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeFileSystemsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeFileSystemsTests.cs @@ -48,7 +48,7 @@ private void ClearFiles(IIOHelper ioHelper) [Test] public void MediaFileManager_does_not_write_to_physical_file_system_when_scoped_if_scope_does_not_complete() { - string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPath); + string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPhysicalRootPath); string rootUrl = HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoMediaPath); var physMediaFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, GetRequiredService>(), rootPath, rootUrl); MediaFileManager mediaFileManager = MediaFileManager; @@ -77,7 +77,7 @@ public void MediaFileManager_does_not_write_to_physical_file_system_when_scoped_ [Test] public void MediaFileManager_writes_to_physical_file_system_when_scoped_and_scope_is_completed() { - string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPath); + string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPhysicalRootPath); string rootUrl = HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoMediaPath); var physMediaFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, GetRequiredService>(), rootPath, rootUrl); MediaFileManager mediaFileManager = MediaFileManager; @@ -108,7 +108,7 @@ public void MediaFileManager_writes_to_physical_file_system_when_scoped_and_scop [Test] public void MultiThread() { - string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPath); + string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPhysicalRootPath); string rootUrl = HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoMediaPath); var physMediaFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, GetRequiredService>(), rootPath, rootUrl); MediaFileManager mediaFileManager = MediaFileManager; diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs index 8dee7db12d67..ba89c9775efa 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs @@ -134,9 +134,9 @@ public static MapperConfigurationStore CreateMaps() /// public static string MapPathForTestFiles(string relativePath) => s_testHelperInternal.MapPathForTestFiles(relativePath); - public static void InitializeContentDirectories() => CreateDirectories(new[] { Constants.SystemDirectories.MvcViews, new GlobalSettings().UmbracoMediaPath, Constants.SystemDirectories.AppPlugins }); + public static void InitializeContentDirectories() => CreateDirectories(new[] { Constants.SystemDirectories.MvcViews, new GlobalSettings().UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.AppPlugins }); - public static void CleanContentDirectories() => CleanDirectories(new[] { Constants.SystemDirectories.MvcViews, new GlobalSettings().UmbracoMediaPath }); + public static void CleanContentDirectories() => CleanDirectories(new[] { Constants.SystemDirectories.MvcViews, new GlobalSettings().UmbracoMediaPhysicalRootPath }); public static void CreateDirectories(string[] directories) {