Skip to content

Commit 642c216

Browse files
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 <[email protected]>
1 parent 84fea8f commit 642c216

File tree

24 files changed

+228
-91
lines changed

24 files changed

+228
-91
lines changed

src/Umbraco.Core/Configuration/Models/GlobalSettings.cs

+20-11
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,19 @@ public class GlobalSettings
3131
internal const bool StaticSanitizeTinyMce = false;
3232

3333
/// <summary>
34-
/// Gets or sets a value for the reserved URLs.
35-
/// It must end with a comma
34+
/// Gets or sets a value for the reserved URLs (must end with a comma).
3635
/// </summary>
3736
[DefaultValue(StaticReservedUrls)]
3837
public string ReservedUrls { get; set; } = StaticReservedUrls;
3938

4039
/// <summary>
41-
/// Gets or sets a value for the reserved paths.
42-
/// It must end with a comma
40+
/// Gets or sets a value for the reserved paths (must end with a comma).
4341
/// </summary>
4442
[DefaultValue(StaticReservedPaths)]
4543
public string ReservedPaths { get; set; } = StaticReservedPaths;
4644

4745
/// <summary>
48-
/// Gets or sets a value for the timeout
46+
/// Gets or sets a value for the back-office login timeout.
4947
/// </summary>
5048
[DefaultValue(StaticTimeOut)]
5149
public TimeSpan TimeOut { get; set; } = TimeSpan.Parse(StaticTimeOut);
@@ -104,11 +102,19 @@ public class GlobalSettings
104102
public string UmbracoScriptsPath { get; set; } = StaticUmbracoScriptsPath;
105103

106104
/// <summary>
107-
/// Gets or sets a value for the Umbraco media path.
105+
/// Gets or sets a value for the Umbraco media request path.
108106
/// </summary>
109107
[DefaultValue(StaticUmbracoMediaPath)]
110108
public string UmbracoMediaPath { get; set; } = StaticUmbracoMediaPath;
111109

110+
/// <summary>
111+
/// Gets or sets a value for the physical Umbraco media root path (falls back to <see cref="UmbracoMediaPath" /> when empty).
112+
/// </summary>
113+
/// <remarks>
114+
/// If the value is a virtual path, it's resolved relative to the webroot.
115+
/// </remarks>
116+
public string UmbracoMediaPhysicalRootPath { get; set; }
117+
112118
/// <summary>
113119
/// Gets or sets a value indicating whether to install the database when it is missing.
114120
/// </summary>
@@ -131,6 +137,9 @@ public class GlobalSettings
131137
/// </summary>
132138
public string MainDomLock { get; set; } = string.Empty;
133139

140+
/// <summary>
141+
/// Gets or sets the telemetry ID.
142+
/// </summary>
134143
public string Id { get; set; } = string.Empty;
135144

136145
/// <summary>
@@ -164,19 +173,19 @@ public class GlobalSettings
164173
/// </summary>
165174
public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation);
166175

167-
/// Gets a value indicating whether TinyMCE scripting sanitization should be applied
176+
/// <summary>
177+
/// Gets a value indicating whether TinyMCE scripting sanitization should be applied.
168178
/// </summary>
169179
[DefaultValue(StaticSanitizeTinyMce)]
170180
public bool SanitizeTinyMce => StaticSanitizeTinyMce;
171181

172182
/// <summary>
173-
/// An int value representing the time in milliseconds to lock the database for a write operation
183+
/// Gets a value representing the time in milliseconds to lock the database for a write operation.
174184
/// </summary>
175185
/// <remarks>
176-
/// The default value is 5000 milliseconds
186+
/// The default value is 5000 milliseconds.
177187
/// </remarks>
178-
/// <value>The timeout in milliseconds.</value>
179188
[DefaultValue(StaticSqlWriteLockTimeOut)]
180189
public TimeSpan SqlWriteLockTimeOut { get; } = TimeSpan.Parse(StaticSqlWriteLockTimeOut);
181190
}
182-
}
191+
}

src/Umbraco.Core/Constants-SystemDirectories.cs

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public static class SystemDirectories
4343

4444
public const string AppPlugins = "/App_Plugins";
4545
public static string AppPluginIcons => "/Backoffice/Icons";
46+
public const string CreatedPackages = "/created-packages";
47+
4648

