/*
# 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,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACGFjVEwAAAASAAAAAJNtBPIAAAAaZmNUTAAAAAAAAAAQAAAAEAAAAAAAAAAALuAD6AABhIDeugAAALhJREFUOI2Nk8sNxCAMRDlGohauXFOMpfTiAlxICqAELltHLqlgctg1InzMRhpFAc+LGWTnmoeZYamt78zXdZmaQtQMADlnU0OIAlbmJUBEcO4bRKQY2rUXIPmAGnDuG/Bx3/fvOPVaDUg+oAPUf1PArIMCSD5glMEsUGaG+kyAFWIBaCsKuA+HGCNijLgP133XgOEtaPFMy2vUolEGJoCIzBmoRUR9+7rxj16DZaW/mgtmxnJ8V3oAnApQwNS5zpcAAAAaZmNUTAAAAAEAAAAQAAAAEAAAAAAAAAAAAB4D6AIB52fclgAAACpmZEFUAAAAAjiNY2AYBVhBc3Pzf2LEcGreqcbwH1kDNjHauWAUjAJyAADymxf9WF+u8QAAABpmY1RMAAAAAwAAABAAAAAQAAAAAAAAAAAAHgPoAgEK8Q9/AAAAFmZkQVQAAAAEOI1jYBgFo2AUjAIIAAAEEAAB0xIn4wAAABpmY1RMAAAABQAAABAAAAAQAAAAAAAAAAAAHgPoAgHnO30FAAAAQGZkQVQAAAAGOI1jYBieYKcaw39ixHCC/6cwFWMTw2rz/1MM/6Vu/f///xTD/51qEIwuRjsXILuEGLFRMApgAADhNCsVfozYcAAAABpmY1RMAAAABwAAABAAAAAQAAAAAAAAAAAAHgPoAgEKra7sAAAAFmZkQVQAAAAIOI1jYBgFo2AUjAIIAAAEEAABM9s3hAAAABpmY1RMAAAACQAAABAAAAAQAAAAAAAAAAAAHgPoAgHn3p+wAAAAKmZkQVQAAAAKOI1jYBgFWEFzc/N/YsRwat6pxvAfWQM2Mdq5YBSMAnIAAPKbF/1BhPl6AAAAGmZjVEwAAAALAAAAEAAAABAAAAAAAAAAAAAeA+gCAQpITFkAAAAWZmRBVAAAAAw4jWNgGAWjYBSMAggAAAQQAAHaszpmAAAAGmZjVEwAAAANAAAAEAAAABAAAAAAAAAAAAAeA+gCAeeCPiMAAABAZmRBVAAAAA44jWNgGJ5gpxrDf2LEcIL/pzAVYxPDavP/Uwz/pW79////FMP/nWoQjC5GOxcgu4QYsVEwCmAAAOE0KxUmBL0KAAAAGmZjVEwAAAAPAAAAEAAAABAAAAAAAAAAAAAeA+gCAQoU7coAAAAWZmRBVAAAABA4jWNgGAWjYBSMAggAAAQQAAEpOBELAAAAGmZjVEwAAAARAAAAEAAAABAAAAAAAAAAAAAeA+gCAeYVWtoAAAAqZmRBVAAAABI4jWNgGAVYQXNz839ixHBq3qnG8B9ZAzYx2rlgFIwCcgAA8psX/WvpAecAAAAaZmNUTAAAABMAAAAQAAAAEAAAAAAAAAAAAB4D6AIBC4OJMwAAABZmZEFUAAAAFDiNY2AYBaNgFIwCCAAABBAAAcBQHOkAAAAaZmNUTAAAABUAAAAQAAAAEAAAAAAAAAAAAB4D6AIB5kn7SQAAAEBmZEFUAAAAFjiNY2AYnmCnGsN/YsRwgv+nMBVjE8Nq8/9TDP+lbv3///8Uw/+dahCMLkY7FyC7hBixUTAKYAAA4TQrFc+cEoQAAAAaZmNUTAAAABcAAAAQAAAAEAAAAAAAAAAAAB4D6AIBC98ooAAAABZmZEFUAAAAGDiNY2AYBaNgFIwCCAAABBAAASCZDI4AAAAaZmNUTAAAABkAAAAQAAAAEAAAAAAAAAAAAB4D6AIB5qwZ/AAAACpmZEFUAAAAGjiNY2AYBVhBc3Pzf2LEcGreqcbwH1kDNjHauWAUjAJyAADymxf9cjJWbAAAABpmY1RMAAAAGwAAABAAAAAQAAAAAAAAAAAAHgPoAgELOsoVAAAAFmZkQVQAAAAcOI1jYBgFo2AUjAIIAAAEEAAByfEBbAAAABpmY1RMAAAAHQAAABAAAAAQAAAAAAAAAAAAHgPoAgHm8LhvAAAAQGZkQVQAAAAeOI1jYBieYKcaw39ixHCC/6cwFWMTw2rz/1MM/6Vu/f///xTD/51qEIwuRjsXILuEGLFRMApgAADhNCsVlxR3/gAAABpmY1RMAAAAHwAAABAAAAAQAAAAAAAAAAAAHgPoAgELZmuGAAAAFmZkQVQAAAAgOI1jYBgFo2AUjAIIAAAEEAABHP5cFQAAABpmY1RMAAAAIQAAABAAAAAQAAAAAAAAAAAAHgPoAgHlgtAOAAAAKmZkQVQAAAAiOI1jYBgFWEFzc/N/YsRwat6pxvAfWQM2Mdq5YBSMAnIAAPKbF/0/MvDdAAAAAElFTkSuQmCC
`.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;