From 5979fe5eefc04863a564834eea231351ea277762 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Wed, 21 Oct 2020 14:02:20 +0200 Subject: [PATCH] BTCPay Extensions Part 2 (#2001) * BTCPay Extensions Part 2 This PR cleans up the extension system a bit in that: * It renames the test extension to a more uniform name * Allows yo uto have system extensions, which are extensions but bundled by default with the release (and cannot be removed) * Adds a tool to help you generate an extension package from a csproj * Refactors the UI extension points to a view component * Moves some more interfaces to the Abstractions csproj * Rename to plugins --- .gitignore | 1 + .run/Build and pack extensions.run.xml | 6 + .run/Pack Test Extension.run.xml | 21 ++ .../Constants}/AuthenticationSchemes.cs | 0 ...verExtension.cs => IBTCPayServerPlugin.cs} | 10 +- .../Contracts/ISettingsRepository.cs | 12 ++ .../Contracts}/IStartupTask.cs | 0 .../Contracts/IStoreNavExtension.cs | 6 +- .../Converters/VersionConverter.cs | 19 ++ .../Extensions/Extensions.cs | 20 ++ .../Extensions/ServiceCollectionExtensions.cs | 0 .../Models/BaseIbtcPayServerPlugin.cs | 36 ++++ .../Models/StatusMessageModel.cs | 0 BTCPayServer.Common/BTCPayNetwork.cs | 4 +- .../BTCPayServer.PluginPacker.csproj | 13 ++ BTCPayServer.PluginPacker/Program.cs | 50 +++++ BTCPayServer.PluginPacker/README.md | 4 + .../ApplicationPartsLogger.cs | 2 +- .../BTCPayServer.Plugins.Test.csproj | 1 + .../Resources/img/screengrab.png | Bin BTCPayServer.Plugins.Test/TestExtension.cs | 19 ++ .../TestExtensionController.cs | 2 +- .../Shared/TestExtensionNavExtension.cshtml | 0 .../Views/TestExtension/Index.cshtml | 0 .../Views/_ViewImports.cshtml | 0 BTCPayServer.Test/TestExtension.cs | 25 --- .../BTCPayExtensions/ExtensionManager.cs | 160 -------------- .../UIExtensionPoint/Default.cshtml | 6 + .../UIExtensionPoint/UIExtensionPoint.cs | 23 ++ .../Configuration/BTCPayServerOptions.cs | 4 +- .../Configuration/ConfigurationExtensions.cs | 4 +- .../ServerController.Extensions.cs | 123 ----------- .../Controllers/ServerController.Plugins.cs | 124 +++++++++++ BTCPayServer/Extensions.cs | 10 +- BTCPayServer/Hosting/BTCPayServerServices.cs | 4 +- BTCPayServer/Hosting/Startup.cs | 5 +- BTCPayServer/Plugins/PluginManager.cs | 184 ++++++++++++++++ .../PluginService.cs} | 61 +++--- .../Ethereum/EthereumLikeExtensions.cs | 3 +- .../Ethereum/EthereumStoreNavExtension.cs | 12 -- .../Altcoins/Monero/MoneroLikeExtensions.cs | 2 +- .../Monero/MoneroStoreNavExtension.cs | 12 -- BTCPayServer/Services/SettingsRepository.cs | 2 +- BTCPayServer/Views/Manage/_Nav.cshtml | 1 + .../Views/Server/ListExtensions.cshtml | 152 ------------- BTCPayServer/Views/Server/ListPlugins.cshtml | 199 ++++++++++++++++++ BTCPayServer/Views/Server/ServerNavPages.cs | 2 +- BTCPayServer/Views/Server/_Nav.cshtml | 3 +- BTCPayServer/Views/Shared/_Layout.cshtml | 7 +- BTCPayServer/Views/Stores/_Nav.cshtml | 6 +- BTCPayServer/Views/ViewsRazor.cs | 4 + BTCPayServer/Views/Wallets/_Nav.cshtml | 35 +-- btcpayserver.sln | 34 ++- 53 files changed, 860 insertions(+), 573 deletions(-) create mode 100644 .run/Build and pack extensions.run.xml create mode 100644 .run/Pack Test Extension.run.xml rename {BTCPayServer/Security => BTCPayServer.Abstractions/Constants}/AuthenticationSchemes.cs (100%) rename BTCPayServer.Abstractions/Contracts/{IBTCPayServerExtension.cs => IBTCPayServerPlugin.cs} (59%) create mode 100644 BTCPayServer.Abstractions/Contracts/ISettingsRepository.cs rename {BTCPayServer/Hosting => BTCPayServer.Abstractions/Contracts}/IStartupTask.cs (100%) create mode 100644 BTCPayServer.Abstractions/Converters/VersionConverter.cs create mode 100644 BTCPayServer.Abstractions/Extensions/Extensions.cs rename {BTCPayServer => BTCPayServer.Abstractions}/Extensions/ServiceCollectionExtensions.cs (100%) create mode 100644 BTCPayServer.Abstractions/Models/BaseIbtcPayServerPlugin.cs rename {BTCPayServer => BTCPayServer.Abstractions}/Models/StatusMessageModel.cs (100%) create mode 100644 BTCPayServer.PluginPacker/BTCPayServer.PluginPacker.csproj create mode 100644 BTCPayServer.PluginPacker/Program.cs create mode 100644 BTCPayServer.PluginPacker/README.md rename BTCPayServer.Test/ss.cs => BTCPayServer.Plugins.Test/ApplicationPartsLogger.cs (97%) rename BTCPayServer.Test/BTCPayServer.Test.csproj => BTCPayServer.Plugins.Test/BTCPayServer.Plugins.Test.csproj (92%) rename {BTCPayServer.Test => BTCPayServer.Plugins.Test}/Resources/img/screengrab.png (100%) create mode 100644 BTCPayServer.Plugins.Test/TestExtension.cs rename {BTCPayServer.Test => BTCPayServer.Plugins.Test}/TestExtensionController.cs (87%) rename {BTCPayServer.Test => BTCPayServer.Plugins.Test}/Views/Shared/TestExtensionNavExtension.cshtml (100%) rename {BTCPayServer.Test => BTCPayServer.Plugins.Test}/Views/TestExtension/Index.cshtml (100%) rename {BTCPayServer.Test => BTCPayServer.Plugins.Test}/Views/_ViewImports.cshtml (100%) delete mode 100644 BTCPayServer.Test/TestExtension.cs delete mode 100644 BTCPayServer/BTCPayExtensions/ExtensionManager.cs create mode 100644 BTCPayServer/Components/UIExtensionPoint/Default.cshtml create mode 100644 BTCPayServer/Components/UIExtensionPoint/UIExtensionPoint.cs delete mode 100644 BTCPayServer/Controllers/ServerController.Extensions.cs create mode 100644 BTCPayServer/Controllers/ServerController.Plugins.cs create mode 100644 BTCPayServer/Plugins/PluginManager.cs rename BTCPayServer/{BTCPayExtensions/ExtensionService.cs => Plugins/PluginService.cs} (60%) delete mode 100644 BTCPayServer/Services/Altcoins/Ethereum/EthereumStoreNavExtension.cs delete mode 100644 BTCPayServer/Services/Altcoins/Monero/MoneroStoreNavExtension.cs delete mode 100644 BTCPayServer/Views/Server/ListExtensions.cshtml create mode 100644 BTCPayServer/Views/Server/ListPlugins.cshtml diff --git a/.gitignore b/.gitignore index 1fb4de6cc..bce7c51e4 100644 --- a/.gitignore +++ b/.gitignore @@ -298,3 +298,4 @@ BTCPayServer/wwwroot/bundles/* !.vscode/extensions.json BTCPayServer/testpwd .DS_Store +Packed Plugins diff --git a/.run/Build and pack extensions.run.xml b/.run/Build and pack extensions.run.xml new file mode 100644 index 000000000..72b7ea83c --- /dev/null +++ b/.run/Build and pack extensions.run.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.run/Pack Test Extension.run.xml b/.run/Pack Test Extension.run.xml new file mode 100644 index 000000000..dd89cb00d --- /dev/null +++ b/.run/Pack Test Extension.run.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/BTCPayServer/Security/AuthenticationSchemes.cs b/BTCPayServer.Abstractions/Constants/AuthenticationSchemes.cs similarity index 100% rename from BTCPayServer/Security/AuthenticationSchemes.cs rename to BTCPayServer.Abstractions/Constants/AuthenticationSchemes.cs diff --git a/BTCPayServer.Abstractions/Contracts/IBTCPayServerExtension.cs b/BTCPayServer.Abstractions/Contracts/IBTCPayServerPlugin.cs similarity index 59% rename from BTCPayServer.Abstractions/Contracts/IBTCPayServerExtension.cs rename to BTCPayServer.Abstractions/Contracts/IBTCPayServerPlugin.cs index 525f60976..822e3412a 100644 --- a/BTCPayServer.Abstractions/Contracts/IBTCPayServerExtension.cs +++ b/BTCPayServer.Abstractions/Contracts/IBTCPayServerPlugin.cs @@ -1,16 +1,20 @@ using System; +using System.Text.Json.Serialization; +using BTCPayServer.Abstractions.Converters; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; namespace BTCPayServer.Contracts { - public interface IBTCPayServerExtension + public interface IBTCPayServerPlugin { - public string Identifier { get;} + public string Identifier { get; } string Name { get; } + [JsonConverter(typeof(VersionConverter))] Version Version { get; } string Description { get; } - + bool SystemPlugin { get; set; } + string[] Dependencies { get; } void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices); void Execute(IServiceCollection applicationBuilder); } diff --git a/BTCPayServer.Abstractions/Contracts/ISettingsRepository.cs b/BTCPayServer.Abstractions/Contracts/ISettingsRepository.cs new file mode 100644 index 000000000..add724d6d --- /dev/null +++ b/BTCPayServer.Abstractions/Contracts/ISettingsRepository.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace BTCPayServer.Services +{ + public interface ISettingsRepository + { + Task GetSettingAsync(string name = null); + Task UpdateSetting(T obj, string name = null); + Task WaitSettingsChanged(CancellationToken cancellationToken = default); + } +} diff --git a/BTCPayServer/Hosting/IStartupTask.cs b/BTCPayServer.Abstractions/Contracts/IStartupTask.cs similarity index 100% rename from BTCPayServer/Hosting/IStartupTask.cs rename to BTCPayServer.Abstractions/Contracts/IStartupTask.cs diff --git a/BTCPayServer.Abstractions/Contracts/IStoreNavExtension.cs b/BTCPayServer.Abstractions/Contracts/IStoreNavExtension.cs index 59b6d1958..4bf60b3c2 100644 --- a/BTCPayServer.Abstractions/Contracts/IStoreNavExtension.cs +++ b/BTCPayServer.Abstractions/Contracts/IStoreNavExtension.cs @@ -1,15 +1,15 @@ namespace BTCPayServer.Contracts { - public interface INavExtension + public interface IUIExtension { string Partial { get; } string Location { get; } } - public class NavExtension: INavExtension + public class UIExtension: IUIExtension { - public NavExtension(string partial, string location) + public UIExtension(string partial, string location) { Partial = partial; Location = location; diff --git a/BTCPayServer.Abstractions/Converters/VersionConverter.cs b/BTCPayServer.Abstractions/Converters/VersionConverter.cs new file mode 100644 index 000000000..aab88faef --- /dev/null +++ b/BTCPayServer.Abstractions/Converters/VersionConverter.cs @@ -0,0 +1,19 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace BTCPayServer.Abstractions.Converters +{ + public class VersionConverter : JsonConverter + { + public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return Version.Parse(reader.GetString()); + } + + public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } + } +} diff --git a/BTCPayServer.Abstractions/Extensions/Extensions.cs b/BTCPayServer.Abstractions/Extensions/Extensions.cs new file mode 100644 index 000000000..98c3f5973 --- /dev/null +++ b/BTCPayServer.Abstractions/Extensions/Extensions.cs @@ -0,0 +1,20 @@ +using System.Text.Json; +using BTCPayServer.Models; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace BTCPayServer +{ + public static class SetStatusMessageModelExtensions + { + public static void SetStatusMessageModel(this ITempDataDictionary tempData, StatusMessageModel statusMessage) + { + if (statusMessage == null) + { + tempData.Remove("StatusMessageModel"); + return; + } + + tempData["StatusMessageModel"] = JsonSerializer.Serialize(statusMessage, new JsonSerializerOptions()); + } + } +} diff --git a/BTCPayServer/Extensions/ServiceCollectionExtensions.cs b/BTCPayServer.Abstractions/Extensions/ServiceCollectionExtensions.cs similarity index 100% rename from BTCPayServer/Extensions/ServiceCollectionExtensions.cs rename to BTCPayServer.Abstractions/Extensions/ServiceCollectionExtensions.cs diff --git a/BTCPayServer.Abstractions/Models/BaseIbtcPayServerPlugin.cs b/BTCPayServer.Abstractions/Models/BaseIbtcPayServerPlugin.cs new file mode 100644 index 000000000..70a7ba31a --- /dev/null +++ b/BTCPayServer.Abstractions/Models/BaseIbtcPayServerPlugin.cs @@ -0,0 +1,36 @@ +using System; +using System.Reflection; +using BTCPayServer.Contracts; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Models +{ + public abstract class BaseBTCPayServerPlugin : IBTCPayServerPlugin + { + public abstract string Identifier { get; } + public abstract string Name { get; } + + public virtual Version Version + { + get + { + return Assembly.GetAssembly(GetType())?.GetName().Version ?? new Version(1, 0, 0, 0); + } + } + + public abstract string Description { get; } + public bool SystemPlugin { get; set; } + public bool SystemExtension { get; set; } + public virtual string[] Dependencies { get; } = Array.Empty(); + + public virtual void Execute(IApplicationBuilder applicationBuilder, + IServiceProvider applicationBuilderApplicationServices) + { + } + + public virtual void Execute(IServiceCollection applicationBuilder) + { + } + } +} diff --git a/BTCPayServer/Models/StatusMessageModel.cs b/BTCPayServer.Abstractions/Models/StatusMessageModel.cs similarity index 100% rename from BTCPayServer/Models/StatusMessageModel.cs rename to BTCPayServer.Abstractions/Models/StatusMessageModel.cs diff --git a/BTCPayServer.Common/BTCPayNetwork.cs b/BTCPayServer.Common/BTCPayNetwork.cs index b290f017d..f2696c284 100644 --- a/BTCPayServer.Common/BTCPayNetwork.cs +++ b/BTCPayServer.Common/BTCPayNetwork.cs @@ -24,7 +24,7 @@ namespace BTCPayServer var settings = new BTCPayDefaultSettings(); _Settings.Add(chainType, settings); settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", NBXplorerDefaultSettings.GetFolderName(chainType)); - settings.DefaultExtensionDirectory = + settings.DefaultPluginDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", "Extensions"); settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config"); settings.DefaultPort = (chainType == NetworkType.Mainnet ? 23000 : @@ -41,7 +41,7 @@ namespace BTCPayServer } public string DefaultDataDirectory { get; set; } - public string DefaultExtensionDirectory { get; set; } + public string DefaultPluginDirectory { get; set; } public string DefaultConfigurationFile { get; set; } public int DefaultPort { get; set; } } diff --git a/BTCPayServer.PluginPacker/BTCPayServer.PluginPacker.csproj b/BTCPayServer.PluginPacker/BTCPayServer.PluginPacker.csproj new file mode 100644 index 000000000..25043c004 --- /dev/null +++ b/BTCPayServer.PluginPacker/BTCPayServer.PluginPacker.csproj @@ -0,0 +1,13 @@ + + + + Exe + netcoreapp3.1 + 1.0.0.1 + + + + + + + diff --git a/BTCPayServer.PluginPacker/Program.cs b/BTCPayServer.PluginPacker/Program.cs new file mode 100644 index 000000000..3d807abf0 --- /dev/null +++ b/BTCPayServer.PluginPacker/Program.cs @@ -0,0 +1,50 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using BTCPayServer.Contracts; + +namespace BTCPayServer.PluginPacker +{ + class Program + { + static void Main(string[] args) + { + var directory = args[0]; + var name = args[1]; + var outputDir = args[2]; + var outputFile = Path.Combine(outputDir, name); + var rootDLLPath = Path.Combine(directory, name +".dll"); + if (!File.Exists(rootDLLPath) ) + { + throw new Exception($"{rootDLLPath} could not be found"); + } + + var assembly = Assembly.LoadFrom(rootDLLPath); + var extension = GetAllExtensionTypesFromAssembly(assembly).FirstOrDefault(); + if (extension is null) + { + throw new Exception($"{rootDLLPath} is not a valid plugin"); + } + + var loadedPlugin = (IBTCPayServerPlugin)Activator.CreateInstance(extension); + var json = JsonSerializer.Serialize(loadedPlugin); + Directory.CreateDirectory(outputDir); + if (File.Exists(outputFile + ".btcpay")) + { + File.Delete(outputFile + ".btcpay"); + } + ZipFile.CreateFromDirectory(directory, outputFile + ".btcpay", CompressionLevel.Optimal, false); + File.WriteAllText(outputFile + ".btcpay.json", json); + } + + private static Type[] GetAllExtensionTypesFromAssembly(Assembly assembly) + { + return assembly.GetTypes().Where(type => + typeof(IBTCPayServerPlugin).IsAssignableFrom(type) && + !type.IsAbstract).ToArray(); + } + } +} diff --git a/BTCPayServer.PluginPacker/README.md b/BTCPayServer.PluginPacker/README.md new file mode 100644 index 000000000..54e9171ce --- /dev/null +++ b/BTCPayServer.PluginPacker/README.md @@ -0,0 +1,4 @@ +This tool makes it easy to create a BTCPay plugin package. To create a package you must: +* Build your BTCPay Plugin project +* open a terminal in this project +* run `dotnet run PATH_TO_PLUGIN_BUILD_DIRECTORY NAME_OF_PLUGIN BUILT_PACKAGE_OUTPUT_DIRECTORY` diff --git a/BTCPayServer.Test/ss.cs b/BTCPayServer.Plugins.Test/ApplicationPartsLogger.cs similarity index 97% rename from BTCPayServer.Test/ss.cs rename to BTCPayServer.Plugins.Test/ApplicationPartsLogger.cs index adfaa8ca5..d7bbd57bc 100644 --- a/BTCPayServer.Test/ss.cs +++ b/BTCPayServer.Plugins.Test/ApplicationPartsLogger.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace BTCPayServer.Test +namespace BTCPayServer.Plugins.Test { public class ApplicationPartsLogger : IHostedService { diff --git a/BTCPayServer.Test/BTCPayServer.Test.csproj b/BTCPayServer.Plugins.Test/BTCPayServer.Plugins.Test.csproj similarity index 92% rename from BTCPayServer.Test/BTCPayServer.Test.csproj rename to BTCPayServer.Plugins.Test/BTCPayServer.Plugins.Test.csproj index 3a9c797c9..3a084657c 100644 --- a/BTCPayServer.Test/BTCPayServer.Test.csproj +++ b/BTCPayServer.Plugins.Test/BTCPayServer.Plugins.Test.csproj @@ -4,6 +4,7 @@ true false true + 1.0.0 diff --git a/BTCPayServer.Test/Resources/img/screengrab.png b/BTCPayServer.Plugins.Test/Resources/img/screengrab.png similarity index 100% rename from BTCPayServer.Test/Resources/img/screengrab.png rename to BTCPayServer.Plugins.Test/Resources/img/screengrab.png diff --git a/BTCPayServer.Plugins.Test/TestExtension.cs b/BTCPayServer.Plugins.Test/TestExtension.cs new file mode 100644 index 000000000..f28fcf402 --- /dev/null +++ b/BTCPayServer.Plugins.Test/TestExtension.cs @@ -0,0 +1,19 @@ +using BTCPayServer.Contracts; +using BTCPayServer.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Plugins.Test +{ + public class TestPlugin: BaseBTCPayServerPlugin + { + public override string Identifier { get; } = "BTCPayServer.Plugins.Test"; + public override string Name { get; } = "Test Plugin!"; + public override string Description { get; } = "This is a description of the loaded test extension!"; + + public override void Execute(IServiceCollection services) + { + services.AddSingleton(new UIExtension("TestExtensionNavExtension", "header-nav")); + services.AddHostedService(); + } + } +} diff --git a/BTCPayServer.Test/TestExtensionController.cs b/BTCPayServer.Plugins.Test/TestExtensionController.cs similarity index 87% rename from BTCPayServer.Test/TestExtensionController.cs rename to BTCPayServer.Plugins.Test/TestExtensionController.cs index 348c0e241..a5bfcc166 100644 --- a/BTCPayServer.Test/TestExtensionController.cs +++ b/BTCPayServer.Plugins.Test/TestExtensionController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; -namespace BTCPayServer.Test +namespace BTCPayServer.Plugins.Test { [Route("extensions/test")] public class TestExtensionController : Controller diff --git a/BTCPayServer.Test/Views/Shared/TestExtensionNavExtension.cshtml b/BTCPayServer.Plugins.Test/Views/Shared/TestExtensionNavExtension.cshtml similarity index 100% rename from BTCPayServer.Test/Views/Shared/TestExtensionNavExtension.cshtml rename to BTCPayServer.Plugins.Test/Views/Shared/TestExtensionNavExtension.cshtml diff --git a/BTCPayServer.Test/Views/TestExtension/Index.cshtml b/BTCPayServer.Plugins.Test/Views/TestExtension/Index.cshtml similarity index 100% rename from BTCPayServer.Test/Views/TestExtension/Index.cshtml rename to BTCPayServer.Plugins.Test/Views/TestExtension/Index.cshtml diff --git a/BTCPayServer.Test/Views/_ViewImports.cshtml b/BTCPayServer.Plugins.Test/Views/_ViewImports.cshtml similarity index 100% rename from BTCPayServer.Test/Views/_ViewImports.cshtml rename to BTCPayServer.Plugins.Test/Views/_ViewImports.cshtml diff --git a/BTCPayServer.Test/TestExtension.cs b/BTCPayServer.Test/TestExtension.cs deleted file mode 100644 index e2c6dec8a..000000000 --- a/BTCPayServer.Test/TestExtension.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using BTCPayServer.Contracts; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace BTCPayServer.Test -{ - public class TestExtension: IBTCPayServerExtension - { - public string Identifier { get; } = "BTCPayServer.Test"; - public string Name { get; } = "Test Plugin!"; - public Version Version { get; } = new Version(1,0,0,0); - public string Description { get; } = "This is a description of the loaded test extension!"; - public void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices) - { - - } - - public void Execute(IServiceCollection services) - { - services.AddSingleton(new NavExtension("TestExtensionNavExtension", "header-nav")); - services.AddHostedService(); - } - } -} diff --git a/BTCPayServer/BTCPayExtensions/ExtensionManager.cs b/BTCPayServer/BTCPayExtensions/ExtensionManager.cs deleted file mode 100644 index 93e7289a6..000000000 --- a/BTCPayServer/BTCPayExtensions/ExtensionManager.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Reflection; -using BTCPayServer.Configuration; -using BTCPayServer.Contracts; -using McMaster.NETCore.Plugins; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Logging; - -namespace BTCPayServer -{ - public static class ExtensionManager - { - public const string BTCPayExtensionSuffix =".btcpay"; - private static readonly List _pluginAssemblies = new List(); - private static ILogger _logger; - - public static IMvcBuilder AddExtensions(this IMvcBuilder mvcBuilder, IServiceCollection serviceCollection, - IConfiguration config, ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(typeof(ExtensionManager)); - var extensionsFolder = config.GetExtensionDir(DefaultConfiguration.GetNetworkType(config)); - var extensions = new List(); - - _logger.LogInformation($"Loading extensions from {extensionsFolder}"); - Directory.CreateDirectory(extensionsFolder); - ExecuteCommands(extensionsFolder); - List<(PluginLoader, Assembly, IFileProvider)> plugins = new List<(PluginLoader, Assembly, IFileProvider)>(); - foreach (var dir in Directory.GetDirectories(extensionsFolder)) - { - var pluginName = Path.GetFileName(dir); - - var plugin = PluginLoader.CreateFromAssemblyFile( - Path.Combine(dir, pluginName + ".dll"), // create a plugin from for the .dll file - config => - // this ensures that the version of MVC is shared between this app and the plugin - config.PreferSharedTypes = true); - - mvcBuilder.AddPluginLoader(plugin); - var pluginAssembly = plugin.LoadDefaultAssembly(); - _pluginAssemblies.Add(pluginAssembly); - var fileProvider = CreateEmbeddedFileProviderForAssembly(pluginAssembly); - plugins.Add((plugin, pluginAssembly, fileProvider)); - extensions.AddRange(GetAllExtensionTypesFromAssembly(pluginAssembly) - .Select(GetExtensionInstanceFromType)); - } - - foreach (var extension in extensions) - { - _logger.LogInformation($"Adding and executing extension {extension.Identifier} - {extension.Version}"); - serviceCollection.AddSingleton(extension); - extension.Execute(serviceCollection); - } - - return mvcBuilder; - } - - public static void UseExtensions(this IApplicationBuilder applicationBuilder) - { - foreach (var extension in applicationBuilder.ApplicationServices - .GetServices()) - { - extension.Execute(applicationBuilder, - applicationBuilder.ApplicationServices); - } - - var webHostEnvironment = applicationBuilder.ApplicationServices.GetService(); - List providers = new List() {webHostEnvironment.WebRootFileProvider}; - providers.AddRange( - _pluginAssemblies - .Select(CreateEmbeddedFileProviderForAssembly)); - webHostEnvironment.WebRootFileProvider = new CompositeFileProvider(providers); - } - - private static Type[] GetAllExtensionTypesFromAssembly(Assembly assembly) - { - return assembly.GetTypes().Where(type => - typeof(IBTCPayServerExtension).IsAssignableFrom(type) && - !type.IsAbstract).ToArray(); - } - - private static IBTCPayServerExtension GetExtensionInstanceFromType(Type type) - { - return (IBTCPayServerExtension)Activator.CreateInstance(type, Array.Empty()); - } - - private static IFileProvider CreateEmbeddedFileProviderForAssembly(Assembly assembly) - { - return new EmbeddedFileProvider(assembly); - } - - private static void ExecuteCommands(string extensionsFolder) - { - var pendingCommands = GetPendingCommands(extensionsFolder); - foreach (var command in pendingCommands) - { - ExecuteCommand(command, extensionsFolder); - } - File.Delete(Path.Combine(extensionsFolder, "commands")); - } - - private static void ExecuteCommand((string command, string extension) command, string extensionsFolder) - { - var dirName = Path.Combine(extensionsFolder, command.extension); - switch (command.command) - { - case "delete": - if (Directory.Exists(dirName)) - { - Directory.Delete(dirName, true); - } - break; - case "install": - var fileName = dirName + BTCPayExtensionSuffix; - if (File.Exists(fileName)) - { - ZipFile.ExtractToDirectory(fileName, dirName, true); - File.Delete(fileName); - } - break; - } - } - - public static (string command, string extension)[] GetPendingCommands(string extensionsFolder) - { - if (!File.Exists(Path.Combine(extensionsFolder, "commands"))) - return Array.Empty<(string command, string extension)>(); - var commands = File.ReadAllLines(Path.Combine(extensionsFolder, "commands")); - return commands.Select(s => - { - var split = s.Split(':'); - return (split[0].ToLower(CultureInfo.InvariantCulture), split[1]); - }).ToArray(); - } - - public static void QueueCommands(string extensionsFolder, params ( string action, string extension)[] commands) - { - File.AppendAllLines(Path.Combine(extensionsFolder, "commands"), - commands.Select((tuple) => $"{tuple.action}:{tuple.extension}")); - } - - public static void CancelCommands(string extensionDir, string extension) - { - var cmds = GetPendingCommands(extensionDir).Where(tuple => - !tuple.extension.Equals(extension, StringComparison.InvariantCultureIgnoreCase)).ToArray(); - - File.Delete(Path.Combine(extensionDir, "commands")); - QueueCommands(extensionDir, cmds); - - } - } -} diff --git a/BTCPayServer/Components/UIExtensionPoint/Default.cshtml b/BTCPayServer/Components/UIExtensionPoint/Default.cshtml new file mode 100644 index 000000000..9f9e5d8df --- /dev/null +++ b/BTCPayServer/Components/UIExtensionPoint/Default.cshtml @@ -0,0 +1,6 @@ +@model IEnumerable + +@foreach (var partial in Model) +{ + await Html.RenderPartialAsync(partial); +} diff --git a/BTCPayServer/Components/UIExtensionPoint/UIExtensionPoint.cs b/BTCPayServer/Components/UIExtensionPoint/UIExtensionPoint.cs new file mode 100644 index 000000000..9d4fedade --- /dev/null +++ b/BTCPayServer/Components/UIExtensionPoint/UIExtensionPoint.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BTCPayServer.Contracts; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Components.UIExtensionPoint +{ + public class UiExtensionPoint : ViewComponent + { + private readonly IEnumerable _uiExtensions; + + public UiExtensionPoint(IEnumerable uiExtensions) + { + _uiExtensions = uiExtensions; + } + + public IViewComponentResult Invoke(string location) + { + return View(_uiExtensions.Where(extension => extension.Location.Equals(location, StringComparison.InvariantCultureIgnoreCase)).Select(extension => extension.Partial)); + } + } +} diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index 0a12ecd12..7b6db5d00 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -80,7 +80,7 @@ namespace BTCPayServer.Configuration { NetworkType = DefaultConfiguration.GetNetworkType(conf); DataDir = conf.GetDataDir(NetworkType); - ExtensionDir = conf.GetExtensionDir(NetworkType); + PluginDir = conf.GetPluginDir(NetworkType); Logs.Configuration.LogInformation("Network: " + NetworkType.ToString()); if (conf.GetOrDefault("launchsettings", false) && NetworkType != NetworkType.Regtest) @@ -243,7 +243,7 @@ namespace BTCPayServer.Configuration DisableRegistration = conf.GetOrDefault("disable-registration", true); } - public string ExtensionDir { get; set; } + public string PluginDir { get; set; } private SSHSettings ParseSSHConfiguration(IConfiguration conf) { diff --git a/BTCPayServer/Configuration/ConfigurationExtensions.cs b/BTCPayServer/Configuration/ConfigurationExtensions.cs index a82f8f783..8ec577f53 100644 --- a/BTCPayServer/Configuration/ConfigurationExtensions.cs +++ b/BTCPayServer/Configuration/ConfigurationExtensions.cs @@ -68,10 +68,10 @@ namespace BTCPayServer.Configuration return configuration.GetOrDefault("datadir", defaultSettings.DefaultDataDirectory); } - public static string GetExtensionDir(this IConfiguration configuration, NetworkType networkType) + public static string GetPluginDir(this IConfiguration configuration, NetworkType networkType) { var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType); - return configuration.GetOrDefault("extensiondir", defaultSettings.DefaultExtensionDirectory); + return configuration.GetOrDefault("plugindir", defaultSettings.DefaultPluginDirectory); } } } diff --git a/BTCPayServer/Controllers/ServerController.Extensions.cs b/BTCPayServer/Controllers/ServerController.Extensions.cs deleted file mode 100644 index 26a426799..000000000 --- a/BTCPayServer/Controllers/ServerController.Extensions.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Configuration; -using BTCPayServer.Contracts; -using BTCPayServer.Models; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace BTCPayServer.Controllers -{ - public partial class ServerController - { - [HttpGet("server/extensions")] - public async Task ListExtensions( - [FromServices] ExtensionService extensionService, - [FromServices] BTCPayServerOptions btcPayServerOptions, - string remote = "btcpayserver/btcpayserver-extensions") - { - IEnumerable availableExtensions; - try - { - availableExtensions = await extensionService.GetRemoteExtensions(remote); - } - catch (Exception e) - { - TempData.SetStatusMessageModel(new StatusMessageModel() - { - Severity = StatusMessageModel.StatusSeverity.Error, - Message = "The remote could not be reached" - }); - availableExtensions = Array.Empty(); - } - var res = new ListExtensionsViewModel() - { - Installed = extensionService.LoadedExtensions, - Available = availableExtensions, - Remote = remote, - Commands = extensionService.GetPendingCommands(), - CanShowRestart = btcPayServerOptions.DockerDeployment - }; - return View(res); - } - - public class ListExtensionsViewModel - { - public string Remote { get; set; } - public IEnumerable Installed { get; set; } - public IEnumerable Available { get; set; } - public (string command, string extension)[] Commands { get; set; } - public bool CanShowRestart { get; set; } - } - - [HttpPost("server/extensions/uninstall")] - public IActionResult UnInstallExtension( - [FromServices] ExtensionService extensionService, string extension) - { - extensionService.UninstallExtension(extension); - TempData.SetStatusMessageModel(new StatusMessageModel() - { - Message = "Extension scheduled to be uninstalled", - Severity = StatusMessageModel.StatusSeverity.Success - }); - - return RedirectToAction("ListExtensions"); - } - [HttpPost("server/extensions/cancel")] - public IActionResult CancelExtensionCommands( - [FromServices] ExtensionService extensionService, string extension) - { - extensionService.CancelCommands(extension); - TempData.SetStatusMessageModel(new StatusMessageModel() - { - Message = "Updated", - Severity = StatusMessageModel.StatusSeverity.Success - }); - - return RedirectToAction("ListExtensions"); - } - - [HttpPost("server/extensions/install")] - public async Task InstallExtension( - [FromServices] ExtensionService extensionService, string remote, string extension) - { - try - { - await extensionService.DownloadRemoteExtension(remote, extension); - extensionService.InstallExtension(extension); - TempData.SetStatusMessageModel(new StatusMessageModel() - { - Message = "Extension scheduled to be installed.", - Severity = StatusMessageModel.StatusSeverity.Success - }); - } - catch (Exception e) - { - TempData.SetStatusMessageModel(new StatusMessageModel() - { - Message = "The extension could not be downloaded. Try again later.", Severity = StatusMessageModel.StatusSeverity.Error - }); - } - - return RedirectToAction("ListExtensions"); - } - - - [HttpPost("server/extensions/upload")] - public async Task UploadExtension([FromServices] ExtensionService extensionService, - List files) - { - foreach (var formFile in files.Where(file => file.Length > 0)) - { - await extensionService.UploadExtension(formFile); - extensionService.InstallExtension(formFile.FileName.TrimEnd(ExtensionManager.BTCPayExtensionSuffix, - StringComparison.InvariantCultureIgnoreCase)); - } - - return RedirectToAction("ListExtensions", - new {StatusMessage = "Files uploaded, restart server to load extensions"}); - } - } -} diff --git a/BTCPayServer/Controllers/ServerController.Plugins.cs b/BTCPayServer/Controllers/ServerController.Plugins.cs new file mode 100644 index 000000000..6514a9425 --- /dev/null +++ b/BTCPayServer/Controllers/ServerController.Plugins.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Configuration; +using BTCPayServer.Contracts; +using BTCPayServer.Models; +using BTCPayServer.Plugins; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers +{ + public partial class ServerController + { + [HttpGet("server/plugins")] + public async Task ListPlugins( + [FromServices] PluginService pluginService, + [FromServices] BTCPayServerOptions btcPayServerOptions, + string remote = "btcpayserver/btcpayserver-plugins") + { + IEnumerable availablePlugins; + try + { + availablePlugins = await pluginService.GetRemotePlugins(remote); + } + catch (Exception) + { + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = "The remote could not be reached" + }); + availablePlugins = Array.Empty(); + } + var res = new ListPluginsViewModel() + { + Installed = pluginService.LoadedPlugins, + Available = availablePlugins, + Remote = remote, + Commands = pluginService.GetPendingCommands(), + CanShowRestart = btcPayServerOptions.DockerDeployment + }; + return View(res); + } + + public class ListPluginsViewModel + { + public string Remote { get; set; } + public IEnumerable Installed { get; set; } + public IEnumerable Available { get; set; } + public (string command, string plugin)[] Commands { get; set; } + public bool CanShowRestart { get; set; } + } + + [HttpPost("server/plugins/uninstall")] + public IActionResult UnInstallPlugin( + [FromServices] PluginService pluginService, string plugin) + { + pluginService.UninstallPlugin(plugin); + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Message = "Plugin scheduled to be uninstalled", + Severity = StatusMessageModel.StatusSeverity.Success + }); + + return RedirectToAction("ListPlugins"); + } + [HttpPost("server/plugins/cancel")] + public IActionResult CancelPluginCommands( + [FromServices] PluginService pluginService, string plugin) + { + pluginService.CancelCommands(plugin); + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Message = "Updated", + Severity = StatusMessageModel.StatusSeverity.Success + }); + + return RedirectToAction("ListPlugins"); + } + + [HttpPost("server/plugins/install")] + public async Task InstallPlugin( + [FromServices] PluginService pluginService, string remote, string plugin) + { + try + { + await pluginService.DownloadRemotePlugin(remote, plugin); + pluginService.InstallPlugin(plugin); + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Message = "Plugin scheduled to be installed.", + Severity = StatusMessageModel.StatusSeverity.Success + }); + } + catch (Exception) + { + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Message = "The plugin could not be downloaded. Try again later.", Severity = StatusMessageModel.StatusSeverity.Error + }); + } + + return RedirectToAction("ListPlugins"); + } + + + [HttpPost("server/plugins/upload")] + public async Task UploadPlugin([FromServices] PluginService pluginService, + List files) + { + foreach (var formFile in files.Where(file => file.Length > 0)) + { + await pluginService.UploadPlugin(formFile); + pluginService.InstallPlugin(formFile.FileName.TrimEnd(PluginManager.BTCPayPluginSuffix, + StringComparison.InvariantCultureIgnoreCase)); + } + + return RedirectToAction("ListPlugins", + new {StatusMessage = "Files uploaded, restart server to load plugins"}); + } + } +} diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index c84da82aa..4ef2bf160 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -224,15 +224,7 @@ namespace BTCPayServer return false; } - public static void SetStatusMessageModel(this ITempDataDictionary tempData, StatusMessageModel statusMessage) - { - if (statusMessage == null) - { - tempData.Remove("StatusMessageModel"); - return; - } - tempData["StatusMessageModel"] = JObject.FromObject(statusMessage).ToString(Formatting.None); - } + public static StatusMessageModel GetStatusMessageModel(this ITempDataDictionary tempData) { diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index aba0809e5..6b34bece1 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -12,6 +12,7 @@ using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.PayJoin; +using BTCPayServer.Plugins; using BTCPayServer.Security; using BTCPayServer.Security.Bitpay; using BTCPayServer.Security.GreenField; @@ -83,6 +84,7 @@ namespace BTCPayServer.Hosting services.AddEthereumLike(); #endif services.TryAddSingleton(); + services.TryAddSingleton(provider => provider.GetService()); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); @@ -146,7 +148,7 @@ namespace BTCPayServer.Hosting }); services.TryAddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.TryAddTransient(); services.TryAddSingleton(o => { diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index 45e48de00..211ca2b60 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -6,6 +6,7 @@ using BTCPayServer.Data; using BTCPayServer.Filters; using BTCPayServer.Logging; using BTCPayServer.PaymentRequest; +using BTCPayServer.Plugins; using BTCPayServer.Security; using BTCPayServer.Services.Apps; using BTCPayServer.Storage; @@ -91,7 +92,7 @@ namespace BTCPayServer.Hosting #if RAZOR_RUNTIME_COMPILE .AddRazorRuntimeCompilation() #endif - .AddExtensions(services, Configuration, LoggerFactory) + .AddPlugins(services, Configuration, LoggerFactory) .AddControllersAsServices(); @@ -178,7 +179,7 @@ namespace BTCPayServer.Hosting private static void ConfigureCore(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider prov, ILoggerFactory loggerFactory, BTCPayServerOptions options) { Logs.Configure(loggerFactory); - app.UseExtensions(); + app.UsePlugins(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); diff --git a/BTCPayServer/Plugins/PluginManager.cs b/BTCPayServer/Plugins/PluginManager.cs new file mode 100644 index 000000000..14a23bb19 --- /dev/null +++ b/BTCPayServer/Plugins/PluginManager.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using BTCPayServer.Configuration; +using BTCPayServer.Contracts; +using McMaster.NETCore.Plugins; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; + +namespace BTCPayServer.Plugins +{ + public static class PluginManager + { + public const string BTCPayPluginSuffix = ".btcpay"; + private static readonly List _pluginAssemblies = new List(); + private static ILogger _logger; + + public static IMvcBuilder AddPlugins(this IMvcBuilder mvcBuilder, IServiceCollection serviceCollection, + IConfiguration config, ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(typeof(PluginManager)); + var pluginsFolder = config.GetPluginDir(DefaultConfiguration.GetNetworkType(config)); + var plugins = new List(); + + _logger.LogInformation($"Loading plugins from {pluginsFolder}"); + Directory.CreateDirectory(pluginsFolder); + ExecuteCommands(pluginsFolder); + List<(PluginLoader, Assembly, IFileProvider)> loadedPlugins = new List<(PluginLoader, Assembly, IFileProvider)>(); + var systemExtensions = GetDefaultLoadedPluginAssemblies(); + plugins.AddRange(systemExtensions.SelectMany(assembly => + GetAllPluginTypesFromAssembly(assembly).Select(GetPluginInstanceFromType))); + foreach (IBTCPayServerPlugin btcPayServerExtension in plugins) + { + btcPayServerExtension.SystemPlugin = true; + } + foreach (var dir in Directory.GetDirectories(pluginsFolder)) + { + var pluginName = Path.GetFileName(dir); + + var plugin = PluginLoader.CreateFromAssemblyFile( + Path.Combine(dir, pluginName + ".dll"), // create a plugin from for the .dll file + config => + // this ensures that the version of MVC is shared between this app and the plugin + config.PreferSharedTypes = true); + + mvcBuilder.AddPluginLoader(plugin); + var pluginAssembly = plugin.LoadDefaultAssembly(); + _pluginAssemblies.Add(pluginAssembly); + var fileProvider = CreateEmbeddedFileProviderForAssembly(pluginAssembly); + loadedPlugins.Add((plugin, pluginAssembly, fileProvider)); + plugins.AddRange(GetAllPluginTypesFromAssembly(pluginAssembly) + .Select(GetPluginInstanceFromType)); + } + + foreach (var plugin in plugins) + { + try + { + _logger.LogInformation( + $"Adding and executing plugin {plugin.Identifier} - {plugin.Version}"); + plugin.Execute(serviceCollection); + serviceCollection.AddSingleton(plugin); + } + catch (Exception e) + { + _logger.LogError($"Error when loading plugin {plugin.Identifier} - {plugin.Version}{Environment.NewLine}{e.Message}"); + } + } + + return mvcBuilder; + } + + public static void UsePlugins(this IApplicationBuilder applicationBuilder) + { + foreach (var extension in applicationBuilder.ApplicationServices + .GetServices()) + { + extension.Execute(applicationBuilder, + applicationBuilder.ApplicationServices); + } + + var webHostEnvironment = applicationBuilder.ApplicationServices.GetService(); + List providers = new List() {webHostEnvironment.WebRootFileProvider}; + providers.AddRange( + _pluginAssemblies + .Select(CreateEmbeddedFileProviderForAssembly)); + webHostEnvironment.WebRootFileProvider = new CompositeFileProvider(providers); + } + private static Assembly[] GetDefaultLoadedPluginAssemblies() + { + return AppDomain.CurrentDomain.GetAssemblies().Where(assembly => + assembly?.FullName?.StartsWith("BTCPayServer.Plugins", + StringComparison.InvariantCultureIgnoreCase) is true) + .ToArray(); + } + + private static Type[] GetAllPluginTypesFromAssembly(Assembly assembly) + { + return assembly.GetTypes().Where(type => + typeof(IBTCPayServerPlugin).IsAssignableFrom(type) && + !type.IsAbstract).ToArray(); + } + + private static IBTCPayServerPlugin GetPluginInstanceFromType(Type type) + { + return (IBTCPayServerPlugin)Activator.CreateInstance(type, Array.Empty()); + } + + private static IFileProvider CreateEmbeddedFileProviderForAssembly(Assembly assembly) + { + return new EmbeddedFileProvider(assembly); + } + + private static void ExecuteCommands(string pluginsFolder) + { + var pendingCommands = GetPendingCommands(pluginsFolder); + foreach (var command in pendingCommands) + { + ExecuteCommand(command, pluginsFolder); + } + + File.Delete(Path.Combine(pluginsFolder, "commands")); + } + + private static void ExecuteCommand((string command, string extension) command, string pluginsFolder) + { + var dirName = Path.Combine(pluginsFolder, command.extension); + switch (command.command) + { + case "delete": + if (Directory.Exists(dirName)) + { + Directory.Delete(dirName, true); + } + + break; + case "install": + var fileName = dirName + BTCPayPluginSuffix; + if (File.Exists(fileName)) + { + ZipFile.ExtractToDirectory(fileName, dirName, true); + File.Delete(fileName); + } + + break; + } + } + + public static (string command, string plugin)[] GetPendingCommands(string pluginsFolder) + { + if (!File.Exists(Path.Combine(pluginsFolder, "commands"))) + return Array.Empty<(string command, string plugin)>(); + var commands = File.ReadAllLines(Path.Combine(pluginsFolder, "commands")); + return commands.Select(s => + { + var split = s.Split(':'); + return (split[0].ToLower(CultureInfo.InvariantCulture), split[1]); + }).ToArray(); + } + + public static void QueueCommands(string pluginsFolder, params ( string action, string plugin)[] commands) + { + File.AppendAllLines(Path.Combine(pluginsFolder, "commands"), + commands.Select((tuple) => $"{tuple.action}:{tuple.plugin}")); + } + + public static void CancelCommands(string pluginDir, string plugin) + { + var cmds = GetPendingCommands(pluginDir).Where(tuple => + !tuple.plugin.Equals(plugin, StringComparison.InvariantCultureIgnoreCase)).ToArray(); + + File.Delete(Path.Combine(pluginDir, "commands")); + QueueCommands(pluginDir, cmds); + } + } +} diff --git a/BTCPayServer/BTCPayExtensions/ExtensionService.cs b/BTCPayServer/Plugins/PluginService.cs similarity index 60% rename from BTCPayServer/BTCPayExtensions/ExtensionService.cs rename to BTCPayServer/Plugins/PluginService.cs index 1757bcab8..6ee57f148 100644 --- a/BTCPayServer/BTCPayExtensions/ExtensionService.cs +++ b/BTCPayServer/Plugins/PluginService.cs @@ -13,46 +13,46 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; -namespace BTCPayServer +namespace BTCPayServer.Plugins { - public class ExtensionService + public class PluginService { private readonly BTCPayServerOptions _btcPayServerOptions; private readonly HttpClient _githubClient; - public ExtensionService(IEnumerable btcPayServerExtensions, + public PluginService(IEnumerable btcPayServerPlugins, IHttpClientFactory httpClientFactory, BTCPayServerOptions btcPayServerOptions) { - LoadedExtensions = btcPayServerExtensions; + LoadedPlugins = btcPayServerPlugins; _githubClient = httpClientFactory.CreateClient(); _githubClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("btcpayserver", "1")); _btcPayServerOptions = btcPayServerOptions; } - public IEnumerable LoadedExtensions { get; } + public IEnumerable LoadedPlugins { get; } - public async Task> GetRemoteExtensions(string remote) + public async Task> GetRemotePlugins(string remote) { var resp = await _githubClient .GetStringAsync(new Uri($"https://api.github.com/repos/{remote}/contents")); var files = JsonConvert.DeserializeObject(resp); - return await Task.WhenAll(files.Where(file => file.Name.EndsWith($"{ExtensionManager.BTCPayExtensionSuffix}.json", StringComparison.InvariantCulture)).Select(async file => + return await Task.WhenAll(files.Where(file => file.Name.EndsWith($"{PluginManager.BTCPayPluginSuffix}.json", StringComparison.InvariantCulture)).Select(async file => { return await _githubClient.GetStringAsync(file.DownloadUrl).ContinueWith( - task => JsonConvert.DeserializeObject(task.Result), TaskScheduler.Current); + task => JsonConvert.DeserializeObject(task.Result), TaskScheduler.Current); })); } - public async Task DownloadRemoteExtension(string remote, string extension) + public async Task DownloadRemotePlugin(string remote, string plugin) { - var dest = _btcPayServerOptions.ExtensionDir; + var dest = _btcPayServerOptions.PluginDir; var resp = await _githubClient .GetStringAsync(new Uri($"https://api.github.com/repos/{remote}/contents")); var files = JsonConvert.DeserializeObject(resp); - var ext = files.SingleOrDefault(file => file.Name == $"{extension}{ExtensionManager.BTCPayExtensionSuffix}"); + var ext = files.SingleOrDefault(file => file.Name == $"{plugin}{PluginManager.BTCPayPluginSuffix}"); if (ext is null) { - throw new Exception("Extension not found on remote"); + throw new Exception("Plugin not found on remote"); } var filedest = Path.Combine(dest, ext.Name); @@ -60,37 +60,40 @@ namespace BTCPayServer new WebClient().DownloadFile(new Uri(ext.DownloadUrl), filedest); } - public void InstallExtension(string extension) + public void InstallPlugin(string plugin) { - var dest = _btcPayServerOptions.ExtensionDir; - UninstallExtension(extension); - ExtensionManager.QueueCommands(dest, ("install", extension)); + var dest = _btcPayServerOptions.PluginDir; + UninstallPlugin(plugin); + PluginManager.QueueCommands(dest, ("install", plugin)); } - public async Task UploadExtension(IFormFile extension) + public async Task UploadPlugin(IFormFile plugin) { - var dest = _btcPayServerOptions.ExtensionDir; - var filedest = Path.Combine(dest, extension.FileName); + var dest = _btcPayServerOptions.PluginDir; + var filedest = Path.Combine(dest, plugin.FileName); Directory.CreateDirectory(Path.GetDirectoryName(filedest)); - if (Path.GetExtension(filedest) == ExtensionManager.BTCPayExtensionSuffix) + if (Path.GetExtension(filedest) == PluginManager.BTCPayPluginSuffix) { await using var stream = new FileStream(filedest, FileMode.Create); - await extension.CopyToAsync(stream); + await plugin.CopyToAsync(stream); } } - public void UninstallExtension(string extension) + public void UninstallPlugin(string plugin) { - var dest = _btcPayServerOptions.ExtensionDir; - ExtensionManager.QueueCommands(dest, ("delete", extension)); + var dest = _btcPayServerOptions.PluginDir; + PluginManager.QueueCommands(dest, ("delete", plugin)); } - public class AvailableExtension : IBTCPayServerExtension + public class AvailablePlugin : IBTCPayServerPlugin { public string Identifier { get; set; } public string Name { get; set; } public Version Version { get; set; } public string Description { get; set; } + public bool SystemPlugin { get; set; } = false; + + public string[] Dependencies { get; } = Array.Empty(); public void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices) @@ -111,14 +114,14 @@ namespace BTCPayServer [JsonProperty("download_url")] public string DownloadUrl { get; set; } } - public (string command, string extension)[] GetPendingCommands() + public (string command, string plugin)[] GetPendingCommands() { - return ExtensionManager.GetPendingCommands(_btcPayServerOptions.ExtensionDir); + return PluginManager.GetPendingCommands(_btcPayServerOptions.PluginDir); } - public void CancelCommands(string extension) + public void CancelCommands(string plugin) { - ExtensionManager.CancelCommands(_btcPayServerOptions.ExtensionDir, extension); + PluginManager.CancelCommands(_btcPayServerOptions.PluginDir, plugin); } } } diff --git a/BTCPayServer/Services/Altcoins/Ethereum/EthereumLikeExtensions.cs b/BTCPayServer/Services/Altcoins/Ethereum/EthereumLikeExtensions.cs index 781747a40..2ba83d680 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/EthereumLikeExtensions.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/EthereumLikeExtensions.cs @@ -21,7 +21,8 @@ namespace BTCPayServer.Services.Altcoins.Ethereum serviceCollection.AddSingleton(provider => provider.GetService()); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(provider => provider.GetService()); - serviceCollection.AddSingleton(); + + serviceCollection.AddSingleton(new UIExtension("Ethereum/StoreNavEthereumExtension", "store-nav")); serviceCollection.AddTransient(); serviceCollection.AddSingleton(); serviceCollection.AddHttpClient(EthereumInvoiceCreateHttpClient) diff --git a/BTCPayServer/Services/Altcoins/Ethereum/EthereumStoreNavExtension.cs b/BTCPayServer/Services/Altcoins/Ethereum/EthereumStoreNavExtension.cs deleted file mode 100644 index e148d4d11..000000000 --- a/BTCPayServer/Services/Altcoins/Ethereum/EthereumStoreNavExtension.cs +++ /dev/null @@ -1,12 +0,0 @@ -#if ALTCOINS -using BTCPayServer.Contracts; - -namespace BTCPayServer.Services.Altcoins.Ethereum -{ - public class EthereumNavExtension: INavExtension - { - public string Partial { get; } = "Ethereum/StoreNavEthereumExtension"; - public string Location { get; } = "store"; - } -} -#endif diff --git a/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs b/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs index 4be1c4a08..b87841183 100644 --- a/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs +++ b/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs @@ -23,7 +23,7 @@ namespace BTCPayServer.Services.Altcoins.Monero serviceCollection.AddHostedService(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(provider => provider.GetService()); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(new UIExtension("Monero/StoreNavMoneroExtension", "store-nav")); serviceCollection.AddSingleton(); return serviceCollection; diff --git a/BTCPayServer/Services/Altcoins/Monero/MoneroStoreNavExtension.cs b/BTCPayServer/Services/Altcoins/Monero/MoneroStoreNavExtension.cs deleted file mode 100644 index 8b5028191..000000000 --- a/BTCPayServer/Services/Altcoins/Monero/MoneroStoreNavExtension.cs +++ /dev/null @@ -1,12 +0,0 @@ -#if ALTCOINS -using BTCPayServer.Contracts; - -namespace BTCPayServer.Services.Altcoins.Monero -{ - public class MoneroNavExtension : INavExtension - { - public string Partial { get; } = "Monero/StoreNavMoneroExtension"; - public string Location { get; } = "store"; - } -} -#endif diff --git a/BTCPayServer/Services/SettingsRepository.cs b/BTCPayServer/Services/SettingsRepository.cs index 1f35d11d3..6e08422fd 100644 --- a/BTCPayServer/Services/SettingsRepository.cs +++ b/BTCPayServer/Services/SettingsRepository.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json; namespace BTCPayServer.Services { - public class SettingsRepository + public class SettingsRepository : ISettingsRepository { private readonly ApplicationDbContextFactory _ContextFactory; private readonly EventAggregator _EventAggregator; diff --git a/BTCPayServer/Views/Manage/_Nav.cshtml b/BTCPayServer/Views/Manage/_Nav.cshtml index 16b4f810e..294f66f3e 100644 --- a/BTCPayServer/Views/Manage/_Nav.cshtml +++ b/BTCPayServer/Views/Manage/_Nav.cshtml @@ -7,5 +7,6 @@ U2F Authentication API Keys Notifications + diff --git a/BTCPayServer/Views/Server/ListExtensions.cshtml b/BTCPayServer/Views/Server/ListExtensions.cshtml deleted file mode 100644 index 4a24a2cb8..000000000 --- a/BTCPayServer/Views/Server/ListExtensions.cshtml +++ /dev/null @@ -1,152 +0,0 @@ -@model BTCPayServer.Controllers.ServerController.ListExtensionsViewModel -@{ - ViewData.SetActivePageAndTitle(ServerNavPages.Extensions); - var installed = Model.Installed.Select(extension => extension.Identifier); - var availableAndNotInstalled = Model.Available.Where(extension => !installed.Contains(extension.Identifier)); -} - -@if (Model.Commands.Any()) -{ -
- You need to restart BTCPay Server in order to update your active extensions. - @if (Model.CanShowRestart) - { -
- -
- } -
-} - - -@if (Model.Installed.Any()) -{ -

Installed Extensions

-
- - @foreach (var extension in Model.Installed) - { - var matchedAvailable = Model.Available.SingleOrDefault(availableExtension => availableExtension.Identifier == extension.Identifier); -
-
-

- @extension.Name @extension.Version -

-

@extension.Description

- -
- @if (matchedAvailable != null) - { -
    -
  • - Current version - @extension.Version -
  • -
  • - Remote version - @matchedAvailable.Version -
  • -
- - } - -
- } -
-} -@if (availableAndNotInstalled.Any()) -{ -

Available Extensions

- -
- @foreach (var extension in availableAndNotInstalled) - { -
-
-

- @extension.Name @extension.Version -

-

@extension.Description

-
- -
- } -
-} - - -
-
-
-

