/* # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* original: http://github.com/piroor/webextensions-lib-tab-favicon-helper */ 'use strict'; const TabFavIconHelper = { LAST_EFFECTIVE_FAVICON: 'last-effective-favIcon', VALID_FAVICON_PATTERN: /^(about|app|chrome|data|file|ftp|https?|moz-extension|resource):/, DRAWABLE_FAVICON_PATTERN: /^(https?|moz-extension|resource):/, // original: chrome://browser/content/aboutlogins/icons/favicon.svg FAVICON_LOCKWISE: ` `, // original: chrome://browser/content/robot.ico FAVICON_ROBOT: ` data:image/x-icon;base64,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 `.trim(), // original: chrome://browser/skin/controlcenter/dashboard.svg FAVICON_DASHBOARD: ` `, // original: chrome://browser/skin/developer.svg FAVICON_DEVELOPER: ` `, // original: chrome://browser/skin/privatebrowsing/favicon.svg FAVICON_PRIVATE_BROWSING: ` `, // original: chrome://browser/skin/settings.svg FAVICON_SETTINGS: ` `, // original: chrome://browser/skin/window.svg FAVICON_WINDOW: ` `, // original: chrome://devtools/skin/images/profiler-stopwatch.svg FAVICON_PROFILER: ` `, // original: chrome://global/skin/icons/performance.svg FAVICON_PERFORMANCE: ` `, // original: chrome://global/skin/icons/warning.svg FAVICON_WARNING: ` `, // original: chrome://mozapps/skin/extensions/extensionGeneric-16.svg FAVICON_EXTENSION: ` `, // original: globe-16.svg FAVICON_GLOBE: ` `, async _urlToKey(url) { // sha1 hash const encoder = new TextEncoder(); const data = encoder.encode(url); const hashBuffer = await crypto.subtle.digest('SHA-1', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join(''); return hashHex; }, DB_NAME: 'TabFavIconHelper', DB_VERSION: 2, STORE_FAVICONS: 'favIcons', STORE_EFFECTIVE_FAVICONS: 'effectiveFavIcons', STORE_UNEFFECTIVE_FAVICONS: 'uneffectiveFavIcons', EXPIRATION_TIME_IN_MSEC: 7 * 24 * 60 * 60 * 1000, // 7 days async _openDB() { if (this._openedDB) return this._openedDB; return new Promise((resolve, _reject) => { const request = indexedDB.open(this.DB_NAME, this.DB_VERSION); request.onerror = () => { // This can fail if this is in a private window. // See: https://github.com/piroor/treestyletab/issues/3387 //reject(new Error('Failed to open database')); resolve(null); }; request.onsuccess = () => { const db = request.result; this._openedDB = db; resolve(db); }; request.onupgradeneeded = (event) => { const db = event.target.result; const objectStores = db.objectStoreNames; const needToUpgrade = event.oldVersion < this.DB_VERSION; if (needToUpgrade) { if (objectStores.contains(this.STORE_FAVICONS)) db.deleteObjectStore(this.STORE_FAVICONS); if (objectStores.contains(this.STORE_EFFECTIVE_FAVICONS)) db.deleteObjectStore(this.STORE_EFFECTIVE_FAVICONS); if (objectStores.contains(this.STORE_UNEFFECTIVE_FAVICONS)) db.deleteObjectStore(this.STORE_UNEFFECTIVE_FAVICONS); } if (needToUpgrade || !objectStores.contains(this.STORE_FAVICONS)) { const favIconsStore = db.createObjectStore(this.STORE_FAVICONS, { keyPath: 'key', unique: true }); favIconsStore.createIndex('urlKey', 'urlKey', { unique: false }); favIconsStore.createIndex('timestamp', 'timestamp'); } if (needToUpgrade || !objectStores.contains(this.STORE_EFFECTIVE_FAVICONS)) { const effectiveFavIconsStore = db.createObjectStore(this.STORE_EFFECTIVE_FAVICONS, { keyPath: 'urlKey', unique: true }); effectiveFavIconsStore.createIndex('timestamp', 'timestamp'); effectiveFavIconsStore.createIndex('favIconKey', 'favIconKey', { unique: false }); } if (needToUpgrade || !objectStores.contains(this.STORE_UNEFFECTIVE_FAVICONS)) { const uneffectiveFavIconsStore = db.createObjectStore(this.STORE_UNEFFECTIVE_FAVICONS, { keyPath: 'urlKey', unique: true }); uneffectiveFavIconsStore.createIndex('timestamp', 'timestamp'); uneffectiveFavIconsStore.createIndex('favIconKey', 'favIconKey', { unique: false }); } }; }); }, async _associateFavIconUrlToTabUrl({ favIconUrl, tabUrl, store } = {}) { const [db, tabUrlKey, favIconKey] = await Promise.all([ this._openDB(), this._urlToKey(tabUrl), this._urlToKey(favIconUrl), ]); if (!db) return; try { const transaction = db.transaction([store, this.STORE_FAVICONS], 'readwrite'); const associationStore = transaction.objectStore(store); const favIconStore = transaction.objectStore(this.STORE_FAVICONS); const timestamp = Date.now(); const associationRequest = associationStore.put({ urlKey: tabUrlKey, favIconKey, timestamp }); const favIconRequest = favIconStore.put({ key: favIconKey, url: favIconUrl, timestamp }); transaction.oncomplete = () => { //db.close(); this._reserveToExpireOldEntries(); favIconUrl = undefined; tabUrl = undefined; store = undefined; }; associationRequest.onerror = event => { console.error(`Failed to associate favIconUrl ${favIconUrl} to tabUrl ${tabUrl} in the store ${store}`, event); }; favIconRequest.onerror = event => { console.error(`Failed to store favIconUrl ${favIconUrl} to tabUrl ${tabUrl} in the store ${store}`, event); }; } catch(error) { console.error(`Failed to associate favIconUrl ${favIconUrl} to tabUrl ${tabUrl} in the store ${store}`, error); } }, async _unassociateFavIconUrlFromTabUrl({ tabUrl, store } = {}) { const [db, tabUrlKey] = await Promise.all([ this._openDB(), this._urlToKey(tabUrl), ]); if (!db) return; try { const transaction = db.transaction([store], 'readwrite'); const associationStore = transaction.objectStore(store); const unassociationRequest = associationStore.delete(tabUrlKey); transaction.oncomplete = () => { //db.close(); this._reserveToExpireOldEntries(); tabUrl = undefined; store = undefined; }; unassociationRequest.onerror = event => { console.error(`Failed to unassociate favIconUrl from tabUrl ${tabUrl} in the store ${store}`, event); }; } catch(error) { console.error(`Failed to unassociate favIconUrl from tabUrl ${tabUrl} in the store ${store}`, error); } }, async _getAssociatedFavIconUrlFromTabUrl({ tabUrl, store } = {}) { return new Promise(async (resolve, _reject) => { const [db, tabUrlKey] = await Promise.all([ this._openDB(), this._urlToKey(tabUrl), ]); if (!db) { resolve(null); return; } try { const transaction = db.transaction([store, this.STORE_FAVICONS], 'readonly'); const associationStore = transaction.objectStore(store); const favIconStore = transaction.objectStore(this.STORE_FAVICONS); const associationRequest = associationStore.get(tabUrlKey); associationRequest.onsuccess = () => { const association = associationRequest.result; if (!association) { //console.log(`No associated favIconUrl for the tabUrl ${tabUrl} in the store ${store}`); resolve(null); return; } const favIconRequest = favIconStore.get(association.favIconKey); favIconRequest.onsuccess = () => { let favIcon = favIconRequest.result; if (!favIcon) { //console.log(`FavIcon data not found for the tabUrl ${tabUrl} in the store ${store}`); resolve(null); return; } if (favIcon.timestamp < Date.now() - this.EXPIRATION_TIME_IN_MSEC) { //console.log(`FavIcon data is expired for the tabUrl ${tabUrl} in the store ${store}`); this._reserveToExpireOldEntries(); resolve(null); return; } resolve(favIcon.url); favIcon.url = undefined; favIcon = undefined; }; favIconRequest.onerror = event => { console.error(`Failed to get favIconUrl from tabUrl ${tabUrl}`, event); resolve(null); }; }; associationRequest.onerror = event => { console.error(`Failed to get favIcon association from tabUrl ${tabUrl}`, event); resolve(null); }; transaction.oncomplete = () => { //db.close(); tabUrl = undefined; store = undefined; }; } catch(error) { console.error('Failed to get from cache:', error); resolve(null); } }); }, async _reserveToExpireOldEntries() { if (this._reservedExpiration) clearTimeout(this._reservedExpiration); this._reservedExpiration = setTimeout(() => { this._reservedExpiration = null; this._expireOldEntries(); }, 500); }, async _expireOldEntries() { return new Promise(async (resolve, reject) => { const db = await this._openDB(); if (!db) { resolve(); return; } try { const transaction = db.transaction([this.STORE_FAVICONS, this.STORE_EFFECTIVE_FAVICONS, this.STORE_UNEFFECTIVE_FAVICONS], 'readwrite'); const favIconsStore = transaction.objectStore(this.STORE_FAVICONS); const effectiveFavIconsStore = transaction.objectStore(this.STORE_EFFECTIVE_FAVICONS); const uneffectiveFavIconsStore = transaction.objectStore(this.STORE_UNEFFECTIVE_FAVICONS); const favIconIndex = favIconsStore.index('timestamp'); const effectiveFavIconIndex = effectiveFavIconsStore.index('timestamp'); const uneffectiveFavIconIndex = uneffectiveFavIconsStore.index('timestamp'); const expirationTimestamp = Date.now() - this.EXPIRATION_TIME_IN_MSEC; const favIconRequest = favIconIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp)); favIconRequest.onsuccess = (event) => { const cursor = event.target.result; if (!cursor) return; const key = cursor.primaryKey; cursor.continue(); const deleteRequest = favIconsStore.delete(key); deleteRequest.onerror = event => { console.error(`Failed to clear favicon index`, event); }; }; favIconRequest.onerror = event => { console.error(`Failed to retrieve favicon index`, event); }; const effectiveFavIconRequest = effectiveFavIconIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp)); effectiveFavIconRequest.onsuccess = (event) => { const cursor = event.target.result; if (!cursor) return; const url = cursor.primaryKey; cursor.continue(); const deleteRequest = effectiveFavIconsStore.delete(url); deleteRequest.onerror = event => { console.error(`Failed to clear effective favicon index`, event); }; }; effectiveFavIconRequest.onerror = event => { console.error(`Failed to retrieve effective favicon index`, event); }; const uneffectiveFavIconRequest = uneffectiveFavIconIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp)); uneffectiveFavIconRequest.onsuccess = (event) => { const cursor = event.target.result; if (!cursor) return; const url = cursor.primaryKey; cursor.continue(); const deleteRequest = uneffectiveFavIconsStore.delete(url); deleteRequest.onerror = event => { console.error(`Failed to clear uneffective favicon index`, event); }; }; uneffectiveFavIconRequest.onerror = event => { console.error(`Failed to retrieve uneffective favicon index`, event); }; transaction.oncomplete = () => { //db.close(); resolve(); }; } catch(error) { console.error('Failed to expire old entries:', error); reject(error); } }); }, _tasks: [], _processStep: 5, FAVICON_SIZE: 16, _init() { this._onTabUpdated = this._onTabUpdated.bind(this); browser.tabs.onUpdated.addListener(this._onTabUpdated); this.canvas = document.createElement('canvas'); this.canvas.width = this.canvas.height = this.FAVICON_SIZE; this.canvas.setAttribute('style', ` visibility: hidden; pointer-events: none; position: fixed `); document.body.appendChild(this.canvas); window.addEventListener('unload', () => { browser.tabs.onUpdated.removeListener(this._onTabUpdated); }, { once: true }); }, _sessionAPIAvailable: ( browser.sessions && browser.sessions.getTabValue && browser.sessions.setTabValue && browser.sessions.removeTabValue ), _addTask(task) { this._tasks.push(task); this._run(); }, _run() { if (this._running) return; this._running = true; const processOneTask = () => { if (this._tasks.length == 0) { this._running = false; } else { const tasks = this._tasks.splice(0, this._processStep); while (tasks.length > 0) { tasks.shift()(); } window.requestAnimationFrame(processOneTask); } }; processOneTask(); }, // public loadToImage(params = {}) { this._addTask(() => { this._getEffectiveFavIconURL(params.tab, params.url) .then(url => { params.image.src = url; params.image.classList.remove('error'); url = undefined; }, _error => { params.image.src = ''; params.image.classList.add('error'); }); }); }, // public maybeImageTab(_tab) { // for backward compatibility return false; }, _getSafeFaviconUrl(url) { switch (url) { case 'chrome://browser/content/aboutlogins/icons/favicon.svg': return this._getSVGDataURI(this.FAVICON_LOCKWISE); case 'chrome://browser/content/robot.ico': return this.FAVICON_ROBOT; case 'chrome://browser/skin/controlcenter/dashboard.svg': return this._getSVGDataURI(this.FAVICON_DASHBOARD); case 'chrome://browser/skin/developer.svg': return this._getSVGDataURI(this.FAVICON_DEVELOPER); case 'chrome://browser/skin/privatebrowsing/favicon.svg': return this._getSVGDataURI(this.FAVICON_PRIVATE_BROWSING); case 'chrome://browser/skin/settings.svg': return this._getSVGDataURI(this.FAVICON_SETTINGS); case 'chrome://browser/skin/window.svg': return this._getSVGDataURI(this.FAVICON_WINDOW); case 'chrome://devtools/skin/images/profiler-stopwatch.svg': return this._getSVGDataURI(this.FAVICON_PROFILER); case 'chrome://global/skin/icons/performance.svg': return this._getSVGDataURI(this.FAVICON_PERFORMANCE); case 'chrome://global/skin/icons/warning.svg': return this._getSVGDataURI(this.FAVICON_WARNING); case 'chrome://mozapps/skin/extensions/extensionGeneric-16.svg': return this._getSVGDataURI(this.FAVICON_EXTENSION); default: if (/^chrome:\/\//.test(url) && !/^chrome:\/\/branding\//.test(url)) return this._getSVGDataURI(this.FAVICON_GLOBE); break; } return url; }, _getSVGDataURI(svg) { return `data:image/svg+xml,${encodeURIComponent(svg.trim())}`; }, // public async getLastEffectiveFavIconURL(tab) { if (tab.favIconUrl?.startsWith('data:')) return tab.favIconUrl; const uneffectiveFavIconUrl = await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_UNEFFECTIVE_FAVICONS }); if (uneffectiveFavIconUrl) return null; const favIconUrl = await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS }); if (favIconUrl) return favIconUrl; if (!this._sessionAPIAvailable) return null; const lastData = await browser.sessions.getTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON); return lastData && lastData.url == tab.url && lastData.favIconUrl; }, async _getEffectiveFavIconURL(tab, favIconUrl = null) { if (tab.favIconUrl?.startsWith('data:')) { browser.sessions.removeTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON); this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_UNEFFECTIVE_FAVICONS }); return tab.favIconUrl; } return new Promise(async (resolve, reject) => { favIconUrl = this._getSafeFaviconUrl(favIconUrl || tab.favIconUrl); let storedFavIconUrl; if (!favIconUrl && tab.discarded) { // discarded tab doesn't have favIconUrl, so we should use cached data. storedFavIconUrl = favIconUrl = await this.getLastEffectiveFavIconURL(tab); } let loader, onLoad, onError; const clear = (() => { if (loader) { loader.removeEventListener('load', onLoad, { once: true }); loader.removeEventListener('error', onError, { once: true }); } loader = onLoad = onError = favIconUrl = storedFavIconUrl = undefined; }); onLoad = async foundFavIconUrl => { let dataURL = null; if (this.DRAWABLE_FAVICON_PATTERN.test(favIconUrl)) { const context = this.canvas.getContext('2d'); context.clearRect(0, 0, this.FAVICON_SIZE, this.FAVICON_SIZE); context.drawImage(loader, 0, 0, this.FAVICON_SIZE, this.FAVICON_SIZE); try { dataURL = this.canvas.toDataURL('image/png'); } catch(_error) { // it can fail due to security reasons } } const oldFavIconUrl = foundFavIconUrl || await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS }); if (!oldFavIconUrl || oldFavIconUrl != favIconUrl) { if (this._sessionAPIAvailable) browser.sessions.setTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON, { url: tab.url, favIconUrl, }); } this._associateFavIconUrlToTabUrl({ tabUrl: tab.url, favIconUrl, store: this.STORE_EFFECTIVE_FAVICONS }); this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_UNEFFECTIVE_FAVICONS }); resolve(dataURL || favIconUrl); clear(); }; onError = async error => { this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS }); this._associateFavIconUrlToTabUrl({ tabUrl: tab.url, favIconUrl, store: this.STORE_UNEFFECTIVE_FAVICONS }); if (this._sessionAPIAvailable) browser.sessions.removeTabValue(tab.id, this.LAST_EFFECTIVE_FAVICON); clear(); reject(error || new Error('No effective icon')); }; storedFavIconUrl = storedFavIconUrl || await this._getAssociatedFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS }); if (storedFavIconUrl) return onLoad(storedFavIconUrl); if (!favIconUrl || !this.VALID_FAVICON_PATTERN.test(favIconUrl)) { onError(); return; } loader = new Image(); if (/^https?:/.test(favIconUrl)) loader.crossOrigin = 'anonymous'; loader.addEventListener('load', () => onLoad(), { once: true }); loader.addEventListener('error', onError, { once: true }); try { loader.src = favIconUrl; } catch(error) { this._unassociateFavIconUrlFromTabUrl({ tabUrl: tab.url, store: this.STORE_EFFECTIVE_FAVICONS }); this._associateFavIconUrlToTabUrl({ tabUrl: tab.url, favIconUrl, store: this.STORE_UNEFFECTIVE_FAVICONS }); onError(error); } }); }, _onTabUpdated(tabId, changeInfo, _tab) { if (!this._hasFavIconInfo(changeInfo)) return; let timer = this._updatingTabs.get(tabId); if (timer) clearTimeout(timer); // Updating of last effective favicon must be done after the loading // of the tab itself is correctly done, to avoid cookie problems on // some websites. // See also: https://github.com/piroor/treestyletab/issues/2064 timer = setTimeout(async () => { this._updatingTabs.delete(tabId); const tab = await browser.tabs.get(tabId); if (!tab || (changeInfo.favIconUrl && tab.favIconUrl != changeInfo.favIconUrl) || (changeInfo.url && tab.url != changeInfo.url) || !this._hasFavIconInfo(tab)) return; // expired await this._getEffectiveFavIconURL( tab, changeInfo.favIconUrl ).catch(_error => {}); }, 5000); this._updatingTabs.set(tabId, timer); }, _hasFavIconInfo(tabOrChangeInfo) { return 'favIconUrl' in tabOrChangeInfo; }, _updatingTabs: new Map(), }; TabFavIconHelper._init(); // eslint-disable-line no-underscore-dangle export default TabFavIconHelper;