Serve Media and App_Plugins using WebRootFileProvider (and allow changing the physical media path) (#11783)

* Allow changing UmbracoMediaPath to an absolute path. Also ensure Imagesharp are handing requests outside of the wwwroot folder.

* Let UmbracoMediaUrl fallback to UmbracoMediaPath when empty

* Add FileSystemFileProvider to expose an IFileSystem as IFileProvider

* Replace IUmbracoMediaFileProvider with IFileProviderFactory implementation

* Fix issue resolving relative paths when media URL has changed

* Remove FileSystemFileProvider and require explicitly implementing IFileProviderFactory

* Update tests (UnauthorizedAccessException isn't thrown anymore for rooted files)

* Update test to use UmbracoMediaUrl

* Add UmbracoMediaPhysicalRootPath global setting

* Remove MediaFileManagerImageProvider and use composited file providers

* Move CreateFileProvider to IFileSystem extension method

* Add rooted path tests

Co-authored-by: Ronald Barendse <ronald@barend.se>
This commit is contained in:
Bjarke Berg
2022-01-06 13:35:24 +01:00
committed by GitHub
parent 84fea8f953
commit 642c216f94
24 changed files with 233 additions and 96 deletions

View File

@@ -31,21 +31,19 @@ namespace Umbraco.Cms.Core.Configuration.Models
internal const bool StaticSanitizeTinyMce = false;
/// <summary>
/// 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).
/// </summary>
[DefaultValue(StaticReservedUrls)]
public string ReservedUrls { get; set; } = StaticReservedUrls;
/// <summary>
/// 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).
/// </summary>
[DefaultValue(StaticReservedPaths)]
public string ReservedPaths { get; set; } = StaticReservedPaths;
/// <summary>
/// Gets or sets a value for the timeout
/// Gets or sets a value for the back-office login timeout.
/// </summary>
[DefaultValue(StaticTimeOut)]
public TimeSpan TimeOut { get; set; } = TimeSpan.Parse(StaticTimeOut);
@@ -104,11 +102,19 @@ namespace Umbraco.Cms.Core.Configuration.Models
public string UmbracoScriptsPath { get; set; } = StaticUmbracoScriptsPath;
/// <summary>
/// Gets or sets a value for the Umbraco media path.
/// Gets or sets a value for the Umbraco media request path.
/// </summary>
[DefaultValue(StaticUmbracoMediaPath)]
public string UmbracoMediaPath { get; set; } = StaticUmbracoMediaPath;
/// <summary>
/// Gets or sets a value for the physical Umbraco media root path (falls back to <see cref="UmbracoMediaPath" /> when empty).
/// </summary>
/// <remarks>
/// If the value is a virtual path, it's resolved relative to the webroot.
/// </remarks>
public string UmbracoMediaPhysicalRootPath { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to install the database when it is missing.
/// </summary>
@@ -131,6 +137,9 @@ namespace Umbraco.Cms.Core.Configuration.Models
/// </summary>
public string MainDomLock { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the telemetry ID.
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
@@ -164,19 +173,19 @@ namespace Umbraco.Cms.Core.Configuration.Models
/// </summary>
public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation);
/// Gets a value indicating whether TinyMCE scripting sanitization should be applied
/// <summary>
/// Gets a value indicating whether TinyMCE scripting sanitization should be applied.
/// </summary>
[DefaultValue(StaticSanitizeTinyMce)]
public bool SanitizeTinyMce => StaticSanitizeTinyMce;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// The default value is 5000 milliseconds
/// The default value is 5000 milliseconds.
/// </remarks>
/// <value>The timeout in milliseconds.</value>
[DefaultValue(StaticSqlWriteLockTimeOut)]
public TimeSpan SqlWriteLockTimeOut { get; } = TimeSpan.Parse(StaticSqlWriteLockTimeOut);
}
}
}

View File

@@ -43,6 +43,8 @@ namespace Umbraco.Cms.Core
public const string AppPlugins = "/App_Plugins";
public static string AppPluginIcons => "/Backoffice/Icons";
public const string CreatedPackages = "/created-packages";
public const string MvcViews = "~/Views";

View File