Add extension manually

-
This is an extremely dangerous operation. Do not upload extensions from someone that you do not trust.
-
-
- - -
-
-
-
-
- -@section Scripts { - -} diff --git a/BTCPayServer/Views/Server/ListPlugins.cshtml b/BTCPayServer/Views/Server/ListPlugins.cshtml new file mode 100644 index 000000000..dcea80ba1 --- /dev/null +++ b/BTCPayServer/Views/Server/ListPlugins.cshtml @@ -0,0 +1,199 @@ +@model BTCPayServer.Controllers.ServerController.ListPluginsViewModel +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Plugins); + var installed = Model.Installed.Select(plugin => plugin.Identifier); + var availableAndNotInstalled = Model.Available.Where(plugin => !installed.Contains(plugin.Identifier)); +} + +@if (Model.Commands.Any()) +{ +
+ You need to restart BTCPay Server in order to update your active plugins. + @if (Model.CanShowRestart) + { +
+ +
+ } +
+} + + +@if (Model.Installed.Any()) +{ +

Installed Plugins

+
+ + @foreach (var plugin in Model.Installed) + { + var matchedAvailable = Model.Available.SingleOrDefault(availablePlugin => availablePlugin.Identifier == plugin.Identifier); +
+
+

+ @plugin.Name @plugin.Version +

+

@plugin.Description

+ +
+ @if (!plugin.SystemPlugin) + { +
    +
  • + Current version + @plugin.Version +
  • + @if (matchedAvailable != null) + { +
  • + Remote version + @matchedAvailable.Version +
  • + } +
+ + } + else if (plugin.SystemPlugin) + { + + } + +
+ } +
+} +@if (availableAndNotInstalled.Any()) +{ +

Available Plugins

+ +
+ @foreach (var plugin in availableAndNotInstalled) + { +
+
+

+ @plugin.Name @plugin.Version +

+

@plugin.Description

+
+ +
+ } +
+} + +
+
+
+

