From 1727a5d6f1ccd354ba4e56f143e33a8bab051e1b Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 8 Dec 2020 12:32:19 +0100 Subject: [PATCH 1/4] Added specific PhysicalFileProvider for the app_plugins folder, that also checks whether extensions are allowed. --- .../Models/UmbracoPluginSettings.cs | 21 ++++++++ src/Umbraco.Core/Constants-Configuration.cs | 1 + .../BackOfficeApplicationBuilderExtensions.cs | 21 +++++++- .../UmbracoPluginPhysicalFileProvider.cs | 53 +++++++++++++++++++ .../Builder/UmbracoBuilderExtensions.cs | 3 +- src/Umbraco.Web.UI.NetCore/Startup.cs | 9 ++-- 6 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs create mode 100644 src/Umbraco.Web.BackOffice/Plugins/UmbracoPluginPhysicalFileProvider.cs diff --git a/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs new file mode 100644 index 0000000000..3532db4e9e --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs @@ -0,0 +1,21 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; + +namespace Umbraco.Core.Configuration.Models +{ + /// + /// Typed configuration options for the plugins. + /// + public class UmbracoPluginSettings + { + /// + /// Gets or sets the allowed file extensions (including the period ".") that should be accessible from the browser. + /// + public ISet BrowsableFileExtensions { get; set; } = new HashSet(new[] + { + ".html", ".css", ".js", ".jpg", ".jpeg", ".gif", ".png", ".svg" + }); + } +} diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 2c9fe3caee..a60f29c39d 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -39,6 +39,7 @@ public const string ConfigMemberPassword = ConfigPrefix + "Security:MemberPassword"; public const string ConfigModelsBuilder = ConfigPrefix + "ModelsBuilder"; public const string ConfigNuCache = ConfigPrefix + "NuCache"; + public const string ConfigPlugins = ConfigPrefix + "Plugins"; public const string ConfigRequestHandler = ConfigPrefix + "RequestHandler"; public const string ConfigRuntime = ConfigPrefix + "Runtime"; public const string ConfigRuntimeMinification = ConfigPrefix + "RuntimeMinification"; diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs index 6ff42a5737..84383b1d40 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs @@ -1,8 +1,13 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using SixLabors.ImageSharp.Web.DependencyInjection; +using Umbraco.Core; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Hosting; using Umbraco.Web.BackOffice.Middleware; +using Umbraco.Web.BackOffice.Plugins; using Umbraco.Web.BackOffice.Routing; using Umbraco.Web.Common.Security; @@ -21,6 +26,7 @@ namespace Umbraco.Extensions app.UseRequestLocalization(); app.UseUmbracoRequestLogging(); app.UseUmbracoBackOffice(); + app.UseUmbracoPlugins(); app.UseUmbracoPreview(); app.UseUmbracoInstaller(); @@ -57,6 +63,20 @@ namespace Umbraco.Extensions return app; } + public static IApplicationBuilder UseUmbracoPlugins(this IApplicationBuilder app) + { + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new UmbracoPluginPhysicalFileProvider( + app.ApplicationServices.GetRequiredService().MapPathContentRoot(Constants.SystemDirectories.AppPlugins), + app.ApplicationServices.GetRequiredService>()), + RequestPath = Constants.SystemDirectories.AppPlugins + }); + + return app; + } + + public static IApplicationBuilder UseUmbracoPreview(this IApplicationBuilder app) { app.UseEndpoints(endpoints => @@ -67,7 +87,6 @@ namespace Umbraco.Extensions return app; } - private static IApplicationBuilder UseBackOfficeUserManagerAuditing(this IApplicationBuilder app) { var auditer = app.ApplicationServices.GetRequiredService(); diff --git a/src/Umbraco.Web.BackOffice/Plugins/UmbracoPluginPhysicalFileProvider.cs b/src/Umbraco.Web.BackOffice/Plugins/UmbracoPluginPhysicalFileProvider.cs new file mode 100644 index 0000000000..42300e3b71 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Plugins/UmbracoPluginPhysicalFileProvider.cs @@ -0,0 +1,53 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.IO; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileProviders.Physical; +using Microsoft.Extensions.Options; +using Umbraco.Core; +using Umbraco.Core.Configuration.Models; + +namespace Umbraco.Web.BackOffice.Plugins +{ + /// + /// Looks up files using the on-disk file system and check file extensions are on a allow list + /// + /// + /// When the environment variable "DOTNET_USE_POLLING_FILE_WATCHER" is set to "1" or "true", calls to + /// will use . + /// + public class UmbracoPluginPhysicalFileProvider : PhysicalFileProvider, IFileProvider + { + private readonly IOptions _options; + + /// + /// Initializes a new instance of the class, at the given root directory. + /// + /// The root directory. This should be an absolute path. + /// The configuration options. + /// Specifies which files or directories are excluded. + public UmbracoPluginPhysicalFileProvider(string root, IOptions options, ExclusionFilters filters = ExclusionFilters.Sensitive) + : base(root, filters) => _options = options; + + /// + /// Locate a file at the given path by directly mapping path segments to physical directories. + /// + /// + /// The path needs to pass the and the to be found. + /// + /// A path under the root directory + /// The file information. Caller must check property. + public new IFileInfo GetFileInfo(string subpath) + { + var extension = Path.GetExtension(subpath); + var subPathInclAppPluginsFolder = Path.Combine(Constants.SystemDirectories.AppPlugins, subpath); + if (!_options.Value.BrowsableFileExtensions.Contains(extension)) + { + return new NotFoundFileInfo(subPathInclAppPluginsFolder); + } + + return base.GetFileInfo(subPathInclAppPluginsFolder); + } + } +} diff --git a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs index e72089b8fe..17c91d433a 100644 --- a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs @@ -18,13 +18,11 @@ using Microsoft.Extensions.Options; using Serilog; using Smidge; using Smidge.Nuglify; -using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Configuration.Models.Validation; -using Umbraco.Core.DependencyInjection; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; @@ -195,6 +193,7 @@ namespace Umbraco.Core.DependencyInjection builder.Services.Configure(builder.Config.GetSection(Core.Constants.Configuration.ConfigTypeFinder)); builder.Services.Configure(builder.Config.GetSection(Core.Constants.Configuration.ConfigUserPassword)); builder.Services.Configure(builder.Config.GetSection(Core.Constants.Configuration.ConfigWebRouting)); + builder.Services.Configure(builder.Config.GetSection(Core.Constants.Configuration.ConfigPlugins)); return builder; } diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index d496aadfd3..2598b09d8d 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -4,8 +4,8 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Umbraco.Extensions; using Umbraco.Core.DependencyInjection; +using Umbraco.Extensions; namespace Umbraco.Web.UI.NetCore { @@ -28,11 +28,14 @@ namespace Umbraco.Web.UI.NetCore _config = config ?? throw new ArgumentNullException(nameof(config)); } - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + /// /// Configures the services /// + /// + /// This method gets called by the runtime. Use this method to add services to the container. + /// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + /// public void ConfigureServices(IServiceCollection services) { #pragma warning disable IDE0022 // Use expression body for methods From f580e38383a25ab651bf94b93e628e6a81067f5e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 8 Dec 2020 13:18:23 +0100 Subject: [PATCH 2/4] Added fonts to default BrowsableFileExtensions --- .../Configuration/Models/UmbracoPluginSettings.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs index 3532db4e9e..1bac495408 100644 --- a/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs @@ -15,7 +15,11 @@ namespace Umbraco.Core.Configuration.Models /// public ISet BrowsableFileExtensions { get; set; } = new HashSet(new[] { - ".html", ".css", ".js", ".jpg", ".jpeg", ".gif", ".png", ".svg" + ".html", // markup + ".css", // styles + ".js", // scripts + ".jpg", ".jpeg", ".gif", ".png", ".svg", // images + ".eot", ".ttf", ".woff" // fonts }); } } From 6bd8285a803a0a7825e1bbbfd3cf232092202e23 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 8 Dec 2020 14:30:51 +0100 Subject: [PATCH 3/4] Added Configuration files and license to default BrowsableFileExtensions --- .../Configuration/Models/UmbracoPluginSettings.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs index 1bac495408..907b29490d 100644 --- a/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs @@ -19,7 +19,9 @@ namespace Umbraco.Core.Configuration.Models ".css", // styles ".js", // scripts ".jpg", ".jpeg", ".gif", ".png", ".svg", // images - ".eot", ".ttf", ".woff" // fonts + ".eot", ".ttf", ".woff", // fonts + ".xml", ".json", ".config", // configurations + ".lic" // license }); } } From 0f36db188e6daa4719260a87c6ec6d8d36086775 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 9 Dec 2020 09:03:49 +0100 Subject: [PATCH 4/4] Ensure folder app_plugins exists --- .../BackOfficeApplicationBuilderExtensions.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs index 84383b1d40..dd7c1b12fc 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -65,11 +66,21 @@ namespace Umbraco.Extensions public static IApplicationBuilder UseUmbracoPlugins(this IApplicationBuilder app) { + var hostingEnvironment = app.ApplicationServices.GetRequiredService(); + var umbracoPluginSettings = app.ApplicationServices.GetRequiredService>(); + + 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 { - FileProvider = new UmbracoPluginPhysicalFileProvider( - app.ApplicationServices.GetRequiredService().MapPathContentRoot(Constants.SystemDirectories.AppPlugins), - app.ApplicationServices.GetRequiredService>()), + FileProvider = fileProvider, RequestPath = Constants.SystemDirectories.AppPlugins });