diff --git a/.eslintrc.js b/.eslintrc.js index 3876d96..748b27b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,10 +11,12 @@ module.exports = { "globals": { "CustomizableUI": true, "CustomizableWidgets": true, - "SessionStore": true + "SessionStore": true, + "Services": true }, "plugins": [ - "promise" + "promise", + "unsafe-property-assignment" ], "root": true, "rules": { @@ -27,6 +29,8 @@ module.exports = { "promise/no-promise-in-callback": "warn", "promise/no-return-wrap": "error", "promise/param-names": "error", + "unsafe-property-assignment/no-key-assignment": ["error"], + "unsafe-property-assignment/enforce-tagged-template-protection": ["error"], "eqeqeq": "error", "indent": ["error", 2], diff --git a/.travis.yml b/.travis.yml index c2a2c34..4614306 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,7 @@ language: node_js node_js: - "6.1" + +notifications: + irc: + - "ircs://irc.mozilla.org:6697/#testpilot-containers-bots" diff --git a/README.md b/README.md index 7c8b6a8..3fead42 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Containers: Test Pilot Experiment -Soon to be [![Available on Test Pilot](https://img.shields.io/badge/available_on-Test_Pilot-0996F8.svg)](https://testpilot.firefox.com/) +[![Available on Test Pilot](https://img.shields.io/badge/available_on-Test_Pilot-0996F8.svg)](https://testpilot.firefox.com/experiments/containers) [Embedded Web Extension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Embedded_WebExtensions) to experiment with [Containers](https://blog.mozilla.org/tanvi/2016/06/16/contextual-identities-on-the-web/) in [Firefox Test Pilot](https://testpilot.firefox.com/) to learn: diff --git a/index.js b/index.js index 1fa0e91..2b9a1fa 100644 --- a/index.js +++ b/index.js @@ -30,6 +30,7 @@ const IDENTITY_ICONS = [ { name: "briefcase", image: "chrome://browser/skin/usercontext/work.svg" }, { name: "dollar", image: "chrome://browser/skin/usercontext/banking.svg" }, { name: "cart", image: "chrome://browser/skin/usercontext/shopping.svg" }, + // All of these do not exist in gecko { name: "gift", image: "gift" }, { name: "vacation", image: "vacation" }, { name: "food", image: "food" }, @@ -37,7 +38,7 @@ const IDENTITY_ICONS = [ { name: "pet", image: "pet" }, { name: "tree", image: "tree" }, { name: "chill", image: "chill" }, - { name: "circle", image: "circle" }, // this doesn't exist in m-b + { name: "circle", image: "circle" }, ]; const PREFS = [ @@ -51,10 +52,12 @@ const { attachTo, detachFrom } = require("sdk/content/mod"); const { Cu } = require("chrome"); const { ContextualIdentityService } = require("resource://gre/modules/ContextualIdentityService.jsm"); const { getFavicon } = require("sdk/places/favicon"); +const { LightweightThemeManager } = Cu.import("resource://gre/modules/LightweightThemeManager.jsm", {}); const Metrics = require("./testpilot-metrics"); const { modelFor } = require("sdk/model/core"); const prefService = require("sdk/preferences/service"); const self = require("sdk/self"); +const { Services } = require("resource://gre/modules/Services.jsm"); const ss = require("sdk/simple-storage"); const { Style } = require("sdk/stylesheet/style"); const tabs = require("sdk/tabs"); @@ -68,6 +71,7 @@ const windowUtils = require("sdk/window/utils"); Cu.import("resource:///modules/CustomizableUI.jsm"); Cu.import("resource:///modules/CustomizableWidgets.jsm"); Cu.import("resource:///modules/sessionstore/SessionStore.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); // ---------------------------------------------------------------------------- // ContextualIdentityProxy @@ -113,6 +117,7 @@ const ContainerService = { _identitiesState: {}, _windowMap: new Map(), _containerWasEnabled: false, + _onThemeChangedCallback: null, init(installation) { // If we are just been installed, we must store some information for the @@ -194,6 +199,7 @@ const ContainerService = { "updateIdentity", "getPreference", "sendTelemetryPayload", + "getTheme", "checkIncompatibleAddons" ]; @@ -252,6 +258,8 @@ const ContainerService = { sendReply(this[message.method](message)); } }); + + this.registerThemeConnection(api); }).catch(() => { throw new Error("WebExtension startup failed. Unable to continue."); }); @@ -290,6 +298,51 @@ const ContainerService = { }; } // End-Of-Hack + + Services.obs.addObserver(this, "lightweight-theme-changed", false); + }, + + registerThemeConnection(api) { + // This is only used for theme notifications + api.browser.runtime.onConnect.addListener((port) => { + this.onThemeChanged((theme, topic) => { + port.postMessage({ + type: topic, + theme + }); + }); + }); + }, + + triggerThemeChanged(theme, topic) { + if (this._onThemeChangedCallback) { + this._onThemeChangedCallback(theme, topic); + } + }, + + observe(subject, topic) { + if (topic === "lightweight-theme-changed") { + this.getTheme().then((theme) => { + this.triggerThemeChanged(theme, topic); + }).catch(() => { + throw new Error("Unable to get theme"); + }); + } + }, + + getTheme() { + const defaultTheme = "firefox-compact-light@mozilla.org"; + return new Promise(function (resolve) { + let theme = defaultTheme; + if (LightweightThemeManager.currentTheme && LightweightThemeManager.currentTheme.id) { + theme = LightweightThemeManager.currentTheme.id; + } + resolve(theme); + }); + }, + + onThemeChanged(callback) { + this._onThemeChangedCallback = callback; }, // utility methods @@ -1076,6 +1129,10 @@ const ContainerService = { ContextualIdentityProxy.getIdentities().forEach(identity => { if (!preInstalledIdentities.includes(identity.userContextId)) { ContextualIdentityProxy.remove(identity.userContextId); + } else { + // Let's cleanup all the cookies for this container. + Services.obs.notifyObservers(null, "clear-origin-attributes-data", + JSON.stringify({ userContextId: identity.userContextId })); } }); diff --git a/package.json b/package.json index 4a0172a..75f6de1 100644 --- a/package.json +++ b/package.json @@ -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": "1.0.4", + "version": "1.1.1", "author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston", "bugs": { "url": "https://github.com/mozilla/testpilot-containers/issues" @@ -11,8 +11,9 @@ "devDependencies": { "addons-linter": "^0.15.14", "deploy-txp": "^1.0.7", - "eslint": "^3.12.2", + "eslint": "^3.17.1", "eslint-plugin-promise": "^3.4.0", + "eslint-plugin-unsafe-property-assign": "^1.0.2", "htmllint-cli": "^0.0.5", "jpm": "^1.2.2", "npm-run-all": "^4.0.0", diff --git a/webextension/background.js b/webextension/background.js index 3875580..e909c6b 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -1,3 +1,50 @@ + +const themeManager = { + existingTheme: null, + init() { + this.check(); + + const port = browser.runtime.connect(); + port.onMessage.addListener(m => { + if (m.type === "lightweight-theme-changed") { + this.update(m.theme); + } + }); + }, + setPopupIcon(theme) { + let icons = { + 16: "img/container-site-d-24.png", + 32: "img/container-site-d-48.png" + }; + if (theme === "firefox-compact-dark@mozilla.org") { + icons = { + 16: "img/container-site-w-24.png", + 32: "img/container-site-w-48.png" + }; + } + browser.browserAction.setIcon({ + path: icons + }); + }, + check() { + browser.runtime.sendMessage({ + method: "getTheme" + }).then((theme) => { + this.update(theme); + }).catch(() => { + throw new Error("Unable to get theme"); + }); + }, + update(theme) { + if (this.existingTheme !== theme) { + this.setPopupIcon(theme); + this.existingTheme = theme; + } + } +}; + +themeManager.init(); + browser.runtime.sendMessage({ method: "getPreference", pref: "browser.privatebrowsing.autostart" diff --git a/webextension/js/popup.js b/webextension/js/popup.js index c75985c..c108558 100644 --- a/webextension/js/popup.js +++ b/webextension/js/popup.js @@ -18,6 +18,42 @@ const P_CONTAINER_INFO = "containerInfo"; const P_CONTAINER_EDIT = "containerEdit"; const P_CONTAINER_DELETE = "containerDelete"; +/** + * Escapes any occurances of &, ", < or > with XML entities. + * + * @param {string} str + * The string to escape. + * @return {string} The escaped string. + */ +function escapeXML(str) { + const replacements = {"&": "&", "\"": """, "'": "'", "<": "<", ">": ">"}; + return String(str).replace(/[&"''<>]/g, m => replacements[m]); +} + +/** + * A tagged template function which escapes any XML metacharacters in + * interpolated values. + * + * @param {Array} strings + * An array of literal strings extracted from the templates. + * @param {Array} values + * An array of interpolated values extracted from the template. + * @returns {string} + * The result of the escaped values interpolated with the literal + * strings. + */ +function escaped(strings, ...values) { + const result = []; + + for (const [i, string] of strings.entries()) { + result.push(string); + if (i < values.length) + result.push(escapeXML(values[i])); + } + + return result.join(""); +} + // This object controls all the panels, identities and many other things. const Logic = { _identities: [], @@ -32,7 +68,8 @@ const Logic = { // Routing to the correct panel. .then(() => { - if (localStorage.getItem("onboarded3")) { + // If localStorage is disabled, we don't show the onboarding. + if (!localStorage || localStorage.getItem("onboarded3")) { this.showPanel(P_CONTAINERS_LIST); } else if (localStorage.getItem("onboarded2")) { this.showPanel(P_ONBOARDING_3); @@ -234,7 +271,7 @@ Logic.registerPanel(P_CONTAINERS_LIST, { tr.classList.add("container-panel-row"); context.classList.add("userContext-wrapper", "open-newtab", "clickable"); manage.classList.add("show-tabs", "pop-button"); - context.innerHTML = ` + context.innerHTML = escaped`
${tab.title}`; @@ -421,7 +458,7 @@ Logic.registerPanel(P_CONTAINERS_EDIT, { const tr = document.createElement("tr"); fragment.appendChild(tr); tr.classList.add("container-panel-row"); - tr.innerHTML = ` + tr.innerHTML = escaped`
{ - const identity = Logic.currentIdentity(); - const formValues = new FormData(document.getElementById("edit-container-panel-form")); - browser.runtime.sendMessage({ - method: identity.userContextId ? "updateIdentity" : "createIdentity", - userContextId: identity.userContextId || 0, - name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(), - icon: formValues.get("container-icon") || DEFAULT_ICON, - color: formValues.get("container-color") || DEFAULT_COLOR, - }).then(() => { - return Logic.refreshIdentities(); - }).then(() => { - Logic.showPreviousPanel(); - }).catch(() => { - Logic.showPanel(P_CONTAINERS_LIST); - }); + this._editForm = document.getElementById("edit-container-panel-form"); + const editLink = document.querySelector("#edit-container-ok-link"); + editLink.addEventListener("click", this._submitForm.bind(this)); + editLink.addEventListener("submit", this._submitForm.bind(this)); + this._editForm.addEventListener("submit", this._submitForm.bind(this)); + }, + + _submitForm() { + const identity = Logic.currentIdentity(); + const formValues = new FormData(this._editForm); + browser.runtime.sendMessage({ + method: identity.userContextId ? "updateIdentity" : "createIdentity", + userContextId: identity.userContextId || 0, + name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(), + icon: formValues.get("container-icon") || DEFAULT_ICON, + color: formValues.get("container-color") || DEFAULT_COLOR, + }).then(() => { + return Logic.refreshIdentities(); + }).then(() => { + Logic.showPreviousPanel(); + }).catch(() => { + Logic.showPanel(P_CONTAINERS_LIST); }); }, initializeRadioButtons() { const colorRadioTemplate = (containerColor) => { - return ` + return escaped`