Add plugin manually

+
This is an extremely dangerous operation. Do not upload plugins from someone that you do not trust.
+
+ +
+
+ + +
+
+
+ +
+
+
+
+
+@if (Model.Commands.Any()) +{ + +
+
+
+

Pending actions

+
    + @foreach (var extComm in Model.Commands.GroupBy(tuple => tuple.plugin)) + { +
  • +
    + @extComm.Key (@extComm.Last().command) +
    + +
    +
    +
  • + } +
+
+
+
+} + +@section Scripts { + +} diff --git a/BTCPayServer/Views/Server/ServerNavPages.cs b/BTCPayServer/Views/Server/ServerNavPages.cs index e1d4d66fb..a111951cd 100644 --- a/BTCPayServer/Views/Server/ServerNavPages.cs +++ b/BTCPayServer/Views/Server/ServerNavPages.cs @@ -2,6 +2,6 @@ namespace BTCPayServer.Views.Server { public enum ServerNavPages { - Index, Users, Emails, Policies, Theme, Services, Maintenance, Logs, Files, Extensions + Index, Users, Emails, Policies, Theme, Services, Maintenance, Logs, Files, Plugins } } diff --git a/BTCPayServer/Views/Server/_Nav.cshtml b/BTCPayServer/Views/Server/_Nav.cshtml index 6b18d4db1..be18db802 100644 --- a/BTCPayServer/Views/Server/_Nav.cshtml +++ b/BTCPayServer/Views/Server/_Nav.cshtml @@ -12,5 +12,6 @@ } Logs Files - Extensions (experimental) + Plugins (experimental) + diff --git a/BTCPayServer/Views/Shared/_Layout.cshtml b/BTCPayServer/Views/Shared/_Layout.cshtml index 68397fd6f..3443820e6 100644 --- a/BTCPayServer/Views/Shared/_Layout.cshtml +++ b/BTCPayServer/Views/Shared/_Layout.cshtml @@ -40,12 +40,7 @@