const SYNC_DEBUG = false; const sync = { storageArea: { area: browser.storage.sync, async get(){ return this.area.get(); }, async set(options) { return this.area.set(options); }, async deleteIdentity(deletedIdentityUUID) { const deletedIdentityList = await sync.storageArea.getDeletedIdentityList(); if ( ! deletedIdentityList.find(element => element === deletedIdentityUUID) ) { deletedIdentityList.push(deletedIdentityUUID); await sync.storageArea.set({ deletedIdentityList }); } await this.removeIdentityKeyFromSync(deletedIdentityUUID); }, async removeIdentityKeyFromSync(deletedIdentityUUID) { await sync.storageArea.area.remove( "identity@@_" + deletedIdentityUUID); }, async deleteSite(siteStoreKey) { const deletedSiteList = await sync.storageArea.getDeletedSiteList(); if (deletedSiteList.find(element => element === siteStoreKey)) return; deletedSiteList.push(siteStoreKey); await sync.storageArea.set({ deletedSiteList }); await sync.storageArea.area.remove(siteStoreKey); }, async getDeletedIdentityList() { const storedArray = await this.getStoredItem("deletedIdentityList"); return storedArray || []; }, async getIdentities() { const allSyncStorage = await this.get(); const identities = []; for (const storageKey of Object.keys(allSyncStorage)) { if (storageKey.includes("identity@@_")) { identities.push(allSyncStorage[storageKey]); } } return identities; }, async getDeletedSiteList() { const storedArray = await this.getStoredItem("deletedSiteList"); return (storedArray) ? storedArray : []; }, async getAssignedSites() { const allSyncStorage = await this.get(); const sites = {}; for (const storageKey of Object.keys(allSyncStorage)) { if (storageKey.includes("siteContainerMap@@_")) { sites[storageKey] = allSyncStorage[storageKey]; } } return sites; }, async getStoredItem(objectKey) { const outputObject = await this.get(objectKey); if (outputObject && outputObject[objectKey]) return outputObject[objectKey]; return false; }, async getAllInstanceInfo() { const instanceList = {}; const allSyncInfo = await this.get(); for (const objectKey of Object.keys(allSyncInfo)) { if (objectKey.includes("MACinstance")) { instanceList[objectKey] = allSyncInfo[objectKey]; } } return instanceList; }, getInstanceKey() { return browser.runtime.getURL("") .replace(/moz-extension:\/\//, "MACinstance:") .replace(/\//, ""); }, async removeInstance(installUUID) { if (SYNC_DEBUG) console.log("removing", installUUID); await this.area.remove(installUUID); return; }, async removeThisInstanceFromSync() { const installUUID = this.getInstanceKey(); await this.removeInstance(installUUID); return; }, async hasSyncStorage(){ const inSync = await this.get(); return !(Object.entries(inSync).length === 0); }, async backup(options) { // remove listeners to avoid an infinite loop! await sync.checkForListenersMaybeRemove(); const identities = await updateSyncIdentities(); const siteAssignments = await updateSyncSiteAssignments(); await updateInstanceInfo(identities, siteAssignments); if (options && options.uuid) await this.deleteIdentity(options.uuid); if (options && options.undeleteUUID) await removeFromDeletedIdentityList(options.undeleteUUID); if (options && options.siteStoreKey) await this.deleteSite(options.siteStoreKey); if (options && options.undeleteSiteStoreKey) await removeFromDeletedSitesList(options.undeleteSiteStoreKey); if (SYNC_DEBUG) console.log("Backed up!"); await sync.checkForListenersMaybeAdd(); async function updateSyncIdentities() { const excludeRegExp = await getExcludeRegExp(); const identities = await browser.contextualIdentities.query({}); for (const identity of identities) { // skip excluded identities if (excludeRegExpMatchesIdentity(excludeRegExp, identity)) { continue; } delete identity.colorCode; delete identity.iconUrl; identity.macAddonUUID = await identityState.lookupMACaddonUUID(identity.cookieStoreId); if(identity.macAddonUUID) { const storageKey = "identity@@_" + identity.macAddonUUID; await sync.storageArea.set({ [storageKey]: identity }); } } //await sync.storageArea.set({ identities }); return identities; } async function updateSyncSiteAssignments() { const assignedSites = await assignManager.storageArea.getAssignedSites(); for (const siteKey of Object.keys(assignedSites)) { await sync.storageArea.set({ [siteKey]: assignedSites[siteKey] }); } return assignedSites; } async function updateInstanceInfo(identitiesInput, siteAssignmentsInput) { const date = new Date(); const timestamp = date.getTime(); const installUUID = sync.storageArea.getInstanceKey(); if (SYNC_DEBUG) console.log("adding", installUUID); const identities = []; const siteAssignments = []; for (const identity of identitiesInput) { identities.push(identity.macAddonUUID); } for (const siteAssignmentKey of Object.keys(siteAssignmentsInput)) { siteAssignments.push(siteAssignmentKey); } await sync.storageArea.set({ [installUUID]: { timestamp, identities, siteAssignments } }); } async function removeFromDeletedIdentityList(identityUUID) { const deletedIdentityList = await sync.storageArea.getDeletedIdentityList(); const newDeletedIdentityList = deletedIdentityList .filter(element => element !== identityUUID); await sync.storageArea.set({ deletedIdentityList: newDeletedIdentityList }); } async function removeFromDeletedSitesList(siteStoreKey) { const deletedSiteList = await sync.storageArea.getDeletedSiteList(); const newDeletedSiteList = deletedSiteList .filter(element => element !== siteStoreKey); await sync.storageArea.set({ deletedSiteList: newDeletedSiteList }); } }, onChangedListener(changes, areaName) { if (areaName === "sync") sync.errorHandledRunSync(); }, async addToDeletedList(changeInfo) { const identity = changeInfo.contextualIdentity; const deletedUUID = await identityState.lookupMACaddonUUID(identity.cookieStoreId); await identityState.storageArea.remove(identity.cookieStoreId); sync.storageArea.backup({uuid: deletedUUID}); } }, async init() { const syncEnabled = await assignManager.storageArea.getSyncEnabled(); if (syncEnabled) { // Add listener to sync storage and containers. // Works for all installs that have any sync storage. // Waits for sync storage change before kicking off the restore/backup // initial sync must be kicked off by user. this.checkForListenersMaybeAdd(); return; } this.checkForListenersMaybeRemove(); }, async errorHandledRunSync () { await sync.runSync().catch( async (error)=> { if (SYNC_DEBUG) console.error("Error from runSync", error); await sync.checkForListenersMaybeAdd(); }); }, async checkForListenersMaybeAdd() { const hasStorageListener = await browser.storage.onChanged.hasListener( sync.storageArea.onChangedListener ); const hasCIListener = await sync.hasContextualIdentityListeners(); if (!hasCIListener) { await sync.addContextualIdentityListeners(); } if (!hasStorageListener) { await browser.storage.onChanged.addListener( sync.storageArea.onChangedListener); } }, async checkForListenersMaybeRemove() { const hasStorageListener = await browser.storage.onChanged.hasListener( sync.storageArea.onChangedListener ); const hasCIListener = await sync.hasContextualIdentityListeners(); if (hasCIListener) { await sync.removeContextualIdentityListeners(); } if (hasStorageListener) { await browser.storage.onChanged.removeListener( sync.storageArea.onChangedListener); } }, async runSync() { if (SYNC_DEBUG) { const syncInfo = await sync.storageArea.get(); const localInfo = await browser.storage.local.get(); const idents = await browser.contextualIdentities.query({}); console.log("Initial State:", {syncInfo, localInfo, idents}); } await sync.checkForListenersMaybeRemove(); if (SYNC_DEBUG) console.log("runSync"); await identityState.storageArea.upgradeData(); await assignManager.storageArea.upgradeData(); const hasSyncStorage = await sync.storageArea.hasSyncStorage(); if (hasSyncStorage) await restore(); await sync.storageArea.backup(); await removeOldDeletedItems(); return; }, async addContextualIdentityListeners() { await browser.contextualIdentities.onCreated.addListener(sync.storageArea.backup); await browser.contextualIdentities.onRemoved.addListener(sync.storageArea.addToDeletedList); await browser.contextualIdentities.onUpdated.addListener(sync.storageArea.backup); }, async removeContextualIdentityListeners() { await browser.contextualIdentities.onCreated.removeListener(sync.storageArea.backup); await browser.contextualIdentities.onRemoved.removeListener(sync.storageArea.addToDeletedList); await browser.contextualIdentities.onUpdated.removeListener(sync.storageArea.backup); }, async hasContextualIdentityListeners() { return ( await browser.contextualIdentities.onCreated.hasListener(sync.storageArea.backup) && await browser.contextualIdentities.onRemoved.hasListener(sync.storageArea.addToDeletedList) && await browser.contextualIdentities.onUpdated.hasListener(sync.storageArea.backup) ); }, async resetSync() { const syncEnabled = await assignManager.storageArea.getSyncEnabled(); if (syncEnabled) { this.errorHandledRunSync(); return; } await this.checkForListenersMaybeRemove(); await this.storageArea.removeThisInstanceFromSync(); } }; // attaching to window for use in mocha tests window.sync = sync; sync.init(); async function restore() { if (SYNC_DEBUG) console.log("restore"); await reconcileIdentities(); await reconcileSiteAssignments(); return; } /* * Checks for the container name. If it exists, they are assumed to be the * same container, and the color and icon are overwritten from sync, if * different. */ async function reconcileIdentities(){ if (SYNC_DEBUG) console.log("reconcileIdentities"); const excludeRegExp = await getExcludeRegExp(); // first delete any from the deleted list const deletedIdentityList = await sync.storageArea.getDeletedIdentityList(); // first remove any deleted identities for (const deletedUUID of deletedIdentityList) { const deletedCookieStoreId = await identityState.lookupCookieStoreId(deletedUUID); if (deletedCookieStoreId){ if (excludeRegExp) { const deletedIdentity = await identityState.get(deletedCookieStoreId); // skip excluded identities if (excludeRegExpMatchesIdentity(excludeRegExp, deletedIdentity)) { continue; } } try{ await browser.contextualIdentities.remove(deletedCookieStoreId); } catch (error) { // if the identity we are deleting is not there, that's fine. console.error("Error deleting contextualIdentity", deletedCookieStoreId); continue; } } } const localIdentities = await browser.contextualIdentities.query({}); const syncIdentitiesRemoveDupes = await sync.storageArea.getIdentities(); // find any local dupes created on sync storage and delete from sync storage for (const localIdentity of localIdentities) { // skip excluded identities if (excludeRegExpMatchesIdentity(excludeRegExp, localIdentity)) { continue; } const syncIdentitiesOfName = syncIdentitiesRemoveDupes .filter(identity => identity.name === localIdentity.name); if (syncIdentitiesOfName.length > 1) { const identityMatchingContextId = syncIdentitiesOfName .find(identity => identity.cookieStoreId === localIdentity.cookieStoreId); if (identityMatchingContextId) await sync.storageArea.removeIdentityKeyFromSync(identityMatchingContextId.macAddonUUID); } } const syncIdentities = await sync.storageArea.getIdentities(); // now compare all containers for matching names. for (const syncIdentity of syncIdentities) { // skip excluded identities if (excludeRegExpMatchesIdentity(excludeRegExp, syncIdentity)) { continue; } if (syncIdentity.macAddonUUID){ const localMatch = localIdentities.find( localIdentity => localIdentity.name === syncIdentity.name ); if (!localMatch) { // if there's no name match found, check on uuid, const localCookieStoreID = await identityState.lookupCookieStoreId(syncIdentity.macAddonUUID); if (localCookieStoreID) { await ifUUIDMatch(syncIdentity, localCookieStoreID); continue; } await ifNoMatch(syncIdentity); continue; } // Names match, so use the info from Sync await updateIdentityWithSyncInfo(syncIdentity, localMatch); continue; } // if no macAddonUUID, there is a problem with the sync info and it needs to be ignored. } await updateSiteAssignmentUUIDs(); async function updateSiteAssignmentUUIDs(){ const sites = assignManager.storageArea.getAssignedSites(); for (const siteKey of Object.keys(sites)) { await assignManager.storageArea.set(siteKey, sites[siteKey]); } } } async function updateIdentityWithSyncInfo(syncIdentity, localMatch) { // Sync is truth. if there is a match, compare data and update as needed if (syncIdentity.color !== localMatch.color || syncIdentity.icon !== localMatch.icon) { await browser.contextualIdentities.update( localMatch.cookieStoreId, { name: syncIdentity.name, color: syncIdentity.color, icon: syncIdentity.icon }); if (SYNC_DEBUG) { if (localMatch.color !== syncIdentity.color) { console.log(localMatch.name, "Change color: ", syncIdentity.color); } if (localMatch.icon !== syncIdentity.icon) { console.log(localMatch.name, "Change icon: ", syncIdentity.icon); } } } // Sync is truth. If all is the same, update the local uuid to match sync if (localMatch.macAddonUUID !== syncIdentity.macAddonUUID) { await identityState.updateUUID( localMatch.cookieStoreId, syncIdentity.macAddonUUID ); } // TODOkmw: update any site assignment UUIDs } async function ifUUIDMatch(syncIdentity, localCookieStoreID) { // if there's an identical local uuid, it's the same container. Sync is truth const identityInfo = { name: syncIdentity.name, color: syncIdentity.color, icon: syncIdentity.icon }; if (SYNC_DEBUG) { try { const getIdent = await browser.contextualIdentities.get(localCookieStoreID); if (getIdent.name !== identityInfo.name) { console.log(getIdent.name, "Change name: ", identityInfo.name); } if (getIdent.color !== identityInfo.color) { console.log(getIdent.name, "Change color: ", identityInfo.color); } if (getIdent.icon !== identityInfo.icon) { console.log(getIdent.name, "Change icon: ", identityInfo.icon); } } catch (error) { //if this fails, there is probably differing sync info. console.error("Error getting info on CI", error); } } try { // update the local container with the sync data await browser.contextualIdentities .update(localCookieStoreID, identityInfo); return; } catch (error) { // If this fails, sync info is off. console.error("Error udpating CI", error); } } async function ifNoMatch(syncIdentity){ // if no uuid match either, make new identity if (SYNC_DEBUG) console.log("create new ident: ", syncIdentity.name); const newIdentity = await browser.contextualIdentities.create({ name: syncIdentity.name, color: syncIdentity.color, icon: syncIdentity.icon }); await identityState.updateUUID( newIdentity.cookieStoreId, syncIdentity.macAddonUUID ); return; } /* * Checks for site previously assigned. If it exists, and has the same * container assignment, the assignment is kept. If it exists, but has * a different assignment, the user is prompted (not yet implemented). * If it does not exist, it is created. */ async function reconcileSiteAssignments() { if (SYNC_DEBUG) console.log("reconcileSiteAssignments"); const assignedSitesLocal = await assignManager.storageArea.getAssignedSites(); const assignedSitesFromSync = await sync.storageArea.getAssignedSites(); const deletedSiteList = await sync.storageArea.getDeletedSiteList(); for(const siteStoreKey of deletedSiteList) { if (Object.prototype.hasOwnProperty.call(assignedSitesLocal,siteStoreKey)) { await assignManager .storageArea .remove(siteStoreKey, false); } } for(const urlKey of Object.keys(assignedSitesFromSync)) { const assignedSite = assignedSitesFromSync[urlKey]; try{ if (assignedSite.identityMacAddonUUID) { // Sync is truth. // Not even looking it up. Just overwrite if (SYNC_DEBUG){ const isInStorage = await assignManager.storageArea.getByUrlKey(urlKey); if (!isInStorage) console.log("new assignment ", assignedSite); } await setAssignmentWithUUID(assignedSite, urlKey); continue; } } catch (error) { // this is probably old or incorrect site info in Sync // skip and move on. } } } const MILISECONDS_IN_THIRTY_DAYS = 2592000000; async function removeOldDeletedItems() { const instanceList = await sync.storageArea.getAllInstanceInfo(); const deletedSiteList = await sync.storageArea.getDeletedSiteList(); const deletedIdentityList = await sync.storageArea.getDeletedIdentityList(); for (const instanceKey of Object.keys(instanceList)) { const date = new Date(); const currentTimestamp = date.getTime(); if (instanceList[instanceKey].timestamp < currentTimestamp - MILISECONDS_IN_THIRTY_DAYS) { delete instanceList[instanceKey]; sync.storageArea.removeInstance(instanceKey); continue; } } for (const siteStoreKey of deletedSiteList) { let hasMatch = false; for (const instance of Object.values(instanceList)) { const match = instance.siteAssignments.find(element => element === siteStoreKey); if (!match) continue; hasMatch = true; } if (!hasMatch) { await sync.storageArea.backup({undeleteSiteStoreKey: siteStoreKey}); } } for (const identityUUID of deletedIdentityList) { let hasMatch = false; for (const instance of Object.values(instanceList)) { const match = instance.identities.find(element => element === identityUUID); if (!match) continue; hasMatch = true; } if (!hasMatch) { await sync.storageArea.backup({undeleteUUID: identityUUID}); } } } async function setAssignmentWithUUID(assignedSite, urlKey) { const uuid = assignedSite.identityMacAddonUUID; const cookieStoreId = await identityState.lookupCookieStoreId(uuid); if (cookieStoreId) { // eslint-disable-next-line require-atomic-updates assignedSite.userContextId = cookieStoreId .replace(/^firefox-container-/, ""); await assignManager.storageArea.set( urlKey, assignedSite, false, false ); return; } throw new Error (`No cookieStoreId found for: ${uuid}, ${urlKey}`); } // Retrieve the sync exclude regexp from local storage. async function getExcludeRegExp() { const { syncExcludeRegExp } = await browser.storage.local.get("syncExcludeRegExp"); if (syncExcludeRegExp) { return new RegExp(syncExcludeRegExp, "i"); } else { return false; } } // Matching the provided exclude regexp against the provided identity and return // true if they match. function excludeRegExpMatchesIdentity(excludeRegExp, identity) { if (excludeRegExp && identity.name.match(excludeRegExp)) { if (SYNC_DEBUG) console.log(`Exclude regexp matches identity '${identity.name}'`); return true; } else { return false; } }