4749
public const string MvcViews = "~/Views";
4850

src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs

+17-9
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,25 @@ namespace Umbraco.Cms.Core.DependencyInjection
1313
public static partial class UmbracoBuilderExtensions
1414
{
1515

16-
private static IUmbracoBuilder AddUmbracoOptions<TOptions>(this IUmbracoBuilder builder)
16+
private static IUmbracoBuilder AddUmbracoOptions<TOptions>(this IUmbracoBuilder builder, Action<OptionsBuilder<TOptions>> configure = null)
1717
where TOptions : class
1818
{
1919
var umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute<UmbracoOptionsAttribute>();
20-
2120
if (umbracoOptionsAttribute is null)
2221
{
23-
throw new ArgumentException("typeof(TOptions) do not have the UmbracoOptionsAttribute");
22+
throw new ArgumentException($"{typeof(TOptions)} do not have the UmbracoOptionsAttribute.");
2423
}
2524

26-
27-
builder.Services.AddOptions<TOptions>()
28-
.Bind(builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey),
29-
o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties)
25+
var optionsBuilder = builder.Services.AddOptions<TOptions>()
26+
.Bind(
27+
builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey),
28+
o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties
29+
)
3030
.ValidateDataAnnotations();
3131

32-
return builder;
32+
configure?.Invoke(optionsBuilder);
33+
34+
return builder;
3335
}
3436

3537
/// <summary>
@@ -52,7 +54,13 @@ public static IUmbracoBuilder AddConfiguration(this IUmbracoBuilder builder)
5254
.AddUmbracoOptions<ContentSettings>()
5355
.AddUmbracoOptions<CoreDebugSettings>()
5456
.AddUmbracoOptions<ExceptionFilterSettings>()
55-
.AddUmbracoOptions<GlobalSettings>()
57+
.AddUmbracoOptions<GlobalSettings>(optionsBuilder => optionsBuilder.PostConfigure(options =>
58+
{
59+
if (string.IsNullOrEmpty(options.UmbracoMediaPhysicalRootPath))
60+
{
61+
options.UmbracoMediaPhysicalRootPath = options.UmbracoMediaPath;
62+
}
63+
}))
5664
.AddUmbracoOptions<HealthChecksSettings>()
5765
.AddUmbracoOptions<HostingSettings>()
5866
.AddUmbracoOptions<ImagingSettings>()

src/Umbraco.Core/IO/FileSystemExtensions.cs

+20
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Security.Cryptography;
44
using System.Text;
55
using System.Threading;
6+
using Microsoft.Extensions.FileProviders;
67
using Umbraco.Cms.Core.IO;
78

89
namespace Umbraco.Extensions
@@ -87,5 +88,24 @@ public static void CreateFolder(this IFileSystem fs, string folderPath)
8788
}
8889
fs.DeleteFile(tempFile);
8990
}
91+
92+
/// <summary>
93+
/// Creates a new <see cref="IFileProvider" /> from the file system.
94+
/// </summary>
95+
/// <param name="fileSystem">The file system.</param>
96+
/// <param name="fileProvider">When this method returns, contains an <see cref="IFileProvider"/> created from the file system.</param>
97+
/// <returns>
98+
/// <c>true</c> if the <see cref="IFileProvider" /> was successfully created; otherwise, <c>false</c>.
99+
/// </returns>
100+
public static bool TryCreateFileProvider(this IFileSystem fileSystem, out IFileProvider fileProvider)
101+
{
102+
fileProvider = fileSystem switch
103+
{
104+
IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(),
105+
_ => null
106+
};
107+
108+
return fileProvider != null;
109+
}
90110
}
91111
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Microsoft.Extensions.FileProviders;
2+
3+
namespace Umbraco.Cms.Core.IO
4+
{
5+
/// <summary>
6+
/// Factory for creating <see cref="IFileProvider" /> instances.
7+
/// </summary>
8+
public interface IFileProviderFactory
9+
{
10+
/// <summary>
11+
/// Creates a new <see cref="IFileProvider" /> instance.
12+
/// </summary>
13+
/// <returns>
14+
/// The newly created <see cref="IFileProvider" /> instance (or <c>null</c> if not supported).
15+
/// </returns>
16+
IFileProvider Create();
17+
}
18+
}

