btcpayserver/BTCPayServer/Views/UIServer/ListPlugins.cshtml
2023-02-13 09:25:24 +01:00

511 lines
26 KiB
Plaintext

@using BTCPayServer.Configuration
@using BTCPayServer.Plugins
@using BTCPayServer.Abstractions.Contracts
@model BTCPayServer.Controllers.UIServerController.ListPluginsViewModel
@inject BTCPayServerOptions BTCPayServerOptions
@{
Layout = "_Layout";
ViewData.SetActivePage(ServerNavPages.Plugins);
var installed = Model.Installed.ToDictionary(plugin => plugin.Identifier.ToLowerInvariant(), plugin => plugin.Version);
var availableAndNotInstalledx = Model.Available
.Where(plugin => !installed.ContainsKey(plugin.Identifier.ToLowerInvariant()))
.GroupBy(plugin => plugin.Identifier)
.ToList();
var availableAndNotInstalled = new List<PluginService.AvailablePlugin>();
foreach (var availableAndNotInstalledItem in availableAndNotInstalledx)
{
var ordered = availableAndNotInstalledItem.OrderByDescending(plugin => plugin.Version).ToArray();
availableAndNotInstalled.Add(ordered.FirstOrDefault(availablePlugin => DependenciesMet(availablePlugin.Dependencies)) ?? ordered.FirstOrDefault());
}
bool DependentOn(string plugin)
{
foreach (var installedPlugin in Model.Installed)
{
if (installedPlugin.Dependencies.Any(dep => dep.Identifier.Equals(plugin, StringComparison.InvariantCultureIgnoreCase)))
{
return true;
}
}
var pendingInstalls = Model.Commands.Where(tuple => tuple.command != "uninstall").Select(tuple => tuple.plugin).Distinct();
foreach (var pendingInstall in pendingInstalls)
{
if (Model.Available.Any(availablePlugin => availablePlugin.Identifier.Equals(pendingInstall, StringComparison.InvariantCultureIgnoreCase) &&
availablePlugin.Dependencies.Any(dep => dep.Identifier.Equals(plugin, StringComparison.InvariantCultureIgnoreCase))))
{
return true;
}
}
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;
}
}
<style>
.version-switch .nav-link {
display: inline;
}
.version-switch .nav-link.active {
display: none;
}
</style>
<partial name="_StatusMessage" />
@if (Model.Disabled.Any())
{
<div class="alert alert-danger mb-4 d-flex align-items-center justify-content-between">
Some plugins were disabled due to fatal errors. They may be incompatible with this version of BTCPay Server.
<button class="btn btn-danger" data-bs-toggle="collapse" data-bs-target="#disabled-plugins">View disabled plugins</button>
</div>
}
@if (Model.Commands.Any())
{
<div class="alert alert-info mb-4 d-flex align-items-center justify-content-between">
You need to restart BTCPay Server in order to update your active plugins.
@if (Model.CanShowRestart)
{
<form method="post" asp-action="Maintenance" class="mt-2">
<button type="submit" name="command" value="soft-restart" class="btn btn-info" asp-action="Maintenance">Restart now</button>
</form>
}
</div>
}
@if (Model.Installed.Any())
{
<h3 class="mb-4">Installed Plugins</h3>
<div class="row mb-4">
@foreach (var plugin in Model.Installed.Where(i => !i.SystemPlugin))
{
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 updateAvailable = matchedAvailable.Any();
var tabId = plugin.Identifier.ToLowerInvariant().Replace(".", "_");
<div class="col col-12 col-md-6 col-lg-12 col-xl-6 col-xxl-4 mb-4">
<div class="card h-100" id="@plugin.Identifier">
<div class="card-body">
<div class="d-flex flex-wrap align-items-baseline justify-content-between gap-2 mb-3">
<h4 class="card-title mb-0" data-bs-toggle="tooltip" title="@plugin.Identifier">@plugin.Name</h4>
@if (!string.IsNullOrEmpty(downloadInfo?.Author))
{
<span class="text-muted text-nowrap">
by
<a href="@downloadInfo.AuthorLink" rel="noreferrer noopener" target="_blank">
@downloadInfo.Author
</a>
</span>
}
</div>
<div class="d-flex flex-wrap align-items-center mb-2 gap-3">
<h5 class="text-muted d-flex align-items-center mt-1 gap-3">
@plugin.Version
@if (updateAvailable && x != null)
{
<div class="badge bg-info">
@x.Version available
</div>
}
</h5>
@if (updateAvailable)
{
<span class="nav version-switch mt-n1" role="tablist">
<a data-bs-toggle="tab" href="#@tabId-current" class="nav-link text-info p-0 show active">Show current info</a>
<a data-bs-toggle="tab" href="#@tabId-update" class="nav-link text-info p-0">Show update info</a>
</span>
}
</div>
<div class="tab-content">
<div class="tab-pane active" id="@tabId-current">
<p class="card-text">@plugin.Description</p>
@if (plugin.Dependencies.Any())
{
<h6 class="text-muted fw-semibold">Dependencies</h6>
<ul class="list-group list-group-flush">
@foreach (var dependency in plugin.Dependencies)
{
<li class="list-group-item p-2 d-inline-flex align-items-center gap-2">
@dependency
@if (!DependencyMet(dependency))
{
<span title="Dependency not met." data-bs-toggle="tooltip" class="text-danger">
<vc:icon symbol="warning" />
</span>
}
</li>
}
</ul>
}
</div>
@if (updateAvailable && x != null)
{
<div class="tab-pane" id="@tabId-update">
<p class="card-text">@x.Description</p>
@if (x.Dependencies.Any())
{
<h6 class="text-muted fw-semibold">Dependencies</h6>
<ul class="list-group list-group-flush">
@foreach (var dependency in x.Dependencies)
{
<li class="list-group-item p-2 d-inline-flex align-items-center gap-2">
@dependency
@if (!DependencyMet(dependency))
{
<span title="Dependency not met." data-bs-toggle="tooltip" class="text-danger">
<vc:icon symbol="warning" />
</span>
}
</li>
}
</ul>
}
</div>
}
@if (plugin != null)
{
<h6 class="text-muted fw-semibold mt-4">Resources</h6>
<ul class="list-group list-group-flush list-unstyled">
@if (downloadInfo?.Source is not null)
{
<li>
<a href="@downloadInfo.Source" rel="noreferrer noopener" class="d-flex align-items-center" target="_blank">
<vc:icon symbol="github" />
<span style="margin-left:.4rem">Sources</span>
</a>
</li>
}
@if (!string.IsNullOrEmpty(downloadInfo?.Documentation))
{
<li>
<a href="@downloadInfo.Documentation" rel="noreferrer noopener" class="d-flex align-items-center gap-2" target="_blank">
<vc:icon symbol="docs" />
<span>Documentation</span>
</a>
</li>
}
else
{
<li>
<span rel="noreferrer noopener" class="d-flex align-items-center gap-2 text-danger" target="_blank">
<vc:icon symbol="docs" />
<span>No documentation</span>
</span>
</li>
}
</ul>
}
</div>
</div>
@{
var pendingAction = Model.Commands.Any(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
}
@if (pendingAction || (updateAvailable && x != null && DependenciesMet(x.Dependencies)) || !DependentOn(plugin.Identifier))
{
<div class="card-footer border-0 pb-3 d-flex">
@if (pendingAction)
{
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
<button type="submit" class="btn btn-outline-secondary">Cancel pending action</button>
</form>
}
else
{
@if (updateAvailable && x != null && DependenciesMet(x.Dependencies))
{
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@x.Version" asp-route-update="true" class="me-3">
<button type="submit" class="btn btn-secondary">Update</button>
</form>
}
@if (DependentOn(plugin.Identifier))
{
<button type="button" class="btn btn-outline-danger" data-bs-toggle="tooltip" title="This plugin cannot be uninstalled as it is depended on by other plugins.">Uninstall <span class="fa fa-exclamation"></span></button>
}
else
{
<form asp-action="UnInstallPlugin" asp-route-plugin="@plugin.Identifier">
<button type="submit" class="btn btn-outline-danger">Uninstall</button>
</form>
}
}
</div>
}
</div>
</div>
}
</div>
}
@if (availableAndNotInstalled.Any())
{
<h3 class="mb-4">Available Plugins</h3>
<div class="row mb-4">
@foreach (var plugin in availableAndNotInstalled)
{
var recommended = BTCPayServerOptions.RecommendedPlugins.Contains(plugin.Identifier.ToLowerInvariant());
var disabled = Model.Disabled?.Contains(plugin.Identifier) ?? false;
<div class="col col-12 col-md-6 col-lg-12 col-xl-6 col-xxl-4 mb-4">
<div class="card h-100" id="@plugin.Identifier">
<div class="card-body">
<div class="d-flex flex-wrap align-items-baseline justify-content-between gap-2 mb-3">
<h4 class="card-title mb-0" data-bs-toggle="tooltip" title="@plugin.Identifier">@plugin.Name</h4>
@if (!string.IsNullOrEmpty(plugin.Author))
{
<span class="text-muted text-nowrap">
by
<a href="@plugin.AuthorLink" rel="noreferrer noopener" target="_blank">
@plugin.Author
</a>
</span>
}
</div>
<h5 class="text-muted d-flex align-items-center mt-1 gap-2">
@plugin.Version
@if (disabled)
{
<div class="badge bg-light">Disabled</div>
}
else if (recommended)
{
<div class="badge bg-light text-nowrap" data-bs-toggle="tooltip" title="This plugin has been recommended to be installed by your deployment method.">Recommended <vc:icon symbol="info" /></div>
}
</h5>
<p class="card-text">@plugin.Description</p>
@if (plugin.Dependencies?.Any() is true)
{
<h6 class="text-muted fw-semibold">Dependencies</h6>
<ul class="list-group list-group-flush">
@foreach (var dependency in plugin.Dependencies)
{
<li class="list-group-item p-2 d-inline-flex align-items-center gap-2">
@dependency
@if (!DependencyMet(dependency))
{
<span title="Dependency not met." data-bs-toggle="tooltip" class="text-danger">
<vc:icon symbol="warning" />
</span>
}
</li>
}
</ul>
}
@if (plugin != null)
{
<h6 class="text-muted fw-semibold mt-4">Resources</h6>
<ul class="list-group list-group-flush list-unstyled">
@if (plugin.Source is not null)
{
<li>
<a href="@plugin.Source" rel="noreferrer noopener" class="d-flex align-items-center" target="_blank">
<vc:icon symbol="github" />
<span style="margin-left:.4rem">Sources</span>
</a>
</li>
}
@if (!string.IsNullOrEmpty(plugin.Documentation))
{
<li>
<a href="@plugin.Documentation" rel="noreferrer noopener" class="d-flex align-items-center gap-2" target="_blank">
<vc:icon symbol="docs" />
<span>Documentation</span>
</a>
</li>
}
else
{
<li>
<span rel="noreferrer noopener" class="d-flex align-items-center gap-2 text-danger" target="_blank">
<vc:icon symbol="docs" />
<span>No documentation</span>
</span>
</li>
}
</ul>
}
</div>
<div class="card-footer border-0 pb-3">
@{
var pending = Model.Commands.LastOrDefault(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
}
@if (!pending.Equals(default))
{
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
<button type="submit" class="btn btn-outline-secondary">Cancel pending @pending.command</button>
</form>
}
else if (DependenciesMet(plugin.Dependencies))
{
@* Don't show the "Install" button if plugin has been disabled *@
@if (!disabled)
{
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@plugin.Version">
<button type="submit" class="btn btn-primary">Install</button>
</form>
}
}
else
{
<div class="text-danger">
Cannot install until dependencies are met.
</div>
}
</div>
</div>
</div>
}
</div>
}
<div class="mb-4">
<h3 class="mb-4">Upload Plugin</h3>
<button class="btn btn-secondary mb-4" type="button" data-bs-toggle="collapse" data-bs-target="#manual-upload">
Upload Plugin
</button>
<div class="row collapse" id="manual-upload">
<div class="col col-xl-6 mb-4">
<div class="card">
<div class="card-body">
<h4 class="card-title">Add plugin manually</h4>
<div class="alert alert-warning my-3">
<h6 class="me-1">This is an extremely dangerous operation!</h6>
Only upload plugins from trusted sources.
</div>
<form method="post" enctype="multipart/form-data" asp-action="UploadPlugin">
<input type="file" class="form-control mb-3" required name="files" accept=".btcpay" id="files">
<button class="btn btn-primary" type="submit">Upload</button>
</form>
</div>
</div>
</div>
</div>
</div>
@if (Model.Commands.Any())
{
<div class="mb-4">
<h3 class="mb-4">Pending Action</h3>
<button class="btn btn-secondary mb-4" type="button" data-bs-toggle="collapse" data-bs-target="#pending-actions">
Pending Actions
</button>
<div class="row collapse" id="pending-actions">
<div class="col col-12 col-lg-6 mb-4">
<div class="card">
<div class="card-body">
<h4 class="card-title">Pending actions</h4>
<ul class="list-group list-group-flush">
@foreach (var extComm in Model.Commands.GroupBy(tuple => tuple.plugin))
{
<li class="list-group-item p-2">
<div class="d-flex flex-wrap align-items-center justify-content-between">
<span class="my-2 me-3">@extComm.Key</span>
<form asp-action="CancelPluginCommands" asp-route-plugin="@extComm.Key">
<button type="submit" class="btn btn-outline-secondary">Cancel pending @extComm.Last().command</button>
</form>
</div>
</li>
}
</ul>
</div>
</div>
</div>
</div>
</div>
}
@if (Model.Disabled.Any())
{
<div class="mb-4">
<h3 class="mb-4">Disabled Plugins</h3>
<button class="btn btn-secondary mb-4" type="button" data-bs-toggle="collapse" data-bs-target="#disabled-plugins">
Disabled Plugins
</button>
<div class="row collapse" id="disabled-plugins">
<div class="col col-12 col-lg-6 mb-4">
<div class="card">
<div class="card-body">
<h4 class="card-title">Disabled plugins</h4>
<ul class="list-group list-group-flush">
@foreach (var d in Model.Disabled)
{
<li class="list-group-item px-0">
<div class="d-flex flex-wrap align-items-center justify-content-between mx-3">
<span class="my-2 me-3">@d</span>
<form asp-action="UnInstallPlugin" asp-route-plugin="@d">
<button type="submit" class="btn btn-outline-secondary">Uninstall</button>
</form>
</div>
</li>
}
</ul>
</div>
</div>
</div>
</div>
</div>
}