diff --git a/BTCPayServer/Plugins/PluginManager.cs b/BTCPayServer/Plugins/PluginManager.cs index ea32dd72c..bf7c80125 100644 --- a/BTCPayServer/Plugins/PluginManager.cs +++ b/BTCPayServer/Plugins/PluginManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; @@ -10,7 +9,6 @@ using System.Reflection; using System.Text.RegularExpressions; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Configuration; -using BTCPayServer.Logging; using McMaster.NETCore.Plugins; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -19,14 +17,14 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; -using NBXplorer; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Plugins { public static class PluginManager { public const string BTCPayPluginSuffix = ".btcpay"; - private static readonly List _pluginAssemblies = new List(); + private static readonly List _pluginAssemblies = new (); public static bool IsExceptionByPlugin(Exception exception, [MaybeNullWhen(false)] out string pluginName) { @@ -65,6 +63,31 @@ namespace BTCPayServer.Plugins public static IMvcBuilder AddPlugins(this IMvcBuilder mvcBuilder, IServiceCollection serviceCollection, IConfiguration config, ILoggerFactory loggerFactory, ServiceProvider bootstrapServiceProvider) { + void LoadPluginsFromAssemblies(Assembly systemAssembly1, HashSet hashSet, HashSet loadedPluginIdentifiers1, + List btcPayServerPlugins) + { + // Load the referenced assembly plugins + // All referenced plugins should have at least one plugin with exact same plugin identifier + // as the assembly. Except for the system assembly (btcpayserver assembly) which are fake plugins + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + var assemblyName = assembly.GetName().Name; + bool isSystemPlugin = assembly == systemAssembly1; + if (!isSystemPlugin && hashSet.Contains(assemblyName)) + continue; + + foreach (var plugin in GetPluginInstancesFromAssembly(assembly)) + { + if (!isSystemPlugin && plugin.Identifier != assemblyName) + continue; + if (!loadedPluginIdentifiers1.Add(plugin.Identifier)) + continue; + btcPayServerPlugins.Add(plugin); + plugin.SystemPlugin = isSystemPlugin; + } + } + } + var logger = loggerFactory.CreateLogger(typeof(PluginManager)); var pluginsFolder = new DataDirectories().Configure(config).PluginDir; var plugins = new List(); @@ -80,25 +103,12 @@ namespace BTCPayServer.Plugins var disabledPlugins = GetDisabledPlugins(pluginsFolder); var systemAssembly = typeof(Program).Assembly; - // Load the referenced assembly plugins - // All referenced plugins should have at least one plugin with exact same plugin identifier - // as the assembly. Except for the system assembly (btcpayserver assembly) which are fake plugins - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - var assemblyName = assembly.GetName().Name; - bool isSystemPlugin = assembly == systemAssembly; - if (!isSystemPlugin && disabledPlugins.Contains(assemblyName)) - continue; + LoadPluginsFromAssemblies(systemAssembly, disabledPlugins, loadedPluginIdentifiers, plugins); - foreach (var plugin in GetPluginInstancesFromAssembly(assembly)) - { - if (!isSystemPlugin && plugin.Identifier != assemblyName) - continue; - if (!loadedPluginIdentifiers.Add(plugin.Identifier)) - continue; - plugins.Add(plugin); - plugin.SystemPlugin = isSystemPlugin; - } + if (ExecuteCommands(pluginsFolder, plugins.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version))) + { + plugins.Clear(); + LoadPluginsFromAssemblies(systemAssembly, disabledPlugins, loadedPluginIdentifiers, plugins); } var pluginsToLoad = new List<(string PluginIdentifier, string PluginFilePath)>(); @@ -244,37 +254,56 @@ namespace BTCPayServer.Plugins !type.IsAbstract). Select(type => (IBTCPayServerPlugin)Activator.CreateInstance(type, Array.Empty())); } + private static IBTCPayServerPlugin GetPluginInstanceFromAssembly(string pluginIdentifier, Assembly assembly) { - return GetPluginInstancesFromAssembly(assembly) - .Where(plugin => plugin.Identifier == pluginIdentifier) - .FirstOrDefault(); + return GetPluginInstancesFromAssembly(assembly).FirstOrDefault(plugin => plugin.Identifier == pluginIdentifier); } - private static void ExecuteCommands(string pluginsFolder) + private static bool ExecuteCommands(string pluginsFolder, Dictionary installed = null) { var pendingCommands = GetPendingCommands(pluginsFolder); - foreach (var command in pendingCommands) + if (!pendingCommands.Any()) { - ExecuteCommand(command, pluginsFolder); + return false; } - File.Delete(Path.Combine(pluginsFolder, "commands")); + var remainingCommands = (from command in pendingCommands where !ExecuteCommand(command, pluginsFolder, false, installed) select $"{command.command}:{command.plugin}").ToList(); + if (remainingCommands.Any()) + { + File.WriteAllLines(Path.Combine(pluginsFolder, "commands"), remainingCommands); + } + else + { + File.Delete(Path.Combine(pluginsFolder, "commands")); + } + + return remainingCommands.Count != pendingCommands.Length; } - private static void ExecuteCommand((string command, string extension) command, string pluginsFolder, - bool ignoreOrder = false) + private static bool DependenciesMet(string pluginsFolder, string plugin, Dictionary installed) + { + var dirName = Path.Combine(pluginsFolder, plugin); + var manifestFileName = dirName + ".json"; + if (!File.Exists(manifestFileName)) return true; + var pluginManifest = JObject.Parse(File.ReadAllText(manifestFileName)).ToObject(); + return DependenciesMet(pluginManifest.Dependencies, installed); + } + + private static bool ExecuteCommand((string command, string extension) command, string pluginsFolder, + bool ignoreOrder = false, Dictionary installed = null) { var dirName = Path.Combine(pluginsFolder, command.extension); switch (command.command) { case "update": - ExecuteCommand(("enable", command.extension), pluginsFolder, true); + if (!DependenciesMet(pluginsFolder, command.extension, installed)) + return false; ExecuteCommand(("delete", command.extension), pluginsFolder, true); ExecuteCommand(("install", command.extension), pluginsFolder, true); break; - case "delete": + case "delete": ExecuteCommand(("enable", command.extension), pluginsFolder, true); if (File.Exists(dirName)) { @@ -290,11 +319,15 @@ namespace BTCPayServer.Plugins orders.Where(s => s != command.extension)); } } - break; + case "install": - ExecuteCommand(("enable", command.extension), pluginsFolder, true); var fileName = dirName + BTCPayPluginSuffix; + var manifestFileName = dirName + ".json"; + if (!DependenciesMet(pluginsFolder, command.extension, installed)) + return false; + + ExecuteCommand(("enable", command.extension), pluginsFolder, true); if (File.Exists(fileName)) { ZipFile.ExtractToDirectory(fileName, dirName, true); @@ -302,10 +335,12 @@ namespace BTCPayServer.Plugins { File.AppendAllLines(Path.Combine(pluginsFolder, "order"), new[] { command.extension }); } - File.Delete(fileName); + if (File.Exists(manifestFileName)) + { + File.Move(manifestFileName, Path.Combine(dirName, Path.GetFileName(manifestFileName))); + } } - break; case "disable": @@ -339,6 +374,8 @@ namespace BTCPayServer.Plugins break; } + + return true; } public static (string command, string plugin)[] GetPendingCommands(string pluginsFolder) @@ -364,25 +401,84 @@ namespace BTCPayServer.Plugins var cmds = GetPendingCommands(pluginDir).Where(tuple => !tuple.plugin.Equals(plugin, StringComparison.InvariantCultureIgnoreCase)).ToArray(); + if (File.Exists(Path.Combine(pluginDir, plugin, BTCPayPluginSuffix))) + { + File.Delete(Path.Combine(pluginDir, plugin, BTCPayPluginSuffix)); + } + if (File.Exists(Path.Combine(pluginDir, plugin, ".json"))) + { + File.Delete(Path.Combine(pluginDir, plugin, ".json")); + } File.Delete(Path.Combine(pluginDir, "commands")); QueueCommands(pluginDir, cmds); } public static void DisablePlugin(string pluginDir, string plugin) { - QueueCommands(pluginDir, ("disable", plugin)); } public static HashSet GetDisabledPlugins(string pluginsFolder) { var disabledFilePath = Path.Combine(pluginsFolder, "disabled"); - if (File.Exists(disabledFilePath)) + return File.Exists(disabledFilePath) + ? File.ReadLines(disabledFilePath).ToHashSet() + : []; + } + + public static bool DependencyMet(IBTCPayServerPlugin.PluginDependency dependency, + Dictionary installed = null) + { + var plugin = dependency.Identifier.ToLowerInvariant(); + var versionReq = dependency.Condition; + // ensure installed is not null and has lowercased keys for comparison + installed = installed == null + ? new Dictionary() + : installed.ToDictionary(x => x.Key.ToLowerInvariant(), x => x.Value); + if (!installed.ContainsKey(plugin) && !versionReq.Equals("!")) { - return File.ReadLines(disabledFilePath).ToHashSet(); + return false; } - return new HashSet(); + var versionConditions = versionReq.Split("||", StringSplitOptions.RemoveEmptyEntries); + return versionConditions.Any(s => + { + s = s.Trim(); + var v = s.Substring(1); + if (s[1] == '=') + { + v = s.Substring(2); + } + + var parsedV = Version.Parse(v); + switch (s) + { + case { } xx when xx.StartsWith(">="): + return installed[plugin] >= parsedV; + case { } xx when xx.StartsWith("<="): + return installed[plugin] <= parsedV; + case { } xx when xx.StartsWith(">"): + return installed[plugin] > parsedV; + case { } xx when xx.StartsWith("<"): + return installed[plugin] >= parsedV; + case { } xx when xx.StartsWith("^"): + return installed[plugin] >= parsedV && installed[plugin].Major == parsedV.Major; + case { } xx when xx.StartsWith("~"): + return installed[plugin] >= parsedV && installed[plugin].Major == parsedV.Major && + installed[plugin].Minor == parsedV.Minor; + case { } xx when xx.StartsWith("!="): + return installed[plugin] != parsedV; + case { } xx when xx.StartsWith("=="): + default: + return installed[plugin] == parsedV; + } + }); + } + + public static bool DependenciesMet(IEnumerable dependencies, + Dictionary installed = null) + { + return dependencies.All(dependency => DependencyMet(dependency, installed)); } } } diff --git a/BTCPayServer/Plugins/PluginService.cs b/BTCPayServer/Plugins/PluginService.cs index 4b0ff6ef6..bc45db44d 100644 --- a/BTCPayServer/Plugins/PluginService.cs +++ b/BTCPayServer/Plugins/PluginService.cs @@ -2,22 +2,14 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Text.RegularExpressions; using System.Threading.Tasks; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Configuration; -using BTCPayServer.Lightning.CLightning; using BTCPayServer.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.Options; -using NBitcoin.DataEncoders; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -27,10 +19,8 @@ namespace BTCPayServer.Plugins { private readonly IOptions _dataDirectories; private readonly PoliciesSettings _policiesSettings; - private readonly ISettingsRepository _settingsRepository; private readonly PluginBuilderClient _pluginBuilderClient; public PluginService( - ISettingsRepository settingsRepository, IEnumerable btcPayServerPlugins, PluginBuilderClient pluginBuilderClient, IOptions dataDirectories, @@ -39,7 +29,6 @@ namespace BTCPayServer.Plugins { LoadedPlugins = btcPayServerPlugins; _pluginBuilderClient = pluginBuilderClient; - _settingsRepository = settingsRepository; _dataDirectories = dataDirectories; _policiesSettings = policiesSettings; Env = env; @@ -47,6 +36,15 @@ namespace BTCPayServer.Plugins public IEnumerable LoadedPlugins { get; } public BTCPayServerEnvironment Env { get; } + + public Version? GetVersionOfPendingInstall(string plugin) + { + var dirName = Path.Combine(_dataDirectories.Value.PluginDir, plugin); + var manifestFileName = dirName + ".json"; + if (!File.Exists(manifestFileName)) return null; + var pluginManifest = JObject.Parse(File.ReadAllText(manifestFileName)).ToObject(); + return pluginManifest.Version; + } public async Task GetRemotePlugins() { @@ -66,14 +64,18 @@ namespace BTCPayServer.Plugins return p; }).ToArray(); } + public async Task DownloadRemotePlugin(string pluginIdentifier, string version) { var dest = _dataDirectories.Value.PluginDir; var filedest = Path.Join(dest, pluginIdentifier + ".btcpay"); + var filemanifestdest = Path.Join(dest, pluginIdentifier + ".json"); Directory.CreateDirectory(Path.GetDirectoryName(filedest)); var url = $"api/v1/plugins/[{Uri.EscapeDataString(pluginIdentifier)}]/versions/{Uri.EscapeDataString(version)}/download"; + var manifest = (await _pluginBuilderClient.GetPublishedVersions(null, true)).Select(v => v.ManifestInfo.ToObject()).FirstOrDefault(p => p.Identifier == pluginIdentifier); + await File.WriteAllTextAsync(filemanifestdest, JsonConvert.SerializeObject(manifest, Formatting.Indented)); using var resp2 = await _pluginBuilderClient.HttpClient.GetAsync(url); - using var fs = new FileStream(filedest, FileMode.Create, FileAccess.ReadWrite); + await using var fs = new FileStream(filedest, FileMode.Create, FileAccess.ReadWrite); await resp2.Content.CopyToAsync(fs); await fs.FlushAsync(); } @@ -84,6 +86,7 @@ namespace BTCPayServer.Plugins UninstallPlugin(plugin); PluginManager.QueueCommands(dest, ("install", plugin)); } + public void UpdatePlugin(string plugin) { var dest = _dataDirectories.Value.PluginDir; @@ -122,8 +125,7 @@ namespace BTCPayServer.Plugins public string Author { get; set; } public string AuthorLink { get; set; } - public void Execute(IApplicationBuilder applicationBuilder, - IServiceProvider applicationBuilderApplicationServices) + public void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices) { } diff --git a/BTCPayServer/Views/UIServer/ListPlugins.cshtml b/BTCPayServer/Views/UIServer/ListPlugins.cshtml index 16b4432b1..46ac770c5 100644 --- a/BTCPayServer/Views/UIServer/ListPlugins.cshtml +++ b/BTCPayServer/Views/UIServer/ListPlugins.cshtml @@ -1,15 +1,15 @@ @using BTCPayServer.Configuration @using BTCPayServer.Plugins -@using BTCPayServer.Abstractions.Contracts @model BTCPayServer.Controllers.UIServerController.ListPluginsViewModel @inject BTCPayServerOptions BTCPayServerOptions +@inject PluginService PluginService @{ Layout = "_Layout"; ViewData.SetActivePage(ServerNavPages.Plugins); - var installed = Model.Installed.ToDictionary(plugin => plugin.Identifier.ToLowerInvariant(), plugin => plugin.Version); + var installed = Model.Installed.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version); var availableAndNotInstalledx = Model.Available - .Where(plugin => !installed.ContainsKey(plugin.Identifier.ToLowerInvariant())) + .Where(plugin => !installed.ContainsKey(plugin.Identifier)) .GroupBy(plugin => plugin.Identifier) .ToList(); @@ -17,7 +17,7 @@ foreach (var availableAndNotInstalledItem in availableAndNotInstalledx) { var ordered = availableAndNotInstalledItem.OrderByDescending(plugin => plugin.Version).ToArray(); - availableAndNotInstalled.Add(ordered.FirstOrDefault(availablePlugin => DependenciesMet(availablePlugin.Dependencies)) ?? ordered.FirstOrDefault()); + availableAndNotInstalled.Add(ordered.FirstOrDefault(availablePlugin => PluginManager.DependenciesMet(availablePlugin.Dependencies, installed)) ?? ordered.FirstOrDefault()); } bool DependentOn(string plugin) @@ -41,74 +41,11 @@ } return false; } - - bool DependencyMet(IBTCPayServerPlugin.PluginDependency dependency) - { - var plugin = dependency.Identifier.ToLowerInvariant(); - var versionReq = dependency.Condition; - if (!installed.ContainsKey(plugin) && !versionReq.Equals("!")) - { - return false; - } - if (installed.ContainsKey(plugin) && versionReq.Equals("!")) - { - return false; - } - - var versionConditions = versionReq.Split("||", StringSplitOptions.RemoveEmptyEntries); - return versionConditions.Any(s => - { - s = s.Trim(); - var v = s.Substring(1); - if (s[1] == '=') - { - v = s.Substring(2); - } - var parsedV = Version.Parse(v); - switch (s) - { - case { } xx when xx.StartsWith(">="): - return installed[plugin] >= parsedV; - case { } xx when xx.StartsWith("<="): - return installed[plugin] <= parsedV; - case { } xx when xx.StartsWith(">"): - return installed[plugin] > parsedV; - case { } xx when xx.StartsWith("<"): - return installed[plugin] >= parsedV; - case { } xx when xx.StartsWith("^"): - return installed[plugin] >= parsedV && installed[plugin].Major == parsedV.Major; - case { } xx when xx.StartsWith("~"): - return installed[plugin] >= parsedV && installed[plugin].Major == parsedV.Major && installed[plugin].Minor == parsedV.Minor; - case { } xx when xx.StartsWith("!="): - return installed[plugin] != parsedV; - case { } xx when xx.StartsWith("=="): - default: - return installed[plugin] == parsedV; - } - }); - } - - bool DependenciesMet(IBTCPayServerPlugin.PluginDependency[] dependencies) - { - foreach (var dependency in dependencies) - { - if (!DependencyMet(dependency)) - { - return false; - } - } - return true; - } } @@ -157,7 +94,7 @@ { Model.DownloadedPluginsByIdentifier.TryGetValue(plugin.Identifier, out var downloadInfo); var matchedAvailable = Model.Available.Where(availablePlugin => availablePlugin.Identifier == plugin.Identifier && availablePlugin.Version > plugin.Version).OrderByDescending(availablePlugin => availablePlugin.Version).ToArray(); - var x = matchedAvailable.FirstOrDefault(availablePlugin => DependenciesMet(availablePlugin.Dependencies)) ?? matchedAvailable.FirstOrDefault(); + var x = matchedAvailable.FirstOrDefault(availablePlugin => PluginManager.DependenciesMet(availablePlugin.Dependencies, installed)) ?? matchedAvailable.FirstOrDefault(); var updateAvailable = matchedAvailable.Any(); var tabId = plugin.Identifier.ToLowerInvariant().Replace(".", "_");
@@ -205,7 +142,7 @@ {
  • @dependency - @if (!DependencyMet(dependency)) + @if (!PluginManager.DependencyMet(dependency, installed)) { @@ -229,7 +166,7 @@ {
  • @dependency - @if (!DependencyMet(dependency)) + @if (!PluginManager.DependencyMet(dependency, installed)) { @@ -280,22 +217,41 @@ @{ var pendingAction = Model.Commands.Any(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase)); } - @if (pendingAction || (updateAvailable && x != null && DependenciesMet(x.Dependencies)) || !DependentOn(plugin.Identifier)) + @if (pendingAction || (updateAvailable && x != null && !DependentOn(plugin.Identifier))) { -
  • @dependency - @if (!DependencyMet(dependency)) + @if (!PluginManager.DependencyMet(dependency, installed)) { @@ -369,6 +325,12 @@
  • } + if(!PluginManager.DependenciesMet(plugin.Dependencies, installed)) + { +
    + Dependencies not met. +
    + } } @if (plugin != null) @@ -405,32 +367,37 @@ }
    -