src/Umbraco.Core/IO/MediaFileManager.cs

+17-10
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using Microsoft.Extensions.Options;
99
using Umbraco.Cms.Core.Configuration.Models;
1010
using Umbraco.Cms.Core.Models;
11-
using Umbraco.Cms.Core.Models.PublishedContent;
1211
using Umbraco.Cms.Core.PropertyEditors;
1312
using Umbraco.Cms.Core.Strings;
1413
using Umbraco.Extensions;
@@ -22,29 +21,37 @@ public sealed class MediaFileManager
2221
private readonly IShortStringHelper _shortStringHelper;
2322
private readonly IServiceProvider _serviceProvider;
2423
private MediaUrlGeneratorCollection _mediaUrlGenerators;
25-
private readonly ContentSettings _contentSettings;
26-
27-
/// <summary>
28-
/// Gets the media filesystem.
29-
/// </summary>
30-
public IFileSystem FileSystem { get; }
3124

3225
public MediaFileManager(
3326
IFileSystem fileSystem,
3427
IMediaPathScheme mediaPathScheme,
3528
ILogger<MediaFileManager> logger,
3629
IShortStringHelper shortStringHelper,
37-
IServiceProvider serviceProvider,
38-
IOptions<ContentSettings> contentSettings)
30+
IServiceProvider serviceProvider)
3931
{
4032
_mediaPathScheme = mediaPathScheme;
4133
_logger = logger;
4234
_shortStringHelper = shortStringHelper;
4335
_serviceProvider = serviceProvider;
44-
_contentSettings = contentSettings.Value;
4536
FileSystem = fileSystem;
4637
}
4738

39+
[Obsolete("Use the ctr that doesn't include unused parameters.")]
40+
public MediaFileManager(
41+
IFileSystem fileSystem,
42+
IMediaPathScheme mediaPathScheme,
43+
ILogger<MediaFileManager> logger,
44+
IShortStringHelper shortStringHelper,
45+
IServiceProvider serviceProvider,
46+
IOptions<ContentSettings> contentSettings)
47+
: this(fileSystem, mediaPathScheme, logger, shortStringHelper, serviceProvider)
48+
{ }
49+
50+
/// <summary>
51+
/// Gets the media filesystem.
52+
/// </summary>
53+
public IFileSystem FileSystem { get; }
54+
4855
/// <summary>
4956
/// Delete media files.
5057
/// </summary>

src/Umbraco.Core/IO/PhysicalFileSystem.cs

+12-6
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
using System.IO;
44
using System.Linq;
55
using System.Threading;
6+
using Microsoft.Extensions.FileProviders;
67
using Microsoft.Extensions.Logging;
78
using Umbraco.Cms.Core.Hosting;
89
using Umbraco.Extensions;
910

1011
namespace Umbraco.Cms.Core.IO
1112
{
12-
public interface IPhysicalFileSystem : IFileSystem {}
13-
public class PhysicalFileSystem : IPhysicalFileSystem
13+
public interface IPhysicalFileSystem : IFileSystem
14+
{ }
15+
16+
public class PhysicalFileSystem : IPhysicalFileSystem, IFileProviderFactory
1417
{
1518
private readonly IIOHelper _ioHelper;
1619
private readonly ILogger<PhysicalFileSystem> _logger;
@@ -28,7 +31,7 @@ public class PhysicalFileSystem : IPhysicalFileSystem
2831
// eg "" or "/Views" or "/Media" or "/<vpath>/Media" in case of a virtual path
2932
private readonly string _rootUrl;
3033

31-
public PhysicalFileSystem(IIOHelper ioHelper,IHostingEnvironment hostingEnvironment, ILogger<PhysicalFileSystem> logger, string rootPath, string rootUrl)
34+
public PhysicalFileSystem(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILogger<PhysicalFileSystem> logger, string rootPath, string rootUrl)
3235
{
3336
_ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper));
3437
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -270,7 +273,7 @@ public string GetRelativePath(string fullPathOrUrl)
270273
return path.Substring(_rootUrl.Length).TrimStart(Constants.CharArrays.ForwardSlash);
271274

272275
// unchanged - what else?
273-
return path;
276+
return path.TrimStart(Constants.CharArrays.ForwardSlash);
274277
}
275278

