fix(autofill): kill residual hotkey chord; dedicated fail-balloon setting

Residual new-entry trigger (1 in 6): password chars go out as
KEYEVENTF_UNICODE (VK_PACKET, can't match a hotkey) — the real chord risk is
the Ctrl+A clear-field, which sends a real VK_A. If the user re-presses
Ctrl+Shift mid-sequence, that VK_A becomes physical Ctrl+Shift+A = our own
new-entry hotkey. ForceReleaseModifiers now runs inside
SendSelectAllAndDelete, at the risky instant, not just once up front.

Balloon: was gated by "Show tray notifications" (OFF for this user) — now
gated by its own synced setting "Tray alert when autofill is blocked"
(autofillFailBalloon, Settings > Autofill, default ON), carried as notify=0
on cmd://autofill/execute. ShowBalloon no longer gates internally; each
caller applies its own setting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-12 05:23:02 +01:00
parent 9a72fc0424
commit 72dcc3dd82
4 changed files with 48 additions and 10 deletions
+10 -4
View File
@@ -127,8 +127,8 @@ type
procedure ShowFirstTimeBalloon;
function FindFMXAppWindow: HWND;
public
// Tray balloon (gated by the "Show tray notifications" setting). Used
// for messages the user must see while the window is hidden — e.g. an
// Tray balloon (caller applies its own gating setting). Used for
// messages the user must see while the window is hidden — e.g. an
// autofill blocked by an elevated target.
procedure ShowBalloon(const ATitle, AText: string; AWarning: Boolean = False);
constructor Create(AMainForm: TForm);
@@ -623,8 +623,8 @@ procedure TPMBridge.ShowBalloon(const ATitle, AText: string;
var
LBalloon: TNotifyIconData;
begin
// Gated by the "Show tray notifications" user setting.
if not FShowNotifications then Exit;
// No gating here — each caller applies its own setting (first-time tray
// balloon → FShowNotifications; autofill-blocked → autofillFailBalloon).
// Build a separate TNotifyIconData with NIF_INFO set, NIM_MODIFY on the
// same uID. szInfo/szInfoTitle carry the balloon content.
FillChar(LBalloon, SizeOf(LBalloon), 0);
@@ -1091,6 +1091,12 @@ procedure SendSelectAllAndDelete;
var
LInputs: array[0..5] of TInput;
begin
// This is the ONLY real-VK chord we inject (password chars go out as
// KEYEVENTF_UNICODE / VK_PACKET, which can't match a RegisterHotKey).
// If the user re-presses Ctrl+Shift mid-sequence, our VK_A below becomes
// a physical Ctrl+Shift+A = our own new-entry hotkey firing mid-fill.
// Force-release right here, at the risky instant — not just once up front.
ForceReleaseModifiers;
FillChar(LInputs, SizeOf(LInputs), 0);
LInputs[0].Itype := INPUT_KEYBOARD;
LInputs[0].ki.wVk := VK_CONTROL;
+11 -5
View File
@@ -108,6 +108,9 @@ type
// Send Ctrl+A + Del before typing each field (user setting, default ON;
// OFF for targets where Ctrl+A isn't select-all — terminals, RDP).
FAutofillPendingClear: Boolean;
// Tray balloon when the fill fails while the window is hidden (user
// setting "Tray alert when autofill is blocked", default ON).
FAutofillPendingNotify: Boolean;
// Created dynamically in FormCreate so the directive can pick either
// TTMSFNCWebBrowser or TTMSFNCEdgeWebBrowser at compile time without
// needing two .fmx variants. Aligned to Client to fill the remaining
@@ -900,7 +903,8 @@ begin
FAutofillPendingHWND := FAutofillTargetHWND;
FAutofillPendingHide := GetParam('hide_after') = '1';
FAutofillPendingUserOnly := GetParam('field') = 'user';
FAutofillPendingClear := GetParam('clear') <> '0'; // absent = ON
FAutofillPendingClear := GetParam('clear') <> '0'; // absent = ON
FAutofillPendingNotify := GetParam('notify') <> '0'; // absent = ON
FAutofillTargetHWND := 0;
// Small timer so SetForegroundWindow has time to take effect before
@@ -1453,7 +1457,7 @@ procedure TMainForm.AutofillTimerTick(Sender: TObject);
var
TargetHwnd: HWND;
PendingUser, PendingPass: string;
HideAfter, UserOnly, ClearFirst: Boolean;
HideAfter, UserOnly, ClearFirst, NotifyFail: Boolean;
ForegroundAfter: HWND;
begin
TargetHwnd := FAutofillPendingHWND;
@@ -1462,12 +1466,14 @@ begin
HideAfter := FAutofillPendingHide;
UserOnly := FAutofillPendingUserOnly;
ClearFirst := FAutofillPendingClear;
NotifyFail := FAutofillPendingNotify;
FAutofillPendingHWND := 0;
FAutofillPendingUser := '';
FAutofillPendingPass := '';
FAutofillPendingHide := False;
FAutofillPendingUserOnly := False;
FAutofillPendingClear := False;
FAutofillPendingNotify := False;
TTimer(Sender).Enabled := False;
TTimer(Sender).Free;
@@ -1487,9 +1493,9 @@ begin
'if(window.Bridge&&typeof Bridge.onAutofillResult==="function")' +
'Bridge.onAutofillResult(' + BoolToStr(LFillOk, True).ToLower + ')');
// Window hidden (tray) → the in-app failure toast is invisible; surface
// the block via a tray balloon instead (gated by the tray-notifications
// setting inside ShowBalloon).
if (not LFillOk) and
// the block via a tray balloon instead (gated by the dedicated
// "Tray alert when autofill is blocked" setting, default ON).
if (not LFillOk) and NotifyFail and
((not Self.Visible) or IsIconic(WindowHandleToPlatform(Self.Handle).Wnd)) then
FBridge.ShowBalloon('Autofill blocked',
'The target window runs as administrator — Windows silently blocks ' +