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:
@@ -127,8 +127,8 @@ type
|
|||||||
procedure ShowFirstTimeBalloon;
|
procedure ShowFirstTimeBalloon;
|
||||||
function FindFMXAppWindow: HWND;
|
function FindFMXAppWindow: HWND;
|
||||||
public
|
public
|
||||||
// Tray balloon (gated by the "Show tray notifications" setting). Used
|
// Tray balloon (caller applies its own gating setting). Used for
|
||||||
// for messages the user must see while the window is hidden — e.g. an
|
// messages the user must see while the window is hidden — e.g. an
|
||||||
// autofill blocked by an elevated target.
|
// autofill blocked by an elevated target.
|
||||||
procedure ShowBalloon(const ATitle, AText: string; AWarning: Boolean = False);
|
procedure ShowBalloon(const ATitle, AText: string; AWarning: Boolean = False);
|
||||||
constructor Create(AMainForm: TForm);
|
constructor Create(AMainForm: TForm);
|
||||||
@@ -623,8 +623,8 @@ procedure TPMBridge.ShowBalloon(const ATitle, AText: string;
|
|||||||
var
|
var
|
||||||
LBalloon: TNotifyIconData;
|
LBalloon: TNotifyIconData;
|
||||||
begin
|
begin
|
||||||
// Gated by the "Show tray notifications" user setting.
|
// No gating here — each caller applies its own setting (first-time tray
|
||||||
if not FShowNotifications then Exit;
|
// balloon → FShowNotifications; autofill-blocked → autofillFailBalloon).
|
||||||
// Build a separate TNotifyIconData with NIF_INFO set, NIM_MODIFY on the
|
// Build a separate TNotifyIconData with NIF_INFO set, NIM_MODIFY on the
|
||||||
// same uID. szInfo/szInfoTitle carry the balloon content.
|
// same uID. szInfo/szInfoTitle carry the balloon content.
|
||||||
FillChar(LBalloon, SizeOf(LBalloon), 0);
|
FillChar(LBalloon, SizeOf(LBalloon), 0);
|
||||||
@@ -1091,6 +1091,12 @@ procedure SendSelectAllAndDelete;
|
|||||||
var
|
var
|
||||||
LInputs: array[0..5] of TInput;
|
LInputs: array[0..5] of TInput;
|
||||||
begin
|
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);
|
FillChar(LInputs, SizeOf(LInputs), 0);
|
||||||
LInputs[0].Itype := INPUT_KEYBOARD;
|
LInputs[0].Itype := INPUT_KEYBOARD;
|
||||||
LInputs[0].ki.wVk := VK_CONTROL;
|
LInputs[0].ki.wVk := VK_CONTROL;
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ type
|
|||||||
// Send Ctrl+A + Del before typing each field (user setting, default ON;
|
// Send Ctrl+A + Del before typing each field (user setting, default ON;
|
||||||
// OFF for targets where Ctrl+A isn't select-all — terminals, RDP).
|
// OFF for targets where Ctrl+A isn't select-all — terminals, RDP).
|
||||||
FAutofillPendingClear: Boolean;
|
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
|
// Created dynamically in FormCreate so the directive can pick either
|
||||||
// TTMSFNCWebBrowser or TTMSFNCEdgeWebBrowser at compile time without
|
// TTMSFNCWebBrowser or TTMSFNCEdgeWebBrowser at compile time without
|
||||||
// needing two .fmx variants. Aligned to Client to fill the remaining
|
// needing two .fmx variants. Aligned to Client to fill the remaining
|
||||||
@@ -901,6 +904,7 @@ begin
|
|||||||
FAutofillPendingHide := GetParam('hide_after') = '1';
|
FAutofillPendingHide := GetParam('hide_after') = '1';
|
||||||
FAutofillPendingUserOnly := GetParam('field') = 'user';
|
FAutofillPendingUserOnly := GetParam('field') = 'user';
|
||||||
FAutofillPendingClear := GetParam('clear') <> '0'; // absent = ON
|
FAutofillPendingClear := GetParam('clear') <> '0'; // absent = ON
|
||||||
|
FAutofillPendingNotify := GetParam('notify') <> '0'; // absent = ON
|
||||||
FAutofillTargetHWND := 0;
|
FAutofillTargetHWND := 0;
|
||||||
|
|
||||||
// Small timer so SetForegroundWindow has time to take effect before
|
// Small timer so SetForegroundWindow has time to take effect before
|
||||||
@@ -1453,7 +1457,7 @@ procedure TMainForm.AutofillTimerTick(Sender: TObject);
|
|||||||
var
|
var
|
||||||
TargetHwnd: HWND;
|
TargetHwnd: HWND;
|
||||||
PendingUser, PendingPass: string;
|
PendingUser, PendingPass: string;
|
||||||
HideAfter, UserOnly, ClearFirst: Boolean;
|
HideAfter, UserOnly, ClearFirst, NotifyFail: Boolean;
|
||||||
ForegroundAfter: HWND;
|
ForegroundAfter: HWND;
|
||||||
begin
|
begin
|
||||||
TargetHwnd := FAutofillPendingHWND;
|
TargetHwnd := FAutofillPendingHWND;
|
||||||
@@ -1462,12 +1466,14 @@ begin
|
|||||||
HideAfter := FAutofillPendingHide;
|
HideAfter := FAutofillPendingHide;
|
||||||
UserOnly := FAutofillPendingUserOnly;
|
UserOnly := FAutofillPendingUserOnly;
|
||||||
ClearFirst := FAutofillPendingClear;
|
ClearFirst := FAutofillPendingClear;
|
||||||
|
NotifyFail := FAutofillPendingNotify;
|
||||||
FAutofillPendingHWND := 0;
|
FAutofillPendingHWND := 0;
|
||||||
FAutofillPendingUser := '';
|
FAutofillPendingUser := '';
|
||||||
FAutofillPendingPass := '';
|
FAutofillPendingPass := '';
|
||||||
FAutofillPendingHide := False;
|
FAutofillPendingHide := False;
|
||||||
FAutofillPendingUserOnly := False;
|
FAutofillPendingUserOnly := False;
|
||||||
FAutofillPendingClear := False;
|
FAutofillPendingClear := False;
|
||||||
|
FAutofillPendingNotify := False;
|
||||||
|
|
||||||
TTimer(Sender).Enabled := False;
|
TTimer(Sender).Enabled := False;
|
||||||
TTimer(Sender).Free;
|
TTimer(Sender).Free;
|
||||||
@@ -1487,9 +1493,9 @@ begin
|
|||||||
'if(window.Bridge&&typeof Bridge.onAutofillResult==="function")' +
|
'if(window.Bridge&&typeof Bridge.onAutofillResult==="function")' +
|
||||||
'Bridge.onAutofillResult(' + BoolToStr(LFillOk, True).ToLower + ')');
|
'Bridge.onAutofillResult(' + BoolToStr(LFillOk, True).ToLower + ')');
|
||||||
// Window hidden (tray) → the in-app failure toast is invisible; surface
|
// Window hidden (tray) → the in-app failure toast is invisible; surface
|
||||||
// the block via a tray balloon instead (gated by the tray-notifications
|
// the block via a tray balloon instead (gated by the dedicated
|
||||||
// setting inside ShowBalloon).
|
// "Tray alert when autofill is blocked" setting, default ON).
|
||||||
if (not LFillOk) and
|
if (not LFillOk) and NotifyFail and
|
||||||
((not Self.Visible) or IsIconic(WindowHandleToPlatform(Self.Handle).Wnd)) then
|
((not Self.Visible) or IsIconic(WindowHandleToPlatform(Self.Handle).Wnd)) then
|
||||||
FBridge.ShowBalloon('Autofill blocked',
|
FBridge.ShowBalloon('Autofill blocked',
|
||||||
'The target window runs as administrator — Windows silently blocks ' +
|
'The target window runs as administrator — Windows silently blocks ' +
|
||||||
|
|||||||
+13
@@ -642,6 +642,19 @@
|
|||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span>
|
||||||
|
Tray alert when autofill is blocked
|
||||||
|
<small class="setting-hint">
|
||||||
|
Shows a Windows balloon if the fill fails while the
|
||||||
|
app is hidden (e.g. the target runs as administrator).
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="settingAutofillFailBalloon">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div id="settingAutofillHotkeysRow" style="margin-top:8px">
|
<div id="settingAutofillHotkeysRow" style="margin-top:8px">
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<span style="font-size:12px;color:var(--text-dim)">
|
<span style="font-size:12px;color:var(--text-dim)">
|
||||||
|
|||||||
@@ -142,7 +142,8 @@ const Bridge = (() => {
|
|||||||
'&password=' + encodeURIComponent(password) +
|
'&password=' + encodeURIComponent(password) +
|
||||||
(hideAfter ? '&hide_after=1' : '') +
|
(hideAfter ? '&hide_after=1' : '') +
|
||||||
(field === 'user' ? '&field=user' : '') +
|
(field === 'user' ? '&field=user' : '') +
|
||||||
(state.autofillClearField ? '' : '&clear=0'));
|
(state.autofillClearField ? '' : '&clear=0') +
|
||||||
|
(state.autofillFailBalloon ? '' : '¬ify=0'));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Ask Delphi to bring the main window to front (used when the
|
// Ask Delphi to bring the main window to front (used when the
|
||||||
@@ -645,6 +646,7 @@ const state = {
|
|||||||
recoveryConfigured: false, // refreshed by refreshRecoveryStatus on Settings open
|
recoveryConfigured: false, // refreshed by refreshRecoveryStatus on Settings open
|
||||||
autofillEnabled: localStorage.getItem('autofillEnabled') !== '0', // default ON
|
autofillEnabled: localStorage.getItem('autofillEnabled') !== '0', // default ON
|
||||||
autofillClearField: localStorage.getItem('autofillClearField') !== '0', // default ON
|
autofillClearField: localStorage.getItem('autofillClearField') !== '0', // default ON
|
||||||
|
autofillFailBalloon: localStorage.getItem('autofillFailBalloon') !== '0', // default ON
|
||||||
// Hotkey combos. Each combo = { ctrl, shift, alt, win, key }.
|
// Hotkey combos. Each combo = { ctrl, shift, alt, win, key }.
|
||||||
// key is the uppercase character or VK label ('A'..'Z', '0'..'9',
|
// key is the uppercase character or VK label ('A'..'Z', '0'..'9',
|
||||||
// 'F1'..'F12'). Default: Ctrl+Shift+L / Ctrl+Shift+P. Combos are
|
// 'F1'..'F12'). Default: Ctrl+Shift+L / Ctrl+Shift+P. Combos are
|
||||||
@@ -6344,6 +6346,7 @@ function openSettings() {
|
|||||||
$('#settingFaviconActionsRow').style.display = Bridge.active ? 'flex' : 'none';
|
$('#settingFaviconActionsRow').style.display = Bridge.active ? 'flex' : 'none';
|
||||||
$('#settingAutofill').checked = state.autofillEnabled;
|
$('#settingAutofill').checked = state.autofillEnabled;
|
||||||
$('#settingAutofillClear').checked = state.autofillClearField;
|
$('#settingAutofillClear').checked = state.autofillClearField;
|
||||||
|
$('#settingAutofillFailBalloon').checked = state.autofillFailBalloon;
|
||||||
$('#settingAutofillRow').style.display = Bridge.active ? '' : 'none';
|
$('#settingAutofillRow').style.display = Bridge.active ? '' : 'none';
|
||||||
// Hotkey capture buttons — labels reflect current combos.
|
// Hotkey capture buttons — labels reflect current combos.
|
||||||
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
||||||
@@ -6756,6 +6759,8 @@ const SYNCED_SETTING_KEYS = [
|
|||||||
// Send Ctrl+A + Del before typing each autofill field (default ON).
|
// Send Ctrl+A + Del before typing each autofill field (default ON).
|
||||||
// OFF for targets where Ctrl+A isn't select-all (terminals, RDP).
|
// OFF for targets where Ctrl+A isn't select-all (terminals, RDP).
|
||||||
'autofillClearField',
|
'autofillClearField',
|
||||||
|
// Tray balloon when a fill fails while the window is hidden (default ON).
|
||||||
|
'autofillFailBalloon',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Sets `data-editor-position` on <body> so CSS can swap the slideover
|
// Sets `data-editor-position` on <body> so CSS can swap the slideover
|
||||||
@@ -6839,6 +6844,9 @@ async function loadServerSettings() {
|
|||||||
case 'autofillClearField':
|
case 'autofillClearField':
|
||||||
localStorage.setItem('autofillClearField', v ? '1' : '0');
|
localStorage.setItem('autofillClearField', v ? '1' : '0');
|
||||||
break;
|
break;
|
||||||
|
case 'autofillFailBalloon':
|
||||||
|
localStorage.setItem('autofillFailBalloon', v ? '1' : '0');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Apply visual settings immediately.
|
// Apply visual settings immediately.
|
||||||
@@ -7674,6 +7682,11 @@ async function init() {
|
|||||||
localStorage.setItem('autofillClearField', e.target.checked ? '1' : '0');
|
localStorage.setItem('autofillClearField', e.target.checked ? '1' : '0');
|
||||||
saveServerSettings();
|
saveServerSettings();
|
||||||
});
|
});
|
||||||
|
$('#settingAutofillFailBalloon').addEventListener('change', e => {
|
||||||
|
state.autofillFailBalloon = e.target.checked;
|
||||||
|
localStorage.setItem('autofillFailBalloon', e.target.checked ? '1' : '0');
|
||||||
|
saveServerSettings();
|
||||||
|
});
|
||||||
|
|
||||||
// ---- Hotkey capture buttons ----
|
// ---- Hotkey capture buttons ----
|
||||||
// Click → button label becomes "Press combo…" → next keydown captures.
|
// Click → button label becomes "Press combo…" → next keydown captures.
|
||||||
|
|||||||
Reference in New Issue
Block a user