276279
/// <summary>
@@ -285,7 +288,7 @@ public string GetRelativePath(string fullPathOrUrl)
285288
public string GetFullPath(string path)
286289
{
287290
// normalize
288-
var opath = path;
291+
var originalPath = path;
289292
path = EnsureDirectorySeparatorChar(path);
290293

291294
// FIXME: this part should go!
@@ -318,7 +321,7 @@ public string GetFullPath(string path)
318321

319322
// nothing prevents us to reach the file, security-wise, yet it is outside
320323
// this filesystem's root - throw
321-
throw new UnauthorizedAccessException($"File original: [{opath}] full: [{path}] is outside this filesystem's root.");
324+
throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root.");
322325
}
323326

324327
/// <summary>
@@ -450,6 +453,9 @@ protected void WithRetry(Action action)
450453
}
451454
}
452455

456+
/// <inheritdoc />
457+
public IFileProvider Create() => new PhysicalFileProvider(_rootPath);
458+
453459
#endregion
454460
}
455461
}

src/Umbraco.Core/IO/ShadowWrapper.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5+
using Microsoft.Extensions.FileProviders;
56
using Microsoft.Extensions.Logging;
67
using Umbraco.Cms.Core.Hosting;
78
using Umbraco.Extensions;
89

910
namespace Umbraco.Cms.Core.IO
1011
{
11-
internal class ShadowWrapper : IFileSystem
12+
internal class ShadowWrapper : IFileSystem, IFileProviderFactory
1213
{
1314
private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs";
1415

@@ -220,5 +221,8 @@ public void AddFile(string path, string physicalPath, bool overrideIfExists = tr
220221
{
221222
FileSystem.AddFile(path, physicalPath, overrideIfExists, copy);
222223
}
224+
225+
/// <inheritdoc />
226+
public IFileProvider Create() => _innerFileSystem.TryCreateFileProvider(out IFileProvider fileProvider) ? fileProvider : null;
223227
}
224228
}

src/Umbraco.Core/Packaging/PackagesRepository.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public PackagesRepository(
9393

9494
_tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles";
9595
_packagesFolderPath = packagesFolderPath ?? Constants.SystemDirectories.Packages;
96-
_mediaFolderPath = mediaFolderPath ?? globalSettings.Value.UmbracoMediaPath + "/created-packages";
96+
_mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages);
9797

9898
_parser = new PackageDefinitionXmlParser();
9999
_mediaService = mediaService;

src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public void Handle(UmbracoApplicationStartingNotification notification)
2525
// ensure we have some essential directories
2626
// every other component can then initialize safely
2727
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data));
28-
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath));
28+
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath));
2929
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews));
3030
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews));
3131
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials));

src/Umbraco.Core/Umbraco.Core.csproj

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
@@ -17,6 +17,7 @@
1717
<ItemGroup>
1818
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
1919
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="5.0.11" />
20+
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="5.0.0" />
2021
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
2122
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
2223
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />

src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ internal static IUmbracoBuilder AddFileSystems(this IUmbracoBuilder builder)
4949
ILogger<PhysicalFileSystem> logger = factory.GetRequiredService<ILogger<PhysicalFileSystem>>();
5050
GlobalSettings globalSettings = factory.GetRequiredService<IOptions<GlobalSettings>>().Value;
5151

52-
var rootPath = hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPath);
52+
var rootPath = hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath);
5353
var rootUrl = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath);
5454
return new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, rootPath, rootUrl);
5555
});

src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public FilePermissionHelper(IOptions<GlobalSettings> globalSettings, IIOHelper i
4444
hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath),
4545
hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config),
4646
hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data),
47-
hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath),
47+
hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath),
4848
hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Preview)
4949
};
5050
_packagesPermissionsDirs = new[]
@@ -70,7 +70,7 @@ public bool RunFilePermissionTestSuite(out Dictionary<FilePermissionTest, IEnume
7070
EnsureFiles(_permissionFiles, out errors);
7171
report[FilePermissionTest.FileWriting] = errors.ToList();
7272

73-
EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath), out errors);
73+
EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), out errors);
7474
report[FilePermissionTest.MediaFolderCreation] = errors.ToList();
7575

7676
return report.Sum(x => x.Value.Count()) == 0;

0 commit comments

Comments
 (0)