--- a/toolkit/components/extensions/ext-contextualIdentities.js
+++ b/toolkit/components/extensions/ext-contextualIdentities.js
@@ -5,42 +5,98 @@
XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
"resource://gre/modules/ContextualIdentityService.jsm");
XPCOMUtils.defineLazyPreferenceGetter(this, "containersEnabled",
"privacy.userContext.enabled");
Cu.import("resource://gre/modules/ExtensionPreferencesManager.jsm");
+var {
+ ExtensionError,
+} = ExtensionUtils;
+
const CONTAINER_PREF_INSTALL_DEFAULTS = {
"privacy.userContext.enabled": true,
"privacy.userContext.longPressBehavior": 2,
"privacy.userContext.ui.enabled": true,
"privacy.usercontext.about_newtab_segregation.enabled": true,
};
const CONTAINERS_ENABLED_SETTING_NAME = "privacy.containers";
+const CONTAINER_COLORS = new Map([
+ ["blue", "#37adff"],
+ ["turquoise", "#00c79a"],
+ ["green", "#51cd00"],
+ ["yellow", "#ffcb00"],
+ ["orange", "#ff9f00"],
+ ["red", "#ff613d"],
+ ["pink", "#ff4bda"],
+ ["purple", "#af51f5"],
+]);
+
+const CONTAINER_ICONS = new Set([
+ "briefcase",
+ "cart",
+ "circle",
+ "dollar",
+ "fingerprint",
+ "gift",
+ "vacation",
+ "food",
+ "fruit",
+ "pet",
+ "tree",
+ "chill",
+]);
+
+function getContainerIcon(iconName) {
+ if (!CONTAINER_ICONS.has(iconName)) {
+ throw new ExtensionError(`Invalid icon ${iconName} for container`);
+ }
+ return `resource://usercontext-content/${iconName}.svg`;
+}
+
+function getContainerColor(colorName) {
+ if (!CONTAINER_COLORS.has(colorName)) {
+ throw new ExtensionError(`Invalid color name ${colorName} for container`);
+ }
+ return CONTAINER_COLORS.get(colorName);
+}
+
const convertIdentity = identity => {
let result = {
name: ContextualIdentityService.getUserContextLabel(identity.userContextId),
icon: identity.icon,
+ iconUrl: getContainerIcon(identity.icon),
color: identity.color,
+ colorCode: getContainerColor(identity.color),
cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
};
return result;
};
const convertIdentityFromObserver = wrappedIdentity => {
let identity = wrappedIdentity.wrappedJSObject;
+ let iconUrl, colorCode;
+ try {
+ iconUrl = getContainerIcon(identity.icon);
+ colorCode = getContainerColor(identity.color);
+ } catch (e) {
+ return null;
+ }
+
let result = {
name: identity.name,
icon: identity.icon,
+ iconUrl,
color: identity.color,
+ colorCode,
cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
};
return result;
};
ExtensionPreferencesManager.addSetting(CONTAINERS_ENABLED_SETTING_NAME, {
prefNames: Object.keys(CONTAINER_PREF_INSTALL_DEFAULTS),
@@ -65,47 +121,51 @@ this.contextualIdentities = class extend
if (extension.hasPermission("contextualIdentities")) {
ExtensionPreferencesManager.setSetting(extension, CONTAINERS_ENABLED_SETTING_NAME, true);
}
}
getAPI(context) {
let self = {
contextualIdentities: {
- get(cookieStoreId) {
+ async get(cookieStoreId) {
let containerId = getContainerForCookieStoreId(cookieStoreId);
if (!containerId) {
return Promise.reject({
message: `Invalid contextual identitiy: ${cookieStoreId}`,
});
}
let identity = ContextualIdentityService.getPublicIdentityFromId(containerId);
- return Promise.resolve(convertIdentity(identity));
+ return convertIdentity(identity);
},
- query(details) {
+ async query(details) {
let identities = [];
ContextualIdentityService.getPublicIdentities().forEach(identity => {
if (details.name &&
ContextualIdentityService.getUserContextLabel(identity.userContextId) != details.name) {
return;
}
identities.push(convertIdentity(identity));
});
- return Promise.resolve(identities);
+ return identities;
},
- create(details) {
+ async create(details) {
+ // Lets prevent making containers that are not valid
+ getContainerIcon(details.icon);
+ getContainerColor(details.color);
+
let identity = ContextualIdentityService.create(details.name,
details.icon,
details.color);
- return Promise.resolve(convertIdentity(identity));
+ return convertIdentity(identity);
},
update(cookieStoreId, details) {
let containerId = getContainerForCookieStoreId(cookieStoreId);
if (!containerId) {
return Promise.reject({
message: `Invalid contextual identitiy: ${cookieStoreId}`,
});
@@ -133,20 +193,20 @@ this.contextualIdentities = class extend
if (!ContextualIdentityService.update(identity.userContextId,
identity.name, identity.icon,
identity.color)) {
return Promise.reject({
message: `Contextual identitiy failed to update: ${cookieStoreId}`,
});
}
- return Promise.resolve(convertIdentity(identity));
+ return convertIdentity(identity);
},
- remove(cookieStoreId) {
+ async remove(cookieStoreId) {
let containerId = getContainerForCookieStoreId(cookieStoreId);
if (!containerId) {
return Promise.reject({
message: `Invalid contextual identitiy: ${cookieStoreId}`,
});
}
let identity = ContextualIdentityService.getPublicIdentityFromId(containerId);
@@ -160,44 +220,53 @@ this.contextualIdentities = class extend
let convertedIdentity = convertIdentity(identity);
if (!ContextualIdentityService.remove(identity.userContextId)) {
return Promise.reject({
message: `Contextual identitiy failed to remove: ${cookieStoreId}`,
});
}
- return Promise.resolve(convertedIdentity);
+ return convertedIdentity;
},
onCreated: new EventManager(context, "contextualIdentities.onCreated", fire => {
let observer = (subject, topic) => {
- fire.async({contextualIdentity: convertIdentityFromObserver(subject)});
+ let convertedIdentity = convertIdentityFromObserver(subject);
+ if (convertedIdentity) {
+ fire.async({contextualIdentity: convertedIdentity});
+ }
};
Services.obs.addObserver(observer, "contextual-identity-created");
return () => {
Services.obs.removeObserver(observer, "contextual-identity-created");
};
}).api(),
onUpdated: new EventManager(context, "contextualIdentities.onUpdated", fire => {
let observer = (subject, topic) => {
- fire.async({contextualIdentity: convertIdentityFromObserver(subject)});
+ let convertedIdentity = convertIdentityFromObserver(subject);
+ if (convertedIdentity) {
+ fire.async({contextualIdentity: convertedIdentity});
+ }
};
Services.obs.addObserver(observer, "contextual-identity-updated");
return () => {
Services.obs.removeObserver(observer, "contextual-identity-updated");
};
}).api(),
onRemoved: new EventManager(context, "contextualIdentities.onRemoved", fire => {
let observer = (subject, topic) => {
- fire.async({contextualIdentity: convertIdentityFromObserver(subject)});
+ let convertedIdentity = convertIdentityFromObserver(subject);
+ if (convertedIdentity) {
+ fire.async({contextualIdentity: convertedIdentity});
+ }
};
Services.obs.addObserver(observer, "contextual-identity-deleted");
return () => {
Services.obs.removeObserver(observer, "contextual-identity-deleted");
};
}).api(),
--- a/toolkit/components/extensions/schemas/contextual_identities.json
+++ b/toolkit/components/extensions/schemas/contextual_identities.json
@@ -23,18 +23,20 @@
"permissions": ["contextualIdentities"],
"types": [
{
"id": "ContextualIdentity",
"type": "object",
"description": "Represents information about a contextual identity.",
"properties": {
"name": {"type": "string", "description": "The name of the contextual identity."},
- "icon": {"type": "string", "description": "The icon of the contextual identity."},
- "color": {"type": "string", "description": "The color of the contextual identity."},
+ "icon": {"type": "string", "description": "The icon name of the contextual identity."},
+ "iconUrl": {"type": "string", "description": "The icon url of the contextual identity."},
+ "color": {"type": "string", "description": "The color name of the contextual identity."},
+ "colorCode": {"type": "string", "description": "The color hash of the contextual identity."},
"cookieStoreId": {"type": "string", "description": "The cookie store ID of the contextual identity."}
}
}
],
"functions": [
{
"name": "get",
"type": "function",
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
@@ -46,34 +46,40 @@ add_task(async function test_contextualI
browser.contextualIdentities[type].addListener(listener);
} catch (e) {
reject(e);
}
});
}
function assertExpected(expected, container) {
+ // Number of keys that are added by the APIs
+ const createdCount = 2;
for (let key of Object.keys(container)) {
browser.test.assertTrue(key in expected, `found property ${key}`);
browser.test.assertEq(expected[key], container[key], `property value for ${key} is correct`);
}
- browser.test.assertEq(Object.keys(expected).length, Object.keys(container).length, "all expected properties found");
+ const hexMatch = /^#[0-9a-f]{6}$/;
+ browser.test.assertTrue(hexMatch.test(expected.colorCode), "Color code property was expected Hex shape");
+ const iconMatch = /^resource:\/\/usercontext-content\/[a-z]+[.]svg$/;
+ browser.test.assertTrue(iconMatch.test(expected.iconUrl), "Icon url property was expected shape");
+ browser.test.assertEq(Object.keys(expected).length, Object.keys(container).length + createdCount, "all expected properties found");
}
let onCreatePromise = createOneTimeListener("onCreated");
- let containerObj = {name: "foobar", color: "red", icon: "icon"};
+ let containerObj = {name: "foobar", color: "red", icon: "circle"};
let ci = await browser.contextualIdentities.create(containerObj);
browser.test.assertTrue(!!ci, "We have an identity");
const onCreateListenerResponse = await onCreatePromise;
const cookieStoreId = ci.cookieStoreId;
assertExpected(onCreateListenerResponse.contextualIdentity, Object.assign(containerObj, {cookieStoreId}));
let onUpdatedPromise = createOneTimeListener("onUpdated");
- let updateContainerObj = {name: "testing", color: "blue", icon: "thing"};
+ let updateContainerObj = {name: "testing", color: "blue", icon: "dollar"};
ci = await browser.contextualIdentities.update(cookieStoreId, updateContainerObj);
browser.test.assertTrue(!!ci, "We have an update identity");
const onUpdatedListenerResponse = await onUpdatedPromise;
assertExpected(onUpdatedListenerResponse.contextualIdentity, Object.assign(updateContainerObj, {cookieStoreId}));
let onRemovePromise = createOneTimeListener("onRemoved");
ci = await browser.contextualIdentities.remove(updateContainerObj.cookieStoreId);
browser.test.assertTrue(!!ci, "We have an remove identity");
@@ -125,49 +131,64 @@ add_task(async function test_contextualI
browser.test.assertEq(4, cis.length, "by default we should have 4 containers");
cis = await browser.contextualIdentities.query({name: "Personal"});
browser.test.assertEq(1, cis.length, "by default we should have 1 container called Personal");
cis = await browser.contextualIdentities.query({name: "foobar"});
browser.test.assertEq(0, cis.length, "by default we should have 0 container called foobar");
- ci = await browser.contextualIdentities.create({name: "foobar", color: "red", icon: "icon"});
+ ci = await browser.contextualIdentities.create({name: "foobar", color: "red", icon: "gift"});
browser.test.assertTrue(!!ci, "We have an identity");
browser.test.assertEq("foobar", ci.name, "identity.name is correct");
browser.test.assertEq("red", ci.color, "identity.color is correct");
- browser.test.assertEq("icon", ci.icon, "identity.icon is correct");
+ browser.test.assertEq("gift", ci.icon, "identity.icon is correct");
browser.test.assertTrue(!!ci.cookieStoreId, "identity.cookieStoreId is correct");
+ browser.test.assertRejects(
+ browser.contextualIdentities.create({name: "foobar", color: "red", icon: "firefox"}),
+ "Invalid icon firefox for container",
+ "Create container called with an invalid icon"
+ );
+
+ browser.test.assertRejects(
+ browser.contextualIdentities.create({name: "foobar", color: "firefox-orange", icon: "gift"}),
+ "Invalid color name firefox-orange for container",
+ "Create container called with an invalid color"
+ );
+
+ cis = await browser.contextualIdentities.query({});
+ browser.test.assertEq(5, cis.length, "we should still have have 5 containers");
+
ci = await browser.contextualIdentities.get(ci.cookieStoreId);
browser.test.assertTrue(!!ci, "We have an identity");
browser.test.assertEq("foobar", ci.name, "identity.name is correct");
browser.test.assertEq("red", ci.color, "identity.color is correct");
- browser.test.assertEq("icon", ci.icon, "identity.icon is correct");
+ browser.test.assertEq("gift", ci.icon, "identity.icon is correct");
cis = await browser.contextualIdentities.query({});
browser.test.assertEq(5, cis.length, "now we have 5 identities");
- ci = await browser.contextualIdentities.update(ci.cookieStoreId, {name: "barfoo", color: "blue", icon: "icon icon"});
+ ci = await browser.contextualIdentities.update(ci.cookieStoreId, {name: "barfoo", color: "blue", icon: "cart"});
browser.test.assertTrue(!!ci, "We have an identity");
browser.test.assertEq("barfoo", ci.name, "identity.name is correct");
browser.test.assertEq("blue", ci.color, "identity.color is correct");
- browser.test.assertEq("icon icon", ci.icon, "identity.icon is correct");
+ browser.test.assertEq("cart", ci.icon, "identity.icon is correct");
ci = await browser.contextualIdentities.get(ci.cookieStoreId);
browser.test.assertTrue(!!ci, "We have an identity");
browser.test.assertEq("barfoo", ci.name, "identity.name is correct");
browser.test.assertEq("blue", ci.color, "identity.color is correct");
- browser.test.assertEq("icon icon", ci.icon, "identity.icon is correct");
+ browser.test.assertEq("cart", ci.icon, "identity.icon is correct");
ci = await browser.contextualIdentities.remove(ci.cookieStoreId);
browser.test.assertTrue(!!ci, "We have an identity");
browser.test.assertEq("barfoo", ci.name, "identity.name is correct");
browser.test.assertEq("blue", ci.color, "identity.color is correct");
- browser.test.assertEq("icon icon", ci.icon, "identity.icon is correct");
+ browser.test.assertEq("cart", ci.icon, "identity.icon is correct");
cis = await browser.contextualIdentities.query({});
browser.test.assertEq(4, cis.length, "we are back to 4 identities");
browser.test.notifyPass("contextualIdentities");
}
function makeExtension(id) {
return ExtensionTestUtils.loadExtension({