review(AdminUI): fix null-TagConfig crash, CTS leak, unencoded historian tag

Review at HEAD 7286d320. AdminUI-002: IsValidJson null/blank -> friendly error (was
ArgumentNullException). AdminUI-003: DriverStatusPanel Reconnect/Restart dispose CTS (build-
verified, live /run deferred). AdminUI-005: HistorianWonderware picker URL-encodes tag name.
AdminUI-008: Format round-trip test. 001 (script-page authz) + 004 (hub [Authorize]) left
Open as cross-cutting w/ Host/Security.
This commit is contained in:
Joseph Doherty
2026-06-19 10:52:23 -04:00
parent 1aa7905676
commit 3c908f1df0
7 changed files with 306 additions and 5 deletions
@@ -227,9 +227,10 @@
try
{
var userName = await GetCurrentUserNameAsync();
using var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(15));
var result = await AdminOps.AskAsync<ReconnectDriverResult>(
new ReconnectDriver(ClusterId, DriverInstanceId, userName, Guid.NewGuid()),
new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(15)).Token);
cts.Token);
ShowOpResult(result.Ok, result.Ok ? "Reconnect dispatched" : (result.Message ?? "Failed"));
}
catch (Exception ex)
@@ -252,9 +253,10 @@
try
{
var userName = await GetCurrentUserNameAsync();
using var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(15));
var result = await AdminOps.AskAsync<RestartDriverResult>(
new RestartDriver(ClusterId, DriverInstanceId, userName, Guid.NewGuid()),
new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(15)).Token);
cts.Token);
ShowOpResult(result.Ok, result.Ok ? "Restart dispatched" : (result.Message ?? "Failed"));
}
catch (Exception ex)
@@ -8,5 +8,8 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers;
public static class HistorianWonderwareAddressBuilder
{
public static string Build(string tagName, string mode, int interval)
=> $"{tagName}?mode={mode}&interval={interval}";
// Percent-encode the tag name so a name carrying query-reserved characters (? & # =) can't
// corrupt the produced query string (AdminUI-005). Mode is a fixed enum-style token, so it
// needs no encoding.
=> $"{Uri.EscapeDataString(tagName)}?mode={mode}&interval={interval}";
}
@@ -1280,9 +1280,17 @@ public sealed class UnsTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
return null;
}
/// <summary>Returns <c>true</c> if <paramref name="json"/> parses as a well-formed JSON document.</summary>
private static bool IsValidJson(string json)
/// <summary>Returns <c>true</c> if <paramref name="json"/> parses as a well-formed JSON document.
/// Null/blank input is treated as invalid (not well-formed JSON) so every caller gets the friendly
/// "not valid JSON" result rather than an unhandled <see cref="ArgumentNullException"/> from
/// <c>JsonDocument.Parse(null)</c> (AdminUI-002).</summary>
private static bool IsValidJson(string? json)
{
if (string.IsNullOrWhiteSpace(json))
{
return false;
}
try
{
using var _ = System.Text.Json.JsonDocument.Parse(json);