@@ -13,23 +13,25 @@ namespace Umbraco.Cms.Core.DependencyInjection
public static partial class UmbracoBuilderExtensions
{
private static IUmbracoBuilder AddUmbracoOptions<TOptions>(this IUmbracoBuilder builder)
private static IUmbracoBuilder AddUmbracoOptions<TOptions>(this IUmbracoBuilder builder, Action<OptionsBuilder<TOptions>> configure = null)
where TOptions : class
{
var umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute<UmbracoOptionsAttribute>();
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<TOptions>()
.Bind(builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey),
o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties)
var optionsBuilder = builder.Services.AddOptions<TOptions>()
.Bind(
builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey),
o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties
)
.ValidateDataAnnotations();
return builder;
configure?.Invoke(optionsBuilder);
return builder;
}
/// <summary>
@@ -52,7 +54,13 @@ namespace Umbraco.Cms.Core.DependencyInjection
.AddUmbracoOptions<ContentSettings>()
.AddUmbracoOptions<CoreDebugSettings>()
.AddUmbracoOptions<ExceptionFilterSettings>()
.AddUmbracoOptions<GlobalSettings>()
.AddUmbracoOptions<GlobalSettings>(optionsBuilder => optionsBuilder.PostConfigure(options =>
{
if (string.IsNullOrEmpty(options.UmbracoMediaPhysicalRootPath))
{
options.UmbracoMediaPhysicalRootPath = options.UmbracoMediaPath;
}
}))
.AddUmbracoOptions<HealthChecksSettings>()
.AddUmbracoOptions<HostingSettings>()
.AddUmbracoOptions<ImagingSettings>()

View File

@@ -3,6 +3,7 @@ using System.IO;
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 @@ namespace Umbraco.Extensions
}
fs.DeleteFile(tempFile);
}
/// <summary>
/// Creates a new <see cref="IFileProvider" /> from the file system.
/// </summary>
/// <param name="fileSystem">The file system.</param>
/// <param name="fileProvider">When this method returns, contains an <see cref="IFileProvider"/> created from the file system.</param>
/// <returns>
/// <c>true</c> if the <see cref="IFileProvider" /> was successfully created; otherwise, <c>false</c>.
/// </returns>
public static bool TryCreateFileProvider(this IFileSystem fileSystem, out IFileProvider fileProvider)
{
fileProvider = fileSystem switch
{
IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(),
_ => null
};
return fileProvider != null;
}
}
}

View File

@@ -0,0 +1,18 @@
using Microsoft.Extensions.FileProviders;
namespace Umbraco.Cms.Core.IO
{
/// <summary>
/// Factory for creating <see cref="IFileProvider" /> instances.
/// </summary>
public interface IFileProviderFactory
{
/// <summary>
/// Creates a new <see cref="IFileProvider" /> instance.
/// </summary>
/// <returns>
/// The newly created <see cref="IFileProvider" /> instance (or <c>null</c> if not supported).
/// </returns>
IFileProvider Create();
}
}

View File

@@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging;
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,13 +21,22 @@ namespace Umbraco.Cms.Core.IO
private readonly IShortStringHelper _shortStringHelper;
private readonly IServiceProvider _serviceProvider;
private MediaUrlGeneratorCollection _mediaUrlGenerators;
private readonly ContentSettings _contentSettings;
/// <summary>
/// Gets the media filesystem.
/// </summary>
public IFileSystem FileSystem { get; }
public MediaFileManager(
IFileSystem fileSystem,
IMediaPathScheme mediaPathScheme,
ILogger<MediaFileManager> logger,
IShortStringHelper shortStringHelper,
IServiceProvider serviceProvider)
{
_mediaPathScheme = mediaPathScheme;
_logger = logger;
_shortStringHelper = shortStringHelper;
_serviceProvider = serviceProvider;
FileSystem = fileSystem;
}
[Obsolete("Use the ctr that doesn't include unused parameters.")]
public MediaFileManager(
IFileSystem fileSystem,
IMediaPathScheme mediaPathScheme,
@@ -36,14 +44,13 @@ namespace Umbraco.Cms.Core.IO
IShortStringHelper shortStringHelper,
IServiceProvider serviceProvider,
IOptions<ContentSettings> contentSettings)
{
_mediaPathScheme = mediaPathScheme;
_logger = logger;
_shortStringHelper = shortStringHelper;
_serviceProvider = serviceProvider;
_contentSettings = contentSettings.Value;
FileSystem = fileSystem;
}
: this(fileSystem, mediaPathScheme, logger, shortStringHelper, serviceProvider)
{ }
/// <summary>
/// Gets the media filesystem.
/// </summary>
public IFileSystem FileSystem { get; }
/// <summary>
/// Delete media files.

View File

