feat: website favicons + vault health dashboard
Favicons
- PM.Favicon (new): THTTPClient/WinHTTP proxy to icons.duckduckgo.com.
Native Windows TLS — no OpenSSL DLLs to ship (Indy would fail
silently without them). 5 s timeout, max 3 redirects, 64 KB cap,
magic-byte MIME sniffing.
- DB: vault_entries.icon_b64 TEXT (idempotent migration).
- Endpoints: POST /entries/{id}/icon stores a cached data URI without
forcing a full PUT (which would re-encrypt the password). DELETE
/entries/icons/all purges the cache.
- Bridge cmd://favicon/fetch?host=X&reqId=Y runs in an anonymous thread
so the up-to-5 s HTTP GET doesn't block the main thread; result
shipped back via Bridge.onFaviconResult(reqId, host, dataUri).
- Hostname validated on both sides (JS faviconHost + Delphi
NormalizeHost) so brand labels like "Gitea" never leak upstream.
- Settings: opt-in "Fetch website icons" toggle (synced), three explicit
actions (Fetch missing / Re-fetch all / Clear cache) that bypass the
toggle — manual user actions always work.
- Entry card avatar shows <img> when cached, falls back to initials.
onerror handler recovers silently from a corrupt data URI.
Vault health
- New sidebar Tools → "Vault health" view. Four category cards:
Weak (strength < 50), Reused (same plaintext on ≥ 2 entries), Old
(updated_at > 365d), Pwned (HIBP cache).
- Score 0-100 with colour band (Good/Fair/At risk/Critical).
- One-shot computation cached per session (healthCache), invalidated
on lockVault, entry save, and the explicit "Recompute" button.
- "Fix" button on each item opens the slideover for the affected
entry, unmasks the password, focuses it, and pulses the dice button
— full context preserved, user decides how to fix.
- Click handler stopPropagation prevents the document-level
"click outside slideover" listener from closing the panel that
we just opened in the same click event.
Fixes
- openSlideover typo (lowercase O) → openSlideOver across all call
sites. Was silently breaking the Authenticator card click and the
Vault health Fix button.
- W1050 WideChar warning in PM.Favicon — replaced set-membership
with explicit Ord-style range comparisons.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,8 @@ uses
|
||||
FMX.Dialogs, FMX.DialogService,
|
||||
FMX.TMSFNCTypes, FMX.TMSFNCUtils, FMX.TMSFNCGraphics, FMX.TMSFNCGraphicsTypes,
|
||||
FMX.TMSFNCCustomControl, FMX.TMSFNCWebBrowser,
|
||||
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs, PM.AutoStart;
|
||||
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs, PM.AutoStart,
|
||||
PM.Favicon;
|
||||
|
||||
type
|
||||
TMainForm = class(TForm)
|
||||
@@ -575,6 +576,52 @@ begin
|
||||
BoolToStr(PM.AutoStart.IsAutoStartEnabled, True).ToLower + ')');
|
||||
end
|
||||
|
||||
// ---- Favicon proxy (Delphi-side fetch to keep CSP tight + privacy
|
||||
// centralised on one upstream domain). Async: the HTTP GET would
|
||||
// block the main thread for up to 5 s on slow networks.
|
||||
else if ACmd = 'favicon/fetch' then
|
||||
begin
|
||||
var LHost := GetParam('host');
|
||||
var LReqId := GetParam('reqId'); // opaque, echoed back to JS resolver
|
||||
if LHost = '' then Exit;
|
||||
LogLine('favicon/fetch host="' + LHost + '" reqId=' + LReqId);
|
||||
TThread.CreateAnonymousThread(
|
||||
procedure
|
||||
var
|
||||
LDataUri: string;
|
||||
begin
|
||||
LDataUri := PM.Favicon.FetchFaviconDataUri(LHost);
|
||||
TThread.Queue(nil,
|
||||
procedure
|
||||
begin
|
||||
if LDataUri = '' then
|
||||
LogLine('favicon: NO RESULT for "' + LHost +
|
||||
'" (TLS error? OpenSSL DLLs missing? DDG 404?)')
|
||||
else
|
||||
LogLine(Format('favicon: got %d bytes for "%s"',
|
||||
[Length(LDataUri), LHost]));
|
||||
end);
|
||||
TThread.Queue(nil,
|
||||
procedure
|
||||
var
|
||||
LEscHost, LEscData, LEscReq: string;
|
||||
begin
|
||||
LEscHost := StringReplace(LHost, '\', '\\', [rfReplaceAll]);
|
||||
LEscHost := StringReplace(LEscHost, '"', '\"', [rfReplaceAll]);
|
||||
LEscReq := StringReplace(LReqId, '\', '\\', [rfReplaceAll]);
|
||||
LEscReq := StringReplace(LEscReq, '"', '\"', [rfReplaceAll]);
|
||||
// The data URI is base64 (ASCII-safe) plus a small prefix —
|
||||
// no embedded quotes by construction, but escape anyway.
|
||||
LEscData := StringReplace(LDataUri, '\', '\\', [rfReplaceAll]);
|
||||
LEscData := StringReplace(LEscData, '"', '\"', [rfReplaceAll]);
|
||||
WebBrowser.ExecuteJavaScript(
|
||||
'if(window.Bridge&&Bridge.onFaviconResult)' +
|
||||
'Bridge.onFaviconResult("' + LEscReq + '","' + LEscHost + '","' +
|
||||
LEscData + '")');
|
||||
end);
|
||||
end).Start;
|
||||
end
|
||||
|
||||
else if ACmd = 'autostart/set' then
|
||||
begin
|
||||
var LOk := PM.AutoStart.SetAutoStart(GetParam('enabled') = '1');
|
||||
|
||||
Reference in New Issue
Block a user