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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
src/Umbraco.Core/IO/IFileProviderFactory.cs
Normal file
18
src/Umbraco.Core/IO/IFileProviderFactory.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddlewareOptions>" />
|
||||
/// <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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
19
src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs
Normal file
19
src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user