re-integrate ContextualIdentityService into SDK code (#27)

* bring ContextualIdentityService back into SDK code
* restore show/hide functionality in WebExtension
* move identitiesState to SDK index to preserve it

When a user closes the pop-up, preserve the hidden tabs for the
containers, so they can still quickly re-open the tabs.
This commit is contained in:
luke crouch 2017-01-03 09:06:10 -06:00 committed by Jonathan Kingston
parent d2978510ca
commit 50419d6bb1
8 changed files with 225 additions and 1698 deletions

179
index.js
View file

@ -1,9 +1,186 @@
/* global require */ /* global require */
const {ContextualIdentityService} = require('resource://gre/modules/ContextualIdentityService.jsm');
const tabs = require('sdk/tabs'); const tabs = require('sdk/tabs');
const webExtension = require('sdk/webextension'); const webExtension = require('sdk/webextension');
const CONTAINER_STORE = 'firefox-container-';
const identitiesState = {
};
function getCookieStoreIdForContainer(containerId) {
return CONTAINER_STORE + containerId;
}
function convert(identity) {
const cookieStoreId = getCookieStoreIdForContainer(identity.userContextId);
let hiddenTabUrls = [];
if (cookieStoreId in identitiesState) {
hiddenTabUrls = identitiesState[cookieStoreId].hiddenTabUrls;
}
const result = {
name: ContextualIdentityService.getUserContextLabel(identity.userContextId),
icon: identity.icon,
color: identity.color,
cookieStoreId: cookieStoreId,
hiddenTabUrls: hiddenTabUrls
};
return result;
}
function isContainerCookieStoreId(storeId) {
return storeId !== null && storeId.startsWith(CONTAINER_STORE);
}
function getContainerForCookieStoreId(storeId) {
if (!isContainerCookieStoreId(storeId)) {
return null;
}
const containerId = storeId.substring(CONTAINER_STORE.length);
if (ContextualIdentityService.getIdentityFromId(containerId)) {
return parseInt(containerId, 10);
}
return null;
}
function getContainer(cookieStoreId) {
const containerId = getContainerForCookieStoreId(cookieStoreId);
if (!containerId) {
return Promise.resolve(null);
}
const identity = ContextualIdentityService.getIdentityFromId(containerId);
return Promise.resolve(convert(identity));
}
function queryContainers(details) {
const identities = [];
ContextualIdentityService.getIdentities().forEach(identity=> {
if (details && details.name &&
ContextualIdentityService.getUserContextLabel(identity.userContextId) !== details.name) {
return;
}
const convertedIdentity = convert(identity);
identities.push(convertedIdentity);
if (!(convertedIdentity.cookieStoreId in identitiesState)) {
identitiesState[convertedIdentity.cookieStoreId] = {hiddenTabUrls: []};
}
});
return Promise.resolve(identities);
}
function createContainer(details) {
const identity = ContextualIdentityService.create(details.name,
details.icon,
details.color);
return Promise.resolve(convert(identity));
}
function updateContainer(cookieStoreId, details) {
const containerId = getContainerForCookieStoreId(cookieStoreId);
if (!containerId) {
return Promise.resolve(null);
}
const identity = ContextualIdentityService.getIdentityFromId(containerId);
if (!identity) {
return Promise.resolve(null);
}
if (details.name !== null) {
identity.name = details.name;
}
if (details.color !== null) {
identity.color = details.color;
}
if (details.icon !== null) {
identity.icon = details.icon;
}
if (!ContextualIdentityService.update(identity.userContextId,
identity.name, identity.icon,
identity.color)) {
return Promise.resolve(null);
}
return Promise.resolve(convert(identity));
}
function removeContainer(cookieStoreId) {
const containerId = getContainerForCookieStoreId(cookieStoreId);
if (!containerId) {
return Promise.resolve(null);
}
const identity = ContextualIdentityService.getIdentityFromId(containerId);
if (!identity) {
return Promise.resolve(null);
}
// We have to create the identity object before removing it.
const convertedIdentity = convert(identity);
if (!ContextualIdentityService.remove(identity.userContextId)) {
return Promise.resolve(null);
}
return Promise.resolve(convertedIdentity);
}
const contextualIdentities = {
get: getContainer,
query: queryContainers,
create: createContainer,
update: updateContainer,
remove: removeContainer
};
function handleWebExtensionMessage(message, sender, sendReply) { function handleWebExtensionMessage(message, sender, sendReply) {
switch (message) { switch (message.method) {
case 'query':
sendReply(contextualIdentities.query(message.arguments));
break;
case 'hide':
identitiesState[message.cookieStoreId].hiddenTabUrls = message.tabUrlsToSave;
break;
case 'show':
sendReply(identitiesState[message.cookieStoreId].hiddenTabUrls);
identitiesState[message.cookieStoreId].hiddenTabUrls = [];
break;
case 'get':
sendReply(contextualIdentities.get(message.arguments));
break;
case 'create':
sendReply(contextualIdentities.create(message.arguments));
break;
case 'update':
sendReply(contextualIdentities.update(message.arguments));
break;
case 'remove':
sendReply(contextualIdentities.remove(message.arguments));
break;
case 'getIdentitiesState':
sendReply(identitiesState);
break;
case 'open-containers-preferences': case 'open-containers-preferences':
tabs.open('about:preferences#containers'); tabs.open('about:preferences#containers');
sendReply({content: 'opened'}); sendReply({content: 'opened'});

View file

@ -1,27 +0,0 @@
Copyright (c) 2010 - 2016, Mozilla Corporation
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the Mozilla Corporation nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -1,7 +0,0 @@
## Webextension contextual identities API extension
This project contains the implementation of the Firefox
browser.contextualIdentites API.
Based on: https://bugzilla.mozilla.org/show_bug.cgi?id=1322856

File diff suppressed because it is too large Load diff

View file

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>contextualidentities@experiments.addons.mozilla.org</em:id>
<em:name>Experimental Contextual Identites API</em:name>
<em:type>256</em:type>
<em:version>0.1</em:version>
<em:description>Experimental Contextual Identities API</em:description>
<em:creator>groovecoder, baku and jkt</em:creator>
<em:multiprocessCompatible>true</em:multiprocessCompatible>
<!-- Firefox -->
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>51.0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<!-- for testing -->
<em:targetApplication>
<Description>
<em:id>xpcshell@tests.mozilla.org</em:id>
<em:minVersion>1</em:minVersion>
<em:maxVersion>2</em:maxVersion>
</Description>
</em:targetApplication>
</Description>
</RDF>

View file

@ -1,131 +0,0 @@
[
{
"namespace": "contextualIdentities",
"description": "Use the <code>browser.contextualIdentities</code> API to query and modify contextual identity, also called as containers.",
"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."},
"cookieStoreId": {"type": "string", "description": "The cookie store ID of the contextual identity."}
}
}
],
"functions": [
{
"name": "get",
"type": "function",
"description": "Retrieves information about a single contextual identity.",
"async": true,
"parameters": [
{
"type": "string",
"name": "cookiestoreid",
"description": "the id of the contextual identity cookie store. "
}
]
},
{
"name": "hide",
"type": "function",
"description": "hides all of a contextual identity.",
"async": true,
"parameters": [
{
"type": "string",
"name": "cookiestoreid",
"description": "the id of the contextual identity cookie store. "
}
]
},
{
"name": "show",
"type": "function",
"description": "unhides all of a contextual identity.",
"async": true,
"parameters": [
{
"type": "string",
"name": "cookiestoreid",
"description": "the id of the contextual identity cookie store. "
}
]
},
{
"name": "query",
"type": "function",
"description": "Retrieves all contextual identities",
"async": true,
"parameters": [
{
"type": "object",
"name": "details",
"description": "Information to filter the contextual identities being retrieved.",
"properties": {
"name": {"type": "string", "optional": true, "description": "Filters the contextual identity by name."}
}
}
]
},
{
"name": "create",
"type": "function",
"description": "Creates a contextual identity with the given data.",
"async": true,
"parameters": [
{
"type": "object",
"name": "details",
"description": "Details about the contextual identity being created.",
"properties": {
"name": {"type": "string", "optional": false, "description": "The name of the contextual identity." },
"color": {"type": "string", "optional": false, "description": "The color of the contextual identity." },
"icon": {"type": "string", "optional": false, "description": "The icon of the contextual identity." }
}
}
]
},
{
"name": "update",
"type": "function",
"description": "Updates a contextual identity with the given data.",
"async": true,
"parameters": [
{
"type": "string",
"name": "cookieStoreId",
"description": "The ID of the contextual identity cookie store. "
},
{
"type": "object",
"name": "details",
"description": "Details about the contextual identity being created.",
"properties": {
"name": {"type": "string", "optional": true, "description": "The name of the contextual identity." },
"color": {"type": "string", "optional": true, "description": "The color of the contextual identity." },
"icon": {"type": "string", "optional": true, "description": "The icon of the contextual identity." }
}
}
]
},
{
"name": "remove",
"type": "function",
"description": "Deletes a contetual identity by its cookie Store ID.",
"async": true,
"parameters": [
{
"type": "string",
"name": "cookieStoreId",
"description": "The ID of the contextual identity cookie store. "
}
]
}
]
}
]

View file

@ -1,25 +1,54 @@
/* global browser, window, document */ /* global browser, window, document */
const identityState = { const CONTAINER_HIDE_SRC = '/img/container-hide.svg';
}; const CONTAINER_UNHIDE_SRC = '/img/container-unhide.svg';
function hideContainer(containerId) { function hideContainerTabs(containerId) {
const tabIdsToRemove = [];
const tabUrlsToSave = [];
const hideorshowIcon = document.querySelector(`#${containerId}-hideorshow-icon`); const hideorshowIcon = document.querySelector(`#${containerId}-hideorshow-icon`);
hideorshowIcon.src = '/img/container-unhide.svg'; browser.tabs.query({cookieStoreId: containerId}).then(tabs=> {
browser.contextualIdentities.hide(containerId); tabs.forEach(tab=> {
tabIdsToRemove.push(tab.id);
tabUrlsToSave.push(tab.url);
});
browser.runtime.sendMessage({
method: 'hide',
cookieStoreId: containerId,
tabUrlsToSave: tabUrlsToSave
}).then(()=> {
browser.tabs.remove(tabIdsToRemove);
hideorshowIcon.src = CONTAINER_UNHIDE_SRC;
});
});
} }
function showContainer(containerId) { function showContainerTabs(containerId) {
const hideorshowIcon = document.querySelector(`#${containerId}-hideorshow-icon`); const hideorshowIcon = document.querySelector(`#${containerId}-hideorshow-icon`);
hideorshowIcon.src = '/img/container-hide.svg'; browser.runtime.sendMessage({
browser.contextualIdentities.show(containerId); method: 'show',
cookieStoreId: containerId
}).then(hiddenTabUrls=> {
hiddenTabUrls.forEach(url=> {
browser.tabs.create({
url: url,
cookieStoreId: containerId
});
});
});
hideorshowIcon.src = CONTAINER_HIDE_SRC;
} }
browser.contextualIdentities.query({}).then(identities=> { browser.runtime.sendMessage({method: 'query'}).then(identities=> {
const identitiesListElement = document.querySelector('.identities-list'); const identitiesListElement = document.querySelector('.identities-list');
identities.forEach(identity=> { identities.forEach(identity=> {
let hideOrShowIconSrc = CONTAINER_HIDE_SRC;
if (identity.hiddenTabUrls.length) {
hideOrShowIconSrc = CONTAINER_UNHIDE_SRC;
}
const identityRow = ` const identityRow = `
<tr data-identity-cookie-store-id="${identity.cookieStoreId}" > <tr data-identity-cookie-store-id="${identity.cookieStoreId}" >
<td><div class="userContext-icon" <td><div class="userContext-icon"
@ -32,14 +61,13 @@ browser.contextualIdentities.query({}).then(identities=> {
data-identity-cookie-store-id="${identity.cookieStoreId}" data-identity-cookie-store-id="${identity.cookieStoreId}"
id="${identity.cookieStoreId}-hideorshow-icon" id="${identity.cookieStoreId}-hideorshow-icon"
class="hideorshow-icon" class="hideorshow-icon"
src="/img/container-hide.svg" src="${hideOrShowIconSrc}"
/> />
</td> </td>
<td>&gt;</td> <td>&gt;</td>
</tr>`; </tr>`;
identitiesListElement.innerHTML += identityRow; identitiesListElement.innerHTML += identityRow;
}); });
const rows = identitiesListElement.querySelectorAll('tr'); const rows = identitiesListElement.querySelectorAll('tr');
@ -49,16 +77,13 @@ browser.contextualIdentities.query({}).then(identities=> {
if (e.target.matches('.hideorshow-icon')) { if (e.target.matches('.hideorshow-icon')) {
const containerId = e.target.dataset.identityCookieStoreId; const containerId = e.target.dataset.identityCookieStoreId;
if (!(containerId in identityState)) { browser.runtime.sendMessage({method: 'getIdentitiesState'}).then(identitiesState=> {
identityState[containerId] = true; if (identitiesState[containerId].hiddenTabUrls.length) {
} showContainerTabs(containerId);
if (identityState[containerId]) { } else {
hideContainer(containerId); hideContainerTabs(containerId);
identityState[containerId] = false; }
} else { });
showContainer(containerId);
identityState[containerId] = true;
}
} }
}); });
}); });
@ -81,7 +106,7 @@ function moveTabs(sortedTabsArray) {
} }
document.querySelector('#sort-containers-link').addEventListener('click', ()=> { document.querySelector('#sort-containers-link').addEventListener('click', ()=> {
browser.contextualIdentities.query({}).then(identities=> { browser.runtime.sendMessage({method: 'query'}).then(identities=> {
identities.unshift({cookieStoreId: 'firefox-default'}); identities.unshift({cookieStoreId: 'firefox-default'});
browser.tabs.query({}).then(tabsArray=> { browser.tabs.query({}).then(tabsArray=> {

View file

@ -21,8 +21,6 @@
"permissions": [ "permissions": [
"cookies", "cookies",
"experiments.contextualidentities",
"contextualidentities",
"tabs", "tabs",
"cookies" "cookies"
], ],