462 lines
15 KiB
JavaScript
462 lines
15 KiB
JavaScript
const SYNC_DEBUG = true;
|
|
|
|
const sync = {
|
|
storageArea: {
|
|
area: browser.storage.sync,
|
|
|
|
async get(){
|
|
return await this.area.get();
|
|
},
|
|
|
|
async set(options) {
|
|
return await this.area.set(options);
|
|
},
|
|
|
|
async getDeletedIdentityList() {
|
|
const storedArray = await this.getStoredItem("deletedIdentityList");
|
|
return (storedArray) ? storedArray : [];
|
|
},
|
|
|
|
async getIdentities() {
|
|
const storedArray = await this.getStoredItem("identities");
|
|
return (storedArray) ? storedArray : [];
|
|
},
|
|
|
|
async getDeletedSiteList() {
|
|
const storedArray = await this.getStoredItem("deletedSiteList");
|
|
return (storedArray) ? storedArray : [];
|
|
},
|
|
|
|
async getCookieStoreIDMap() {
|
|
const storedArray = await this.getStoredItem("cookieStoreIDmap");
|
|
return (storedArray) ? storedArray : {};
|
|
},
|
|
|
|
async getAssignedSites() {
|
|
const storedArray = await this.getStoredItem("assignedSites");
|
|
return (storedArray) ? storedArray : {};
|
|
},
|
|
|
|
async getStoredItem(objectKey) {
|
|
const outputObject = await this.get(objectKey);
|
|
if (outputObject && outputObject[objectKey])
|
|
return outputObject[objectKey];
|
|
return false;
|
|
},
|
|
|
|
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();
|
|
|
|
await updateSyncIdentities();
|
|
await updateCookieStoreIdMap();
|
|
await updateSyncSiteAssignments();
|
|
if (options && options.uuid)
|
|
await updateDeletedIdentityList(options.uuid);
|
|
if (options && options.siteStoreKey)
|
|
await addToDeletedSitesList(options.siteStoreKey);
|
|
if (options && options.undelete)
|
|
await removeFromDeletedSitesList(options.undelete);
|
|
// if (SYNC_DEBUG) {
|
|
// const storage = await sync.storageArea.get();
|
|
// console.log("inSync: ", storage);
|
|
// const localStorage = await browser.storage.local.get();
|
|
// console.log("inLocal:", localStorage);
|
|
// console.log("idents: ", await browser.contextualIdentities.query({}));
|
|
// }
|
|
console.log("Backed up!");
|
|
await sync.checkForListenersMaybeAdd();
|
|
|
|
async function updateSyncIdentities() {
|
|
const identities = await browser.contextualIdentities.query({});
|
|
await sync.storageArea.set({ identities });
|
|
}
|
|
|
|
async function updateCookieStoreIdMap() {
|
|
const cookieStoreIDmap =
|
|
await identityState.getCookieStoreIDuuidMap();
|
|
await sync.storageArea.set({ cookieStoreIDmap });
|
|
}
|
|
|
|
async function updateSyncSiteAssignments() {
|
|
const assignedSites =
|
|
await assignManager.storageArea.getAssignedSites();
|
|
await sync.storageArea.set({ assignedSites });
|
|
}
|
|
|
|
async function updateDeletedIdentityList(deletedIdentityUUID) {
|
|
const deletedIdentityList =
|
|
await sync.storageArea.getDeletedIdentityList();
|
|
if (
|
|
deletedIdentityList.find(element => element === deletedIdentityUUID)
|
|
) return;
|
|
deletedIdentityList.push(deletedIdentityUUID);
|
|
await sync.storageArea.set({ deletedIdentityList });
|
|
}
|
|
|
|
async function addToDeletedSitesList(siteStoreKey) {
|
|
const deletedSiteList =
|
|
await sync.storageArea.getDeletedSiteList();
|
|
if (deletedSiteList.find(element => element === siteStoreKey)) return;
|
|
deletedSiteList.push(siteStoreKey);
|
|
await sync.storageArea.set({ deletedSiteList });
|
|
}
|
|
|
|
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 dataIsReliable() {
|
|
const cookieStoreIDmap = await this.getCookieStoreIDMap();
|
|
const identities = await this.getIdentities();
|
|
for (const cookieStoreId of Object.keys(cookieStoreIDmap)) {
|
|
const match = identities.find(identity =>
|
|
identity.cookieStoreId === cookieStoreId
|
|
);
|
|
// if one has no match, this is bad data.
|
|
if (!match) return false;
|
|
}
|
|
return !(Object.entries(cookieStoreIDmap).length === 0);
|
|
}
|
|
},
|
|
|
|
init() {
|
|
browser.runtime.onInstalled.addListener(this.errorHandledRunSync);
|
|
browser.runtime.onStartup.addListener(this.errorHandledRunSync);
|
|
},
|
|
|
|
async errorHandledRunSync () {
|
|
sync.runSync().catch(async (error)=> {
|
|
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();
|
|
console.log("runSync");
|
|
|
|
await identityState.storageArea.upgradeData();
|
|
await assignManager.storageArea.upgradeData();
|
|
|
|
const hasSyncStorage = await sync.storageArea.hasSyncStorage();
|
|
const dataIsReliable = await sync.storageArea.dataIsReliable();
|
|
if (hasSyncStorage && dataIsReliable) await restore();
|
|
|
|
await sync.storageArea.backup();
|
|
return;
|
|
},
|
|
|
|
async addContextualIdentityListeners(listenerList) {
|
|
if(!listenerList) listenerList = syncCIListenerList;
|
|
await browser.contextualIdentities.onCreated.addListener(listenerList[0]);
|
|
await browser.contextualIdentities.onRemoved.addListener(listenerList[1]);
|
|
await browser.contextualIdentities.onUpdated.addListener(listenerList[2]);
|
|
},
|
|
|
|
async removeContextualIdentityListeners(listenerList) {
|
|
if(!listenerList) listenerList = syncCIListenerList;
|
|
await browser.contextualIdentities.onCreated.removeListener(listenerList[0]);
|
|
await browser.contextualIdentities.onRemoved.removeListener(listenerList[1]);
|
|
await browser.contextualIdentities.onUpdated.removeListener(listenerList[2]);
|
|
},
|
|
|
|
async hasContextualIdentityListeners(listenerList) {
|
|
if(!listenerList) listenerList = syncCIListenerList;
|
|
return (
|
|
await browser.contextualIdentities.onCreated.hasListener(listenerList[0]) &&
|
|
await browser.contextualIdentities.onRemoved.hasListener(listenerList[1]) &&
|
|
await browser.contextualIdentities.onUpdated.hasListener(listenerList[2])
|
|
);
|
|
}
|
|
|
|
};
|
|
|
|
sync.init();
|
|
|
|
async function restore() {
|
|
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(){
|
|
console.log("reconcileIdentities");
|
|
|
|
// 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){
|
|
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 syncIdentities =
|
|
await sync.storageArea.getIdentities();
|
|
const cookieStoreIDmap =
|
|
await sync.storageArea.getCookieStoreIDMap();
|
|
// now compare all containers for matching names.
|
|
for (const syncIdentity of syncIdentities) {
|
|
syncIdentity.macAddonUUID = cookieStoreIDmap[syncIdentity.cookieStoreId];
|
|
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;
|
|
}
|
|
await ifNamesMatch(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 ifNamesMatch(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
|
|
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
|
|
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() {
|
|
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)) {
|
|
assignManager
|
|
.storageArea
|
|
.remove(siteStoreKey.replace(/^siteContainerMap@@_/, "https://"));
|
|
}
|
|
}
|
|
|
|
for(const urlKey of Object.keys(assignedSitesFromSync)) {
|
|
const assignedSite = assignedSitesFromSync[urlKey];
|
|
try{
|
|
const syncUUID = assignedSite.identityMacAddonUUID;
|
|
console.log("syncUUID, ", syncUUID);
|
|
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.
|
|
}
|
|
}
|
|
}
|
|
|
|
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.replace(/^siteContainerMap@@_/, "https://"),
|
|
assignedSite,
|
|
false,
|
|
false
|
|
);
|
|
return;
|
|
}
|
|
throw new Error (`No cookieStoreId found for: ${uuid}, ${urlKey}`);
|
|
}
|
|
|
|
const syncCIListenerList = [
|
|
sync.storageArea.backup,
|
|
sync.storageArea.addToDeletedList,
|
|
sync.storageArea.backup
|
|
];
|