@@ -3,14 +3,17 @@ using System.Collections.Generic;
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<PhysicalFileSystem> _logger;
@@ -28,7 +31,7 @@ namespace Umbraco.Cms.Core.IO
// eg "" or "/Views" or "/Media" or "/<vpath>/Media" in case of a virtual path
private readonly string _rootUrl;
public PhysicalFileSystem(IIOHelper ioHelper,IHostingEnvironment hostingEnvironment, ILogger<PhysicalFileSystem> logger, string rootPath, string rootUrl)
public PhysicalFileSystem(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILogger<PhysicalFileSystem> logger, string rootPath, string rootUrl)
{
_ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -270,7 +273,7 @@ namespace Umbraco.Cms.Core.IO
return path.Substring(_rootUrl.Length).TrimStart(Constants.CharArrays.ForwardSlash);
// unchanged - what else?
return path;
return path.TrimStart(Constants.CharArrays.ForwardSlash);
}
/// <summary>
@@ -285,7 +288,7 @@ namespace Umbraco.Cms.Core.IO
public string GetFullPath(string path)
{
// normalize
var opath = path;
var originalPath = path;
path = EnsureDirectorySeparatorChar(path);
// FIXME: this part should go!
@@ -318,7 +321,7 @@ namespace Umbraco.Cms.Core.IO
// 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.");
}
/// <summary>
@@ -450,6 +453,9 @@ namespace Umbraco.Cms.Core.IO
}
}
/// <inheritdoc />
public IFileProvider Create() => new PhysicalFileProvider(_rootPath);
#endregion
}
}

View File

@@ -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 @@ namespace Umbraco.Cms.Core.IO
{
FileSystem.AddFile(path, physicalPath, overrideIfExists, copy);
}
/// <inheritdoc />
public IFileProvider Create() => _innerFileSystem.TryCreateFileProvider(out IFileProvider fileProvider) ? fileProvider : null;
}
}

View File

@@ -93,7 +93,7 @@ namespace Umbraco.Cms.Core.Packaging
_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;

View File

@@ -25,7 +25,7 @@ namespace Umbraco.Cms.Core.Runtime
// 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));

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
@@ -17,6 +17,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="5.0.11" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />

View File

@@ -49,7 +49,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
ILogger<PhysicalFileSystem> logger = factory.GetRequiredService<ILogger<PhysicalFileSystem>>();
GlobalSettings globalSettings = factory.GetRequiredService<IOptions<GlobalSettings>>().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);
});

View File

@@ -44,7 +44,7 @@ namespace Umbraco.Cms.Infrastructure.Install
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 @@ namespace Umbraco.Cms.Infrastructure.Install
EnsureFiles(_permissionFiles, out errors);
report[FilePermissionTest.FileWriting] = errors.ToList();
EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath), out errors);
EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), out errors);
report[FilePermissionTest.MediaFolderCreation] = errors.ToList();
return report.Sum(x => x.Value.Count()) == 0;

View File

@@ -76,7 +76,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
_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";
}

View File

@@ -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 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder
{
AppBuilder = appBuilder ?? throw new ArgumentNullException(nameof(appBuilder));
ApplicationServices = appBuilder.ApplicationServices;
RuntimeState = appBuilder.ApplicationServices.GetRequiredService<IRuntimeState>();
RuntimeState = appBuilder.ApplicationServices.GetRequiredService<IRuntimeState>();
_umbracoPipelineStartupOptions = ApplicationServices.GetRequiredService<IOptions<UmbracoPipelineOptions>>();
}
public IServiceProvider ApplicationServices { get; }
public IRuntimeState RuntimeState { get; }
public IApplicationBuilder AppBuilder { get; }
/// <inheritdoc />
@@ -78,18 +86,32 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder
}
/// <summary>
/// Registers the default required middleware to run Umbraco
/// Registers the default required middleware to run Umbraco.
/// </summary>
/// <param name="umbracoApplicationBuilderContext"></param>
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<MediaFileManager>();
if (mediaFileManager.FileSystem.TryCreateFileProvider(out IFileProvider mediaFileProvider))
{
GlobalSettings globalSettings = AppBuilder.ApplicationServices.GetRequiredService<IOptions<GlobalSettings>>().Value;
IHostingEnvironment hostingEnvironment = AppBuilder.ApplicationServices.GetService<IHostingEnvironment>();
string mediaRequestPath = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath);
// Configure custom file provider for media
IWebHostEnvironment webHostEnvironment = AppBuilder.ApplicationServices.GetService<IWebHostEnvironment>();
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

View File

@@ -7,7 +7,7 @@ namespace Umbraco.Cms.Web.Common.DependencyInjection
/// <summary>
/// Configures the ImageSharp middleware options to use the registered configuration.
/// </summary>
/// <seealso cref="Microsoft.Extensions.Options.IConfigureOptions&lt;SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions&gt;" />
/// <seealso cref="IConfigureOptions{ImageSharpMiddlewareOptions}" />
public sealed class ImageSharpConfigurationOptions : IConfigureOptions<ImageSharpMiddlewareOptions>
{
/// <summary>
@@ -22,7 +22,7 @@ namespace Umbraco.Cms.Web.Common.DependencyInjection
public ImageSharpConfigurationOptions(Configuration configuration) => _configuration = configuration;
/// <summary>
/// Invoked to configure a <typeparamref name="TOptions" /> instance.
/// Invoked to configure an <see cref="ImageSharpMiddlewareOptions" /> instance.
/// </summary>
/// <param name="options">The options instance to configure.</param>
public void Configure(ImageSharpMiddlewareOptions options) => options.Configuration = _configuration;

View File

@@ -74,6 +74,7 @@ namespace Umbraco.Extensions
.Configure<PhysicalFileSystemCacheOptions>(options => options.CacheFolder = builder.BuilderHostingEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder))
.AddProcessor<CropWebProcessor>();
// Configure middleware to use the registered/shared ImageSharp configuration
builder.Services.AddTransient<IConfigureOptions<ImageSharpMiddlewareOptions>, ImageSharpConfigurationOptions>();
return builder.Services;

