Skip to content

Commit

Permalink
Auto send users to sites they assigned to a container. Fixes #306
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanKingston committed Apr 3, 2017
1 parent 9e1da08 commit 3e657a2
Show file tree
Hide file tree
Showing 8 changed files with 381 additions and 26 deletions.
30 changes: 14 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const ContainerService = {
_identitiesState: {},
_windowMap: new Map(),
_containerWasEnabled: false,
_onThemeChangedCallback: null,
_onBackgroundConnectCallback: null,

init(installation, reason) {
// If we are just been installed, we must store some information for the
Expand Down Expand Up @@ -260,7 +260,7 @@ const ContainerService = {
}
});

this.registerThemeConnection(api);
this.registerBackgroundConnection(api);
}).catch(() => {
throw new Error("WebExtension startup failed. Unable to continue.");
});
Expand Down Expand Up @@ -307,28 +307,28 @@ const ContainerService = {
Services.obs.addObserver(this, "lightweight-theme-changed", false);
},

registerThemeConnection(api) {
// This is only used for theme notifications
registerBackgroundConnection(api) {
// This is only used for theme and container deletion notifications
api.browser.runtime.onConnect.addListener((port) => {
this.onThemeChanged((theme, topic) => {
this._onBackgroundConnectCallback = (message, topic) => {
port.postMessage({
type: topic,
theme
message
});
});
};
});
},

triggerThemeChanged(theme, topic) {
if (this._onThemeChangedCallback) {
this._onThemeChangedCallback(theme, topic);
triggerBackgroundCallback(message, topic) {
if (this._onBackgroundConnectCallback) {
this._onBackgroundConnectCallback(message, topic);
}
},

observe(subject, topic) {
if (topic === "lightweight-theme-changed") {
this.getTheme().then((theme) => {
this.triggerThemeChanged(theme, topic);
this.triggerBackgroundCallback(theme, topic);
}).catch(() => {
throw new Error("Unable to get theme");
});
Expand All @@ -346,10 +346,6 @@ const ContainerService = {
});
},

onThemeChanged(callback) {
this._onThemeChangedCallback = callback;
},

// utility methods

_containerTabCount(userContextId) {
Expand Down Expand Up @@ -960,12 +956,13 @@ const ContainerService = {
},

removeIdentity(args) {
const eventName = "delete-container";
if (!("userContextId" in args)) {
return Promise.reject("removeIdentity must be called with userContextId argument.");
}

this.sendTelemetryPayload({
"event": "delete-container",
"event": eventName,
"userContextId": args.userContextId
});

Expand All @@ -976,6 +973,7 @@ const ContainerService = {

return this._closeTabs(tabsToClose).then(() => {
const removed = ContextualIdentityProxy.remove(args.userContextId);
this.triggerBackgroundCallback({userContextId: args.userContextId}, eventName);
this._forgetIdentity(args.userContextId);
return this._refreshNeeded().then(() => removed );
});
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "testpilot-containers",
"title": "Containers Experiment",
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
"version": "2.0.0",
"version": "2.1.0",
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
"bugs": {
"url": "https://github.com/mozilla/testpilot-containers/issues"
Expand Down
265 changes: 260 additions & 5 deletions webextension/background.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,266 @@
const themeManager = {
existingTheme: null,
const assignManager = {
CLOSEABLE_WINDOWS: new Set([
"about:startpage",
"about:newtab",
"about:home",
"about:blank"
]),
MENU_ASSIGN_ID: "open-in-this-container",
MENU_REMOVE_ID: "remove-open-in-this-container",
storageArea: {
area: browser.storage.local,

getSiteStoreKey(pageUrl) {
const url = new window.URL(pageUrl);
const storagePrefix = "siteContainerMap@@_";
return `${storagePrefix}${url.hostname}`;
},

get(pageUrl) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
return new Promise((resolve, reject) => {
this.area.get([siteStoreKey]).then((storageResponse) => {
if (storageResponse && siteStoreKey in storageResponse) {
resolve(storageResponse[siteStoreKey]);
}
resolve(null);
}).catch((e) => {
reject(e);
});
});
},

set(pageUrl, data) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
return this.area.set({
[siteStoreKey]: data
});
},

remove(pageUrl) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
return this.area.remove([siteStoreKey]);
},

deleteContainer(userContextId) {
const removeKeys = [];
this.area.get().then((siteConfigs) => {
Object.keys(siteConfigs).forEach((key) => {
// For some reason this is stored as string... lets check them both as that
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
removeKeys.push(key);
}
});
this.area.remove(removeKeys);
}).catch((e) => {
throw e;
});
}
},

init() {
this.check();
browser.tabs.onActivated.addListener((info) => {
browser.tabs.get(info.tabId).then((tab) => {
this.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
});

browser.windows.onFocusChanged.addListener((windowId) => {
browser.tabs.query({active: true, windowId}).then((tabs) => {
if (tabs && tabs[0]) {
this.calculateContextMenu(tabs[0]);
}
}).catch((e) => {
throw e;
});
});

browser.runtime.onMessage.addListener((neverAskMessage) => {
const pageUrl = neverAskMessage.pageUrl;
if (neverAskMessage.neverAsk === true) {
// If we have existing data and for some reason it hasn't been deleted etc lets update it
this.storageArea.get(pageUrl).then((siteSettings) => {
if (siteSettings) {
siteSettings.neverAsk = true;
this.storageArea.set(pageUrl, siteSettings);
}
}).catch((e) => {
throw e;
});
}
});

browser.contextMenus.onClicked.addListener((info, tab) => {
const userContextId = this.getUserContextIdFromCookieStore(tab);
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
if (userContextId) {
let actionName;
let storageAction;
if (info.menuItemId === this.MENU_ASSIGN_ID) {
actionName = "added";
storageAction = this.storageArea.set(info.pageUrl, {
userContextId,
neverAsk: false
});
} else {
actionName = "removed";
storageAction = this.storageArea.remove(info.pageUrl);
}
storageAction.then(() => {
browser.notifications.create({
type: "basic",
title: "Containers",
message: `Successfully ${actionName} site to always open in this container`,
iconUrl: browser.extension.getURL("/img/onboarding-1.png")
});
this.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
}
});

browser.webRequest.onBeforeRequest.addListener((options) => {
if (options.frameId !== 0 || options.tabId === -1) {
return {};
}
return Promise.all([
browser.tabs.get(options.tabId),
this.storageArea.get(options.url)
]).then(([tab, siteSettings]) => {
const userContextId = this.getUserContextIdFromCookieStore(tab);
if (!siteSettings
|| userContextId === siteSettings.userContextId
|| tab.incognito) {
return {};
}

this.reloadPageInContainer(options.url, siteSettings.userContextId, tab.index, siteSettings.neverAsk);
this.calculateContextMenu(tab);
// If the user just opened the tab, we can auto close it
if (this.CLOSEABLE_WINDOWS.has(tab.url)) {
browser.tabs.remove(tab.id);
}
return {
cancel: true,
};
}).catch((e) => {
throw e;
});
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);

browser.webRequest.onCompleted.addListener((options) => {
if (options.frameId !== 0 || options.tabId === -1) {
return {};
}
browser.tabs.get(options.tabId).then((tab) => {
this.calculateContextMenu(tab);
}).catch((e) => {
throw e;
});
},{urls: ["<all_urls>"], types: ["main_frame"]});
},


deleteContainer(userContextId) {
this.storageArea.deleteContainer(userContextId);
},

getUserContextIdFromCookieStore(tab) {
if (!("cookieStoreId" in tab)) {
return false;
}
const cookieStore = tab.cookieStoreId;
const container = cookieStore.replace("firefox-container-", "");
if (container !== cookieStore) {
return container;
}
return false;
},

isTabPermittedAssign(tab) {
// Ensure we are not an important about url
// Ensure we are not in incognito mode
if (this.CLOSEABLE_WINDOWS.has(tab.url)
|| tab.incognito) {
return false;
}
return true;
},

calculateContextMenu(tab) {
// There is a focus issue in this menu where if you change window with a context menu click
// you get the wrong menu display because of async
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16
// We also can't change for always private mode
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
const cookieStore = this.getUserContextIdFromCookieStore(tab);
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
browser.contextMenus.remove(this.MENU_REMOVE_ID);
// Ensure we have a cookieStore to assign to
if (cookieStore
&& this.isTabPermittedAssign(tab)) {
this.storageArea.get(tab.url).then((siteSettings) => {
// ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418
let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick
let menuId = this.MENU_ASSIGN_ID;
if (siteSettings) {
prefix = "✓";
menuId = this.MENU_REMOVE_ID;
}
browser.contextMenus.create({
id: menuId,
title: `${prefix} Always Open in This Container`,
checked: true,
contexts: ["all"],
});
}).catch((e) => {
throw e;
});
}
},

reloadPageInContainer(url, userContextId, index, neverAsk = false) {
const loadPage = browser.extension.getURL("confirm-page.html");
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
if (neverAsk) {
browser.tabs.create({url, cookieStoreId: `firefox-container-${userContextId}`, index});
} else {
const confirmUrl = `${loadPage}?url=${url}`;
browser.tabs.create({url: confirmUrl, cookieStoreId: `firefox-container-${userContextId}`, index}).then(() => {
// We don't want to sync this URL ever nor clutter the users history
browser.history.deleteUrl({url: confirmUrl});
}).catch((e) => {
throw e;
});
}
}
};

const messageHandler = {
init() {
const port = browser.runtime.connect();
port.onMessage.addListener(m => {
if (m.type === "lightweight-theme-changed") {
this.update(m.theme);
switch (m.type) {
case "lightweight-theme-changed":
themeManager.update(m.message);
break;
case "delete-container":
assignManager.deleteContainer(m.message.userContextId);
break;
default:
throw new Error(`Unhandled message type: ${m.message}`);
}
});
}
};

const themeManager = {
existingTheme: null,
init() {
this.check();
},
setPopupIcon(theme) {
let icons = {
Expand Down Expand Up @@ -77,8 +329,11 @@ const tabPageCounter = {
}
};

assignManager.init();
themeManager.init();
tabPageCounter.init();
// Lets do this last as theme manager did a check before connecting before
messageHandler.init();

browser.runtime.sendMessage({
method: "getPreference",
Expand Down
Loading

0 comments on commit 3e657a2

Please sign in to comment.