fix(autofill): honest result reporting + restore maximized from tray

Bug 1: a maximized window trayed via the quick-search fill flow came back
"normal" on the next restore. ExecuteAutofill minimizes the window BEFORE
MinimizeToTray snapshots the placement, so the snapshot said SHOWMINIMIZED
and the never-restore-minimized guard forced SHOWNORMAL. Now honours
WPF_RESTORETOMAXIMIZED (Windows keeps the pre-minimize state in flags).

Bug 2: filling into an elevated app (admin Notepad) showed "password sent"
while UIPI silently discarded the keystrokes (SendInput even reports
success). ExecuteAutofill is now a function: it checks the target process
elevation up front (can't-open counts as elevated) and returns False without
typing. UMainForm feeds the result to JS via Bridge.onAutofillResult; the
quick-search success toast is deferred until Delphi confirms, and a failure
shows "Autofill blocked - the target window runs as administrator".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-11 20:19:34 +01:00
parent 7103fbf703
commit 4a47caad55
5 changed files with 101 additions and 17 deletions
+63 -9
View File
@@ -169,9 +169,11 @@ type
// visible (no minimize at all — the foreground process is allowed to hand
// focus to the target). False (tray-origin) → minimize out of the way,
// the caller trays it after the fill.
procedure ExecuteAutofill(ATargetHWND: HWND;
// Returns False when nothing was typed: elevated target (UIPI would
// silently drop the keystrokes) or focus never left our own window.
function ExecuteAutofill(ATargetHWND: HWND;
const AUsername, APassword: string; AUsernameOnly: Boolean = False;
ARestoreAfter: Boolean = False);
ARestoreAfter: Boolean = False): Boolean;
property SecureClipboard: TSecureClipboard read FSecureClipboard;
property TrayAdded: Boolean read FTrayAdded;
property AutofillRegistered: Boolean read FAutofillRegistered;
@@ -669,9 +671,18 @@ begin
if FHasSavedPlacement then
begin
// showCmd governs whether the window comes back maximised or normal;
// it's what SW_RESTORE clobbers. We force it ourselves.
// it's what SW_RESTORE clobbers. We force it ourselves. Captured while
// MINIMISED (tray-origin fill: ExecuteAutofill minimises us before
// MinimizeToTray snapshots): never restore as minimised, but honour
// WPF_RESTORETOMAXIMIZED — a maximised window minimised then trayed
// must come back maximised, not "normal".
if FSavedPlacement.showCmd = SW_SHOWMINIMIZED then
FSavedPlacement.showCmd := SW_SHOWNORMAL; // never restore as minimised
begin
if (FSavedPlacement.flags and WPF_RESTORETOMAXIMIZED) <> 0 then
FSavedPlacement.showCmd := SW_SHOWMAXIMIZED
else
FSavedPlacement.showCmd := SW_SHOWNORMAL;
end;
SetWindowPlacement(LFormHwnd, @FSavedPlacement);
end
else
@@ -1102,6 +1113,39 @@ begin
end;
end;
// True when the process owning AHwnd runs elevated (admin). UIPI silently
// DISCARDS SendInput from a non-elevated process into an elevated one —
// SendInput even reports success — so detecting elevation up front is the
// only way to tell the user the fill can't work instead of lying "sent".
// Can't-tell (OpenProcess denied, which protected/elevated processes do)
// counts as elevated: better an honest "blocked" than a silent no-op.
function IsWindowProcessElevated(AHwnd: HWND): Boolean;
var
LPid: DWORD;
LProc, LToken: THandle;
LElev: TOKEN_ELEVATION;
LLen: DWORD;
begin
Result := False;
LPid := 0;
GetWindowThreadProcessId(AHwnd, LPid);
if LPid = 0 then Exit;
LProc := OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, LPid);
if LProc = 0 then Exit(True);
try
if not OpenProcessToken(LProc, TOKEN_QUERY, LToken) then Exit(True);
try
LLen := 0;
if GetTokenInformation(LToken, TokenElevation, @LElev, SizeOf(LElev), LLen) then
Result := LElev.TokenIsElevated <> 0;
finally
CloseHandle(LToken);
end;
finally
CloseHandle(LProc);
end;
end;
procedure ClickTargetCenterToGrabFocus(ATargetHwnd: HWND);
const
PostClickSettleMs = 40;
@@ -1138,16 +1182,23 @@ begin
Sleep(PostClickSettleMs);
end;
procedure TPMBridge.ExecuteAutofill(ATargetHWND: HWND;
function TPMBridge.ExecuteAutofill(ATargetHWND: HWND;
const AUsername, APassword: string; AUsernameOnly: Boolean = False;
ARestoreAfter: Boolean = False);
ARestoreAfter: Boolean = False): Boolean;
const
MinimizeSettleMs = 80;
FocusSettleDelayMs = 120;
var
OwnFormHwnd: HWND;
begin
Result := False;
OwnFormHwnd := MainFormHWND(FMainForm);
// UIPI: keystrokes into an elevated target are silently dropped by Windows
// (SendInput even claims success). Detect it up front and report failure so
// the UI can say "blocked" instead of a false "password sent".
if (ATargetHWND <> 0) and IsWindowProcessElevated(ATargetHWND) then Exit;
// ARestoreAfter (window was open before the hotkey): DON'T minimize at all.
// Being the foreground process is precisely what lets us hand the focus to
// the target via ForceForegroundWindow — the window just stays where it is,
@@ -1167,11 +1218,14 @@ begin
WaitForModifierRelease(1000);
Sleep(FocusSettleDelayMs);
// Never type into our own window: if the target refused the foreground
// (elevated process / UIPI), the keystrokes would land in the vault UI
// itself — a password typed into a visible search box. Bail instead.
// Never type into our own window: if the target refused the foreground,
// the keystrokes would land in the vault UI itself — a password typed
// into a visible search box. Bail instead.
if GetForegroundWindow = OwnFormHwnd then Exit;
// Past every bail-out — the keystrokes below are the fill itself.
Result := True;
// Username-only: type just the username into the focused field, no Tab,
// no password. Used by the quick-search right-click / Shift+Enter path.
if AUsernameOnly then