Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
"use strict";
ChromeUtils.defineESModuleGetters(this, {
ContextualIdentityService:
"resource://gre/modules/ContextualIdentityService.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"containersEnabled",
"privacy.userContext.enabled"
);
var { ExtensionPreferencesManager } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPreferencesManager.sys.mjs"
);
var { ExtensionError } = ExtensionUtils;
const CONTAINER_PREF_INSTALL_DEFAULTS = {
"privacy.userContext.extension": undefined,
};
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"],
["toolbar", "#7c7c7d"],
]);
const CONTAINER_ICONS = new Set([
"briefcase",
"cart",
"circle",
"dollar",
"fence",
"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 checkAPIEnabled = () => {
if (!containersEnabled) {
throw new ExtensionError("Contextual identities are currently disabled");
}
};
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),
setCallback(value) {
if (value !== true) {
return {
...CONTAINER_PREF_INSTALL_DEFAULTS,
"privacy.userContext.extension": value,
};
}
return {};
},
});
this.contextualIdentities = class extends ExtensionAPIPersistent {
eventRegistrar(eventName) {
return ({ fire }) => {
let observer = subject => {
let convertedIdentity = convertIdentityFromObserver(subject);
if (convertedIdentity) {
fire.async({ contextualIdentity: convertedIdentity });
}
};
Services.obs.addObserver(observer, eventName);
return {
unregister() {
Services.obs.removeObserver(observer, eventName);
},
convert(_fire) {
fire = _fire;
},
};
};
}
PERSISTENT_EVENTS = {
onCreated: this.eventRegistrar("contextual-identity-created"),
onUpdated: this.eventRegistrar("contextual-identity-updated"),
onRemoved: this.eventRegistrar("contextual-identity-deleted"),
};
onStartup() {
let { extension } = this;
if (extension.hasPermission("contextualIdentities")) {
// Turn on contextual identities, and never turn it off. We handle
// this here to ensure prefs are set when an addon is enabled.
Services.prefs.setBoolPref("privacy.userContext.enabled", true);
Services.prefs.setBoolPref("privacy.userContext.ui.enabled", true);
ExtensionPreferencesManager.setSetting(
extension.id,
CONTAINERS_ENABLED_SETTING_NAME,
extension.id
);
}
}
getAPI(context) {
let self = {
contextualIdentities: {
async get(cookieStoreId) {
checkAPIEnabled();
let containerId = getContainerForCookieStoreId(cookieStoreId);
if (!containerId) {
throw new ExtensionError(
`Invalid contextual identity: ${cookieStoreId}`
);
}
let identity =
ContextualIdentityService.getPublicIdentityFromId(containerId);
return convertIdentity(identity);
},
async query(details) {
checkAPIEnabled();
let identities = [];
ContextualIdentityService.getPublicIdentities().forEach(identity => {
if (
details.name &&
ContextualIdentityService.getUserContextLabel(
identity.userContextId
) != details.name
) {
return;
}
identities.push(convertIdentity(identity));
});
return identities;
},
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 convertIdentity(identity);
},
async update(cookieStoreId, details) {
checkAPIEnabled();
let containerId = getContainerForCookieStoreId(cookieStoreId);
if (!containerId) {
throw new ExtensionError(
`Invalid contextual identity: ${cookieStoreId}`
);
}
let identity =
ContextualIdentityService.getPublicIdentityFromId(containerId);
if (!identity) {
throw new ExtensionError(
`Invalid contextual identity: ${cookieStoreId}`
);
}
if (details.name !== null) {
identity.name = details.name;
}
if (details.color !== null) {
getContainerColor(details.color);
identity.color = details.color;
}
if (details.icon !== null) {
getContainerIcon(details.icon);
identity.icon = details.icon;
}
if (
!ContextualIdentityService.update(
identity.userContextId,
identity.name,
identity.icon,
identity.color
)
) {
throw new ExtensionError(
`Contextual identity failed to update: ${cookieStoreId}`
);
}
return convertIdentity(identity);
},
async move(cookieStoreIds, position) {
checkAPIEnabled();
if (!Array.isArray(cookieStoreIds)) {
cookieStoreIds = [cookieStoreIds];
}
if (!cookieStoreIds.length) {
return;
}
const totalIds =
ContextualIdentityService.getPublicIdentities().length;
if (position < -1 || position > totalIds - cookieStoreIds.length) {
throw new ExtensionError(`Moving to invalid position ${position}`);
}
let userContextIds = [];
cookieStoreIds.forEach((cookieStoreId, index) => {
if (cookieStoreIds.indexOf(cookieStoreId) !== index) {
throw new ExtensionError(
`Duplicate contextual identity: ${cookieStoreId}`
);
}
let containerId = getContainerForCookieStoreId(cookieStoreId);
if (!containerId) {
throw new ExtensionError(
`Invalid contextual identity: ${cookieStoreId}`
);
}
userContextIds.push(containerId);
});
if (!ContextualIdentityService.move(userContextIds, position)) {
throw new ExtensionError(
`Contextual identities failed to move: ${cookieStoreIds}`
);
}
},
async remove(cookieStoreId) {
checkAPIEnabled();
let containerId = getContainerForCookieStoreId(cookieStoreId);
if (!containerId) {
throw new ExtensionError(
`Invalid contextual identity: ${cookieStoreId}`
);
}
let identity =
ContextualIdentityService.getPublicIdentityFromId(containerId);
if (!identity) {
throw new ExtensionError(
`Invalid contextual identity: ${cookieStoreId}`
);
}
// We have to create the identity object before removing it.
let convertedIdentity = convertIdentity(identity);
if (!ContextualIdentityService.remove(identity.userContextId)) {
throw new ExtensionError(
`Contextual identity failed to remove: ${cookieStoreId}`
);
}
return convertedIdentity;
},
onCreated: new EventManager({
context,
module: "contextualIdentities",
event: "onCreated",
extensionApi: this,
}).api(),
onUpdated: new EventManager({
context,
module: "contextualIdentities",
event: "onUpdated",
extensionApi: this,
}).api(),
onRemoved: new EventManager({
context,
module: "contextualIdentities",
event: "onRemoved",
extensionApi: this,
}).api(),
},
};
return self;
}
};