View File

@@ -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 @@ namespace Umbraco.Extensions
throw new ArgumentNullException(nameof(app));
}
if (!app.UmbracoCanBoot()) return app;
if (!app.UmbracoCanBoot())
return app;
app.UseMiddleware<UmbracoRequestLoggingMiddleware>();
@@ -109,25 +112,21 @@ namespace Umbraco.Extensions
public static IApplicationBuilder UseUmbracoPluginsStaticFiles(this IApplicationBuilder app)
{
var hostingEnvironment = app.ApplicationServices.GetRequiredService<IHostingEnvironment>();
var umbracoPluginSettings = app.ApplicationServices.GetRequiredService<IOptions<UmbracoPluginSettings>>();
var pluginFolder = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins);
// Ensure the plugin folder exists
Directory.CreateDirectory(pluginFolder);
var fileProvider = new UmbracoPluginPhysicalFileProvider(
pluginFolder,
umbracoPluginSettings);
app.UseStaticFiles(new StaticFileOptions
if (Directory.Exists(pluginFolder))
{
FileProvider = fileProvider,
RequestPath = Constants.SystemDirectories.AppPlugins
});
var umbracoPluginSettings = app.ApplicationServices.GetRequiredService<IOptions<UmbracoPluginSettings>>();
var pluginFileProvider = new UmbracoPluginPhysicalFileProvider(
pluginFolder,
umbracoPluginSettings);
IWebHostEnvironment webHostEnvironment = app.ApplicationServices.GetService<IWebHostEnvironment>();
webHostEnvironment.WebRootFileProvider = webHostEnvironment.WebRootFileProvider.ConcatComposite(new PrependBasePathFileProvider(Constants.SystemDirectories.AppPlugins, pluginFileProvider));
}
return app;
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -53,7 +53,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos
{
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 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos
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 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos
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<UnauthorizedAccessException>(() => // fixed in 7.3 - 7.2.8 used to strip the \
partialView = new PartialView(PartialViewType.PartialView, "..\\test-path-4.cshtml") { Content = "// partialView" };
Assert.Throws<UnauthorizedAccessException>(() =>
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<UnauthorizedAccessException>(() => partialView = (PartialView)repository.Get("\\test-path-4.cshtml"));
Assert.Throws<UnauthorizedAccessException>(() => partialView = (PartialView)repository.Get("../../packages.config"));
Assert.Throws<UnauthorizedAccessException>(() => partialView = repository.Get("..\\test-path-4.cshtml"));
Assert.Throws<UnauthorizedAccessException>(() => partialView = repository.Get("../../packages.config"));
}
}

View File

@@ -303,15 +303,22 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos
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<UnauthorizedAccessException>(() => // fixed in 7.3 - 7.2.8 used to strip the \
script = new Script("..\\test-path-4.js") { Content = "// script" };
Assert.Throws<UnauthorizedAccessException>(() =>
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<UnauthorizedAccessException>(() => script = repository.Get("\\test-path-4.js"));
Assert.Throws<UnauthorizedAccessException>(() => script = repository.Get("..\\test-path-4.js"));
Assert.Throws<UnauthorizedAccessException>(() => script = repository.Get("../packages.config"));
}
}

View File

@@ -275,7 +275,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos
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 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos
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<UnauthorizedAccessException>(() => // 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<UnauthorizedAccessException>(() =>
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

View File

@@ -48,7 +48,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping
[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<ILogger<PhysicalFileSystem>>(), rootPath, rootUrl);
MediaFileManager mediaFileManager = MediaFileManager;
@@ -77,7 +77,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping
[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<ILogger<PhysicalFileSystem>>(), rootPath, rootUrl);
MediaFileManager mediaFileManager = MediaFileManager;
@@ -108,7 +108,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping
[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<ILogger<PhysicalFileSystem>>(), rootPath, rootUrl);
MediaFileManager mediaFileManager = MediaFileManager;

View File

@@ -134,9 +134,9 @@ namespace Umbraco.Cms.Tests.UnitTests.TestHelpers
/// </summary>
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)
{