feat(input): warn and hold before sending plugin-only symbols
This commit is contained in:
@@ -386,6 +386,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
new SelfTests.MainWindowFocusOpacityStep(this),
|
new SelfTests.MainWindowFocusOpacityStep(this),
|
||||||
new SelfTests.MainWindowFlagsStep(this),
|
new SelfTests.MainWindowFlagsStep(this),
|
||||||
new SelfTests.SenderNameReformatStep(this),
|
new SelfTests.SenderNameReformatStep(this),
|
||||||
|
new SelfTests.DisclosureArmStep(this),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Re-surface the wizard for existing users when a major UX
|
// Re-surface the wizard for existing users when a major UX
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
|
using Dalamud.Plugin.SelfTest;
|
||||||
|
using HellionChat._Helpers;
|
||||||
|
|
||||||
|
namespace HellionChat.SelfTests;
|
||||||
|
|
||||||
|
// B2-3: proves the plugin-disclosure arm-and-hold wires the (otherwise verwaist)
|
||||||
|
// scanner into the REAL send entry InputBar.TrySend. Drives TrySend via the
|
||||||
|
// arm-test-hook with a PUA glyph in the buffer and NotifyPluginDisclosure on:
|
||||||
|
// the first send must ARM and HOLD (no send), so PendingMessage stays the probe
|
||||||
|
// string and the armed flag is set. Arm-case ONLY (seiteneffektfrei): a real
|
||||||
|
// send fires ChatBox.SendMessageUnsafe (a real in-game chat line), so the
|
||||||
|
// second-Enter-sends + ASCII-passthrough legs are in-game smoke only, never
|
||||||
|
// headless. Does NOT call PluginDisclosureScanner.ContainsPrivateUseGlyph in
|
||||||
|
// isolation (the false-green trap — it has no other production caller).
|
||||||
|
internal sealed class DisclosureArmStep : ISelfTestStep
|
||||||
|
{
|
||||||
|
private readonly Plugin plugin;
|
||||||
|
|
||||||
|
public DisclosureArmStep(Plugin plugin)
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => "Hellion Chat - plugin disclosure arm";
|
||||||
|
|
||||||
|
public SelfTestStepResult RunStep()
|
||||||
|
{
|
||||||
|
var input = this.plugin.InputBar;
|
||||||
|
if (input is null)
|
||||||
|
{
|
||||||
|
ImGui.Text("Plugin.InputBar is null");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The SymbolPicker inserts exactly these FFXIV Private-Use-Area glyphs;
|
||||||
|
// HighQuality is inside PluginDisclosureScanner's PUA range by
|
||||||
|
// construction (the scanner range IS the SeIconChar range).
|
||||||
|
var probe = $"test {SeIconChar.HighQuality.ToIconString()} msg";
|
||||||
|
|
||||||
|
var savedPending = input.PendingMessage;
|
||||||
|
var savedNotify = Plugin.Config.NotifyPluginDisclosure;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Plugin.Config.NotifyPluginDisclosure = true;
|
||||||
|
input.TestResetDisclosureForSelfTest();
|
||||||
|
input.TestSetPendingMessageForSelfTest(probe);
|
||||||
|
|
||||||
|
// Precondition guard: refuse to drive the real TrySend unless the
|
||||||
|
// toggle is on AND the scanner sees the probe glyph. If the scanner
|
||||||
|
// regressed, this bails with Fail WITHOUT ever calling TrySend, so a
|
||||||
|
// broken scanner can never leak a real chat line. (The remaining
|
||||||
|
// risk — TrySend not calling the scanner at all — is the wiring this
|
||||||
|
// step exists to catch and is covered by the documented residual-leak
|
||||||
|
// note + the mandatory mid-cycle smoke; see the Step 4.8 warning box.)
|
||||||
|
if (
|
||||||
|
!Plugin.Config.NotifyPluginDisclosure
|
||||||
|
|| !PluginDisclosureScanner.ContainsPrivateUseGlyph(input.PendingMessage)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ImGui.Text(
|
||||||
|
"Disclosure precondition not met (toggle off or probe glyph not in the scanner's PUA range) — refusing to drive TrySend to avoid an unintended real send"
|
||||||
|
);
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First send with a PUA glyph + toggle on must ARM, not send. Pass a
|
||||||
|
// null Tab — the arm branch returns before any channel/send use.
|
||||||
|
var armed = input.TestTryArmDisclosureForSelfTest(null);
|
||||||
|
|
||||||
|
if (!armed)
|
||||||
|
{
|
||||||
|
ImGui.Text(
|
||||||
|
"First send did not arm disclosure for a PUA-glyph buffer (scanner not wired into TrySend?)"
|
||||||
|
);
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer must be HELD: TrySend clears _pendingMessage to empty only on
|
||||||
|
// a real send, so an unchanged probe proves nothing was transmitted.
|
||||||
|
if (input.PendingMessage != probe)
|
||||||
|
{
|
||||||
|
ImGui.Text(
|
||||||
|
$"Buffer not held on arm: PendingMessage = '{input.PendingMessage}', expected the unchanged probe (a cleared buffer means it actually sent)"
|
||||||
|
);
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelfTestStepResult.Pass;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
input.TestResetDisclosureForSelfTest();
|
||||||
|
input.TestSetPendingMessageForSelfTest(savedPending);
|
||||||
|
Plugin.Config.NotifyPluginDisclosure = savedNotify;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanUp() { }
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@ using System.Numerics;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using HellionChat._Helpers;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.GameFunctions;
|
using HellionChat.GameFunctions;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
@@ -42,6 +44,12 @@ internal sealed class InputBar
|
|||||||
private bool _wasInputTextHovered;
|
private bool _wasInputTextHovered;
|
||||||
private bool? _isFocusedOverride; // Test-only; null = honour per-frame Draw() value.
|
private bool? _isFocusedOverride; // Test-only; null = honour per-frame Draw() value.
|
||||||
|
|
||||||
|
// UI-11 plugin-disclosure arm-and-hold: holds the buffer that armed the
|
||||||
|
// disclosure warning. null = not armed. Compared by value so an edit
|
||||||
|
// re-arms and a resend on the identical buffer goes through. 1.5.6 parity
|
||||||
|
// (ChatInputBar 1d3b429:27).
|
||||||
|
private string? _disclosureArmedBuffer;
|
||||||
|
|
||||||
// Auto-translate popup state — lives here because the popup lifecycle is
|
// Auto-translate popup state — lives here because the popup lifecycle is
|
||||||
// tightly coupled to the input callback and the pending message buffer.
|
// tightly coupled to the input callback and the pending message buffer.
|
||||||
private const string AutoCompleteId = "##hellion-at-complete";
|
private const string AutoCompleteId = "##hellion-at-complete";
|
||||||
@@ -158,6 +166,21 @@ internal sealed class InputBar
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
DrawQuickButtons();
|
DrawQuickButtons();
|
||||||
|
|
||||||
|
// UI-11: yellow inline warning while a plugin-only-glyph message is
|
||||||
|
// armed-and-held (buffer unchanged since it armed). Renders on its own
|
||||||
|
// line below the input row. 1.5.6 parity (ChatInputBar 1d3b429:93-103).
|
||||||
|
if (
|
||||||
|
Plugin.Config.NotifyPluginDisclosure
|
||||||
|
&& _disclosureArmedBuffer is not null
|
||||||
|
&& _pendingMessage == _disclosureArmedBuffer
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ImGui.TextColored(
|
||||||
|
ImGuiColors.DalamudYellow,
|
||||||
|
HellionStrings.ChatInput_PluginDisclosure_Warning
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// SymbolPicker popup is rendered last so it can splice its fragment
|
// SymbolPicker popup is rendered last so it can splice its fragment
|
||||||
// straight into the pending buffer.
|
// straight into the pending buffer.
|
||||||
var inserted = _symbolPicker.DrawAndConsume();
|
var inserted = _symbolPicker.DrawAndConsume();
|
||||||
@@ -334,6 +357,31 @@ internal sealed class InputBar
|
|||||||
if (string.IsNullOrEmpty(text))
|
if (string.IsNullOrEmpty(text))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// UI-11: plugin-disclosure arm-and-hold. Arm + scan on the RAW
|
||||||
|
// _pendingMessage (NOT the trimmed `text`) so the Draw warning gate
|
||||||
|
// (_pendingMessage == _disclosureArmedBuffer) matches byte-for-byte even
|
||||||
|
// when the buffer has leading/trailing whitespace. 1.5.6 armed/held/
|
||||||
|
// warned on the raw buffer and only trimmed at SendChatBox; storing the
|
||||||
|
// trimmed value here would silently kill the warning for a padded buffer
|
||||||
|
// (the Draw gate compares the untrimmed _pendingMessage). Runs BEFORE the
|
||||||
|
// channel prefix + AutoTranslate.ReplaceWithPayload (the resolved <at:>
|
||||||
|
// macro carries its own non-ASCII bytes and would false-positive;
|
||||||
|
// whitespace is never a PUA codepoint, so scanning the raw buffer is
|
||||||
|
// equivalent for detection). First Enter on a buffer with a plugin-only
|
||||||
|
// PUA glyph arms + HOLDS (returns without sending, buffer kept); a second
|
||||||
|
// Enter on the same unchanged buffer sends; editing re-checks. 1.5.6
|
||||||
|
// parity (ChatInputBar.SubmitCompact 1d3b429:108-118).
|
||||||
|
if (
|
||||||
|
Plugin.Config.NotifyPluginDisclosure
|
||||||
|
&& _disclosureArmedBuffer != _pendingMessage
|
||||||
|
&& PluginDisclosureScanner.ContainsPrivateUseGlyph(_pendingMessage)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_disclosureArmedBuffer = _pendingMessage;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_disclosureArmedBuffer = null;
|
||||||
|
|
||||||
// Slash commands route through verbatim — the game's chat parser
|
// Slash commands route through verbatim — the game's chat parser
|
||||||
// handles /tell, /fc, /hellion etc. on its own. Other text gets
|
// handles /tell, /fc, /hellion etc. on its own. Other text gets
|
||||||
// the active channel's prefix so the line lands on the channel
|
// the active channel's prefix so the line lands on the channel
|
||||||
@@ -417,6 +465,21 @@ internal sealed class InputBar
|
|||||||
// override and let Draw()'s ImGui.IsItemFocused() result take over again.
|
// override and let Draw()'s ImGui.IsItemFocused() result take over again.
|
||||||
internal void TestSetFocusedForSelfTest(bool? value) => _isFocusedOverride = value;
|
internal void TestSetFocusedForSelfTest(bool? value) => _isFocusedOverride = value;
|
||||||
|
|
||||||
|
// Test-only hook; do not call from production code. Drives the REAL TrySend
|
||||||
|
// arm path: with NotifyPluginDisclosure on and a PUA glyph in the buffer the
|
||||||
|
// first call arms and HOLDS (no send). Returns whether the buffer is armed.
|
||||||
|
// The caller asserts PendingMessage is unchanged (held) so a regressed wiring
|
||||||
|
// that fell through to ChatBox.SendMessageUnsafe is caught.
|
||||||
|
internal bool TestTryArmDisclosureForSelfTest(Tab? activeTab)
|
||||||
|
{
|
||||||
|
TrySend(activeTab);
|
||||||
|
return _disclosureArmedBuffer is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test-only hook; do not call from production code. Clears the armed buffer
|
||||||
|
// so a SelfTest leaves no residual arm state.
|
||||||
|
internal void TestResetDisclosureForSelfTest() => _disclosureArmedBuffer = null;
|
||||||
|
|
||||||
private void DrawAutoCompletePopup()
|
private void DrawAutoCompletePopup()
|
||||||
{
|
{
|
||||||
if (_autoCompleteInfo == null)
|
if (_autoCompleteInfo == null)
|
||||||
|
|||||||
@@ -56,6 +56,16 @@ internal sealed class ChatTab
|
|||||||
{
|
{
|
||||||
DrawCommandHelpSideCombo();
|
DrawCommandHelpSideCombo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ImGui.CollapsingHeader("Plugin disclosure"))
|
||||||
|
{
|
||||||
|
DrawToggle(
|
||||||
|
HellionStrings.Settings_Chat_NotifyPluginDisclosure_Name,
|
||||||
|
() => Plugin.Config.NotifyPluginDisclosure,
|
||||||
|
v => Plugin.Config.NotifyPluginDisclosure = v
|
||||||
|
);
|
||||||
|
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_NotifyPluginDisclosure_Description);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawPrivacyPersistChannels()
|
private void DrawPrivacyPersistChannels()
|
||||||
|
|||||||
Reference in New Issue
Block a user