From 6de9959126d8d21b8cfa50f323a2152154fabcba Mon Sep 17 00:00:00 2001 From: Indigo Karasu Date: Wed, 8 Jul 2026 00:25:44 -0700 Subject: [PATCH] Re-implement favicon support on MV3 dev (resolves stale PR merge conflict) The original favicon PR (#1118) targeted an MV2 base and had been open ~2 years; dev is now MV3 and the branch no longer merges. This re-implements the feature against current dev conventions and fixes the original bugs: - Built for MV3: uses the favicon permission + the chrome.runtime.getURL ('/_favicon/') endpoint instead of the dead MV2 chrome://favicon/ URL. Only the 'favicon' optional permission is requested (on toggle); the invalid MV2 'chrome://favicon/' host permission and CSP scheme are dropped. The extension's own _favicon resource is already covered by img-src 'self', so no CSP relaxation is needed. - EntryComponent.shouldShowFavicon is a proper reactive computed returning !isFirefox && !isSafari && menu.showFavicon (uses browser.ts constants), fixing the original && short-circuit bug that discarded the guard. - Wires showFavicon into the new UserSettings model (interface, BooleanOption, isBooleanOption) and the menu store, so the setting persists and syncs. - Favicons are fetched lazily per entry at render time; nothing bundled or pre-downloaded. medal.svg is a UI placeholder glyph, not a site favicon. Maintainer constraints honored: no mandatory permissions added, no pre-installed or pre-downloaded favicons. --- _locales/en/messages.json | 7 +++++++ manifests/manifest-chrome-testing.json | 3 ++- manifests/manifest-chrome.json | 3 ++- manifests/manifest-edge.json | 3 ++- sass/popup.scss | 14 ++++++++++++++ src/components/Popup/EntryComponent.vue | 23 ++++++++++++++++++++++- src/components/Popup/PreferencesPage.vue | 21 +++++++++++++++++++++ src/definitions/module-interface.d.ts | 1 + src/models/settings.ts | 5 ++++- src/store/Menu.ts | 6 ++++++ src/store/Permissions.ts | 5 +++++ svg/medal.svg | 1 + 12 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 svg/medal.svg diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1509958f1..fb6ed3ed7 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -301,6 +301,10 @@ "message": "Use High Contrast", "description": "Use High Contrast" }, + "show_favicon": { + "message": "Show Website Icon", + "description": "Show Issuer Icon" + }, "theme": { "message": "Theme", "description": "Theme" @@ -512,6 +516,9 @@ "permission_onedrive_cannot_revoke": { "message": "You must disable OneDrive backup first." }, + "permission_favicon": { + "message": "Allows fetching website icons." + }, "permission_unknown_permission": { "message": "Unknown permission. If see this message, please send a bug report." }, diff --git a/manifests/manifest-chrome-testing.json b/manifests/manifest-chrome-testing.json index 96d128949..fced88ea4 100644 --- a/manifests/manifest-chrome-testing.json +++ b/manifests/manifest-chrome-testing.json @@ -57,7 +57,8 @@ ], "optional_permissions": [ "clipboardWrite", - "contextMenus" + "contextMenus", + "favicon" ], "optional_host_permissions": [ "https://www.google.com/", diff --git a/manifests/manifest-chrome.json b/manifests/manifest-chrome.json index e792d9c3b..7c4f291db 100644 --- a/manifests/manifest-chrome.json +++ b/manifests/manifest-chrome.json @@ -57,7 +57,8 @@ ], "optional_permissions": [ "clipboardWrite", - "contextMenus" + "contextMenus", + "favicon" ], "optional_host_permissions": [ "https://www.google.com/", diff --git a/manifests/manifest-edge.json b/manifests/manifest-edge.json index 9997b05a4..4d8585b44 100644 --- a/manifests/manifest-edge.json +++ b/manifests/manifest-edge.json @@ -56,7 +56,8 @@ ], "optional_permissions": [ "clipboardWrite", - "contextMenus" + "contextMenus", + "favicon" ], "optional_host_permissions": [ "https://www.google.com/", diff --git a/sass/popup.scss b/sass/popup.scss index 6849dd8c3..ec0d94aa1 100644 --- a/sass/popup.scss +++ b/sass/popup.scss @@ -317,6 +317,20 @@ svg { width: 80%; text-overflow: ellipsis; overflow: hidden; + + .issuerFavicon { + vertical-align: bottom; + margin-right: 5px; + height: 16px; + width: 16px; + border-radius: 3px; + border: 1px solid transparent; + } + } + + .issuerFavicon { + background-color: #fff; + border: 1px solid #fff; } .code { diff --git a/src/components/Popup/EntryComponent.vue b/src/components/Popup/EntryComponent.vue index 92099b5b1..3481b9a49 100644 --- a/src/components/Popup/EntryComponent.vue +++ b/src/components/Popup/EntryComponent.vue @@ -39,6 +39,14 @@
+ {{ entry.issuer.split("::")[0] + (theme === "compact" ? ` (${entry.account})` : "") @@ -90,6 +98,7 @@ import { mapState } from "vuex"; import * as QRGen from "qrcode-generator"; import { OTPEntry, OTPType, CodeState, OTPAlgorithm } from "../../models/otp"; import { EntryStorage } from "../../models/storage"; +import { isFirefox, isSafari } from "../../browser"; import { getCurrentTab, okToInjectContentScript } from "../../utils"; import IconMinusCircle from "../../../svg/minus-circle.svg"; @@ -97,6 +106,7 @@ import IconRedo from "../../../svg/redo.svg"; import IconQr from "../../../svg/qrcode.svg"; import IconBars from "../../../svg/bars.svg"; import IconPin from "../../../svg/pin.svg"; +import IconMedal from "../../../svg/medal.svg"; const computedPrototype = [ mapState("accounts", [ @@ -110,7 +120,11 @@ const computedPrototype = [ mapState("menu", ["theme"]), ]; -let computed = {}; +let computed = { + shouldShowFavicon(this: any) { + return !isFirefox && !isSafari && this.$store.state.menu.showFavicon; + }, +}; for (const module of computedPrototype) { Object.assign(computed, module); @@ -138,6 +152,12 @@ export default Vue.extend({ entry.type !== OTPType.steam ); }, + getFaviconUrl(u: string) { + const url = new URL(chrome.runtime.getURL("/_favicon/")); + url.searchParams.set("pageUrl", "https://" + u); + url.searchParams.set("size", "16"); + return url.toString(); + }, showCode(code: string) { if (code === CodeState.Encrypted) { return this.i18n.encrypted; @@ -254,6 +274,7 @@ export default Vue.extend({ IconQr, IconBars, IconPin, + IconMedal, }, }); diff --git a/src/components/Popup/PreferencesPage.vue b/src/components/Popup/PreferencesPage.vue index 299b9d8d1..275ab2075 100644 --- a/src/components/Popup/PreferencesPage.vue +++ b/src/components/Popup/PreferencesPage.vue @@ -43,6 +43,11 @@ @change="requireContextMenuPermission()" v-if="isSupported" /> +
{ + this.$store.commit( + "menu/setShowFavicon", + granted ? showFavicon : false + ); + } + ); + }, + }, theme: { get(): string { return this.$store.state.menu.theme; diff --git a/src/definitions/module-interface.d.ts b/src/definitions/module-interface.d.ts index 494266315..ee57e2a88 100644 --- a/src/definitions/module-interface.d.ts +++ b/src/definitions/module-interface.d.ts @@ -36,6 +36,7 @@ interface MenuState { useAutofill: boolean; smartFilter: boolean; enableContextMenu: boolean; + showFavicon: boolean; theme: string; backupDisabled: boolean; storageArea: "sync" | "local"; diff --git a/src/models/settings.ts b/src/models/settings.ts index aac7a84b0..9d21c3fd0 100644 --- a/src/models/settings.ts +++ b/src/models/settings.ts @@ -29,6 +29,7 @@ interface UserSettingsData { enableContextMenu?: boolean; encodedPhrase?: string; smartFilter?: boolean; + showFavicon?: boolean; theme?: string; zoom?: number; } @@ -184,7 +185,8 @@ type BooleanOption = | "oneDriveBusiness" | "oneDriveEncrypted" | "oneDriveRevoked" - | "smartFilter"; + | "smartFilter" + | "showFavicon"; type NumberOption = "autolock" | "lastRemindingBackupTime" | "offset" | "zoom"; @@ -202,6 +204,7 @@ function isBooleanOption(key: string): key is BooleanOption { "oneDriveEncrypted", "oneDriveRevoked", "smartFilter", + "showFavicon", ].includes(key); } diff --git a/src/store/Menu.ts b/src/store/Menu.ts index 48487d6ed..8dcad0cd6 100644 --- a/src/store/Menu.ts +++ b/src/store/Menu.ts @@ -13,6 +13,7 @@ export class Menu implements Module { useAutofill: UserSettings.items.autofill === true, smartFilter: UserSettings.items.smartFilter === true, enableContextMenu: UserSettings.items.enableContextMenu === true, + showFavicon: UserSettings.items.showFavicon === true, theme: UserSettings.items.theme || (isSafari ? "flat" : "normal"), autolock: Number(UserSettings.items.autolock) || 30, backupDisabled: await ManagedStorage.get("disableBackup", false), @@ -48,6 +49,11 @@ export class Menu implements Module { UserSettings.items.enableContextMenu = enableContextMenu; UserSettings.commitItems(); }, + setShowFavicon(state: MenuState, showFavicon: boolean) { + state.showFavicon = showFavicon; + UserSettings.items.showFavicon = showFavicon; + UserSettings.commitItems(); + }, setTheme(state: MenuState, theme: string) { state.theme = theme; UserSettings.items.theme = theme; diff --git a/src/store/Permissions.ts b/src/store/Permissions.ts index a8d20ebca..ed3764b00 100644 --- a/src/store/Permissions.ts +++ b/src/store/Permissions.ts @@ -37,6 +37,11 @@ const permissions: Permission[] = [ description: chrome.i18n.getMessage("permission_context_menus"), revocable: true, }, + { + id: "favicon", + description: chrome.i18n.getMessage("permission_favicon"), + revocable: true, + }, { id: "https://www.google.com/*", description: chrome.i18n.getMessage("permission_sync_clock"), diff --git a/svg/medal.svg b/svg/medal.svg new file mode 100644 index 000000000..48c83d328 --- /dev/null +++ b/svg/medal.svg @@ -0,0 +1 @@ + \ No newline at end of file