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