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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user