Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2995b6c66 | |||
| ed383c8dfc | |||
| df9b900db6 | |||
| 8e611de605 | |||
| 0a437ff303 | |||
| f7f4c320a6 | |||
| 5813621fb9 | |||
| 56fc7407da | |||
| 220b902144 | |||
| dcc3b76cda | |||
| 752d18ffca | |||
| 97559dd08a | |||
| 0e7363a87f | |||
| 6c62c2f599 | |||
| 884e419a7c | |||
| d7586dd4c2 | |||
| aada0419eb | |||
| 3d1dcd33d1 | |||
| fe0810b048 | |||
| e1c1ac4bd9 | |||
| 7f7f221a79 | |||
| e57c556427 | |||
| dd57158ab5 | |||
| 99db192792 | |||
| fcbee854d0 | |||
| fae1336467 | |||
| 655d8f3791 | |||
| dcc852bf17 | |||
| dab3005c6f | |||
| ee6a54ffa2 | |||
| 601056406a | |||
| fd72ce12b4 | |||
| 6e45532f58 | |||
| 61da6b5e99 | |||
| e0156388e8 | |||
| 16f1d47bf2 | |||
| ee647344a1 | |||
| 40426ca936 | |||
| d1e9c2d1e3 | |||
| 3bd33cda99 | |||
| 609f62ac7a | |||
| ce84665e3a | |||
| 7dceaf6679 | |||
| b6bcd99dc8 | |||
| 9bc9509316 | |||
| 22ec01d565 | |||
| a16cae0342 | |||
| f17ff7168f | |||
| d3b22faf65 | |||
| 30e5a27eb4 | |||
| 0f720ec11d | |||
| 0ddee7f9d0 | |||
| 1e16e203dc | |||
| af986e8880 | |||
| 7e04c46070 | |||
| 166420dd86 | |||
| d7a2b43b07 | |||
| bea201a389 | |||
| d944116e3e | |||
| 4a1597c87f | |||
| f87bf2a861 | |||
| ef45cde290 | |||
| 752b1c3b27 | |||
| cf26d8547a | |||
| 1d78febafc | |||
| f483119a40 | |||
| abd2b73fca | |||
| 31ac365e6d | |||
| df8471a4dd | |||
| 18539f2540 | |||
| 7c1105a2b7 | |||
| 31298146f3 | |||
| 4e6eee220c | |||
| a7be3c9935 | |||
| f512473986 | |||
| 8166a37722 | |||
| adadb98482 | |||
| 25e760cd64 | |||
| 0ff8e17005 | |||
| c433c6b39e | |||
| 1c09c29104 | |||
| c1e9cc3c56 | |||
| 27296d24c5 | |||
| 030e635417 | |||
| 07711aaecc | |||
| 16ed8992e2 | |||
| 88e6dc7a05 | |||
| 28e8d46743 | |||
| 77ba1b723f | |||
| b0cc6e7c2f | |||
| e84e482130 | |||
| b5ae20b874 | |||
| 3ec81e3d1f | |||
| fb5436c287 | |||
| 01a628822b | |||
| 66e2c8e297 | |||
| 80661d68f2 | |||
| ef8aa3be75 | |||
| 6bc056e019 | |||
| 75deab139b | |||
| ae79f0a303 | |||
| 9b83068234 | |||
| fec2be9429 | |||
| 9f1b06ddd3 | |||
| ad2198e8b5 | |||
| 1791fdf0ef | |||
| 4ab705081e | |||
| 15b9dce1a9 | |||
| da3fc2ede2 | |||
| 385c585888 | |||
| 734b97beb0 | |||
| 3aa311a3c1 | |||
| df7d7f9c38 | |||
| 65be77665a | |||
| 44548659db | |||
| 10c4395efd | |||
| 27b2a4b5f2 | |||
| 6f54e7ff7f | |||
| cb6726b667 | |||
| 0964311fa1 | |||
| 2831d019f5 | |||
| 1cc58cad9b | |||
| bc9660f76e | |||
| f526caca50 | |||
| b6a98fb83e | |||
| af2b4b79a9 | |||
| a762b5eca2 | |||
| 3cc40344af | |||
| 278cdb7f69 | |||
| 0be03ebeb7 | |||
| c69f37a2de | |||
| b20ac8169a | |||
| 9e98d35b45 | |||
| 17f2781e07 | |||
| 0acf9cc0e6 | |||
| 70573d0559 | |||
| da239237f7 |
@@ -0,0 +1,28 @@
|
|||||||
|
<!--
|
||||||
|
Feel free to ignore this Issue template if you just want to ask or suggest something. If you experience an Issue then please provide all asked informations.
|
||||||
|
|
||||||
|
Note: If "Firefox will: Never remember history" in the Firefox Preferences/Options under "Privacy & Security > History" is selected, then Multi-Account Containers will not work, since Containers aren't available in Private Windows.
|
||||||
|
-->
|
||||||
|
- Is "Firefox will: Never remember history" in the Firefox Preferences/Options under "Privacy & Security > History" selected? Yes/No:
|
||||||
|
- Are you using Firefox in a Private Window? Yes/No:
|
||||||
|
- Can you see a grayed out but ticked Checkbox with the description "Enable Container Tabs" in the Firefox Preferences/Options under "Tabs"? Yes/No:
|
||||||
|
- Multi-Account Containers Version:
|
||||||
|
- Operating System + Version:
|
||||||
|
- Firefox Version:
|
||||||
|
- Other installed Add-ons + Version + Enabled/Disabled-Status:
|
||||||
|
<!-- To be able to Copy&Paste the full list of your Add-ons navigate to "about:support" and scroll down to "Extensions" -->
|
||||||
|
|
||||||
|
|
||||||
|
### Actual behavior
|
||||||
|
..
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
..
|
||||||
|
|
||||||
|
### Steps to reproduce
|
||||||
|
1. ..
|
||||||
|
2. ..
|
||||||
|
3. ..
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
..
|
||||||
@@ -3,8 +3,12 @@ package-lock.json
|
|||||||
node_modules
|
node_modules
|
||||||
README.html
|
README.html
|
||||||
*.xpi
|
*.xpi
|
||||||
*.swp
|
*.sw*
|
||||||
*.swo
|
|
||||||
.vimrc
|
.vimrc
|
||||||
.env
|
.env
|
||||||
addon.env
|
addon.env
|
||||||
|
|
||||||
|
src/web-ext-artifacts/*
|
||||||
|
|
||||||
|
# JetBrains IDE files
|
||||||
|
.idea
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ docs/
|
|||||||
test/
|
test/
|
||||||
.npm/
|
.npm/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
bin/
|
||||||
|
|
||||||
.env
|
.env
|
||||||
.eslintrc.js
|
.eslintrc.js
|
||||||
@@ -14,6 +15,8 @@ node_modules/
|
|||||||
.stylelintrc
|
.stylelintrc
|
||||||
.travis.yml
|
.travis.yml
|
||||||
*.xpi
|
*.xpi
|
||||||
|
*.md
|
||||||
.vimrc
|
.vimrc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.gdb_history
|
.gdb_history
|
||||||
|
*.sw*
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
"extends": "stylelint-config-standard",
|
"extends": "stylelint-config-standard",
|
||||||
|
|
||||||
"ignoreFiles": ["webextension/css/*.min.css"],
|
"ignoreFiles": ["src/css/*.min.css"],
|
||||||
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"declaration-block-no-duplicate-properties": true,
|
"declaration-block-no-duplicate-properties": true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- "6.1"
|
- "lts/*"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
irc:
|
irc:
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ Everyone is welcome to contribute to containers. Reach out to team members if yo
|
|||||||
|
|
||||||
If you find a bug with containers, please file a issue.
|
If you find a bug with containers, please file a issue.
|
||||||
|
|
||||||
Check first if the bug might already exist: https://github.com/mozilla/testpilot-containers/issues
|
Check first if the bug might already exist: https://github.com/mozilla/multi-account-containers/issues
|
||||||
|
|
||||||
[Open an issue](https://github.com/mozilla/testpilot-containers/issues/new)
|
[Open an issue](https://github.com/mozilla/multi-account-containers/issues/new)
|
||||||
|
|
||||||
1. Visit about:support
|
1. Visit about:support
|
||||||
2. Click "Copy raw data to clipboard" and paste into the bug. Alternatively copy the following sections into the issue:
|
2. Click "Copy raw data to clipboard" and paste into the bug. Alternatively copy the following sections into the issue:
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
# Containers Add-on
|
# Multi-Account Containers
|
||||||
|
|
||||||
[](https://testpilot.firefox.com/experiments/containers)
|
The Firefox Multi-Account Containers extension lets you carve out a separate box for each of your online lives – no more opening a different browser just to check your work email! [Learn More Here](https://blog.mozilla.org/firefox/introducing-firefox-multi-account-containers/)
|
||||||
|
|
||||||
[Embedded Web Extension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Embedded_WebExtensions) to build [Containers](https://blog.mozilla.org/tanvi/2016/06/16/contextual-identities-on-the-web/) as a Firefox [Test Pilot](https://testpilot.firefox.com/) Experiment and [Shield Study](https://wiki.mozilla.org/Firefox/Shield/Shield_Studies) to learn:
|
[Available on addons.mozilla.org](https://addons.mozilla.org/en-GB/firefox/addon/multi-account-containers/)
|
||||||
|
|
||||||
* Will a general Firefox audience understand the Containers feature?
|
|
||||||
* Is the UI as currently implemented in Nightly clear or discoverable?
|
|
||||||
|
|
||||||
For more info, see:
|
For more info, see:
|
||||||
|
|
||||||
@@ -16,73 +13,44 @@ For more info, see:
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
* node 7+ (for jpm)
|
* node 7+ (for jpm)
|
||||||
* Firefox 53+
|
* Firefox 57+
|
||||||
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
### Development Environment
|
|
||||||
|
|
||||||
Add-on development is better with [a particular environment](https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment). One simple way to get that environment set up is to install the [DevPrefs add-on](https://addons.mozilla.org/en-US/firefox/addon/devprefs/). You can make a custom Firefox profile that includes the DevPrefs add-on, and use that profile when you run the code in this repository.
|
1. `npm install`
|
||||||
|
2. `./node_modules/.bin/web-ext run -s src/`
|
||||||
1. Make a new profile by running `/path/to/firefox -P`, which launches the profile editor. "Create Profile" -- name it whatever you wish (e.g. 'addon_dev') and store it in the default location. It's probably best to deselect the option to "Use without asking," since you probably don't want to use this as your default profile.
|
|
||||||
|
|
||||||
2. Once you've created your profile, click "Start Firefox". A new instance of Firefox should launch. Go to Tools->Add-ons and search for "DevPrefs". Install it. Quit Firefox.
|
|
||||||
|
|
||||||
3. Now you have a new, vanilla Firefox profile with the DevPrefs add-on installed. You can use your new profile with the code in _this_ repository like so:
|
|
||||||
|
|
||||||
#### Run the `.xpi` file in an unbranded build
|
|
||||||
Release & Beta channels do not allow un-signed add-ons, even with the DevPrefs. So, you must run the add-on in an [unbranded build](https://wiki.mozilla.org/Add-ons/Extension_Signing#Unbranded_Builds):
|
|
||||||
|
|
||||||
1. Download and install an un-branded build of Firefox
|
|
||||||
2. Download the latest `.xpi` from this repository's releases
|
|
||||||
3. Run the un-branded build of Firefox with your DevPrefs profile
|
|
||||||
4. Go to `about:addons`
|
|
||||||
5. Click the gear, and select "Install Add-on From File..."
|
|
||||||
6. Select the `.xpi` file
|
|
||||||
|
|
||||||
#### Correct prefs
|
|
||||||
|
|
||||||
Whilst this is still using legacy code to test you will need the following in your profile:
|
|
||||||
|
|
||||||
|
|
||||||
Change the following prefs in about:config:
|
|
||||||
|
|
||||||
- extensions.legacy.enabled = true
|
|
||||||
- xpinstall.signatures.required = false
|
|
||||||
|
|
||||||
|
|
||||||
#### Run the TxP experiment with `jpm`
|
|
||||||
|
|
||||||
1. `git clone git@github.com:mozilla/testpilot-containers.git`
|
|
||||||
2. `cd testpilot-containers`
|
|
||||||
3. `npm install`
|
|
||||||
4. `./node_modules/.bin/jpm run -p /Path/To/Firefox/Profiles/{junk}.addon_dev -b FirefoxBeta` (where FirefoxBeta might be: ~/<reponame>/obj-x86_64-pc-linux-gnu/dist/bin/firefox or ~/<downloadedFirefoxBeta>/firefox)
|
|
||||||
|
|
||||||
Check out the [Browser Toolbox](https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox) for more information about debugging add-on code.
|
|
||||||
|
|
||||||
### Building .xpi
|
|
||||||
|
|
||||||
To build a local testpilot-containers.xpi, use the plain [`jpm
|
|
||||||
xpi`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_xpi) command,
|
|
||||||
or run `npm run build`.
|
|
||||||
|
|
||||||
### Signing an .xpi
|
|
||||||
|
|
||||||
To sign an .xpi, use [`jpm
|
|
||||||
sign`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_sign)
|
|
||||||
command.
|
|
||||||
|
|
||||||
Note: You will need to be [an author on the AMO
|
|
||||||
add-on](https://addons.mozilla.org/en-US/developers/addon/containers-experiment/ownership).
|
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
TBD
|
TBD
|
||||||
|
|
||||||
### Distributing
|
### Distributing
|
||||||
TBD
|
#### Make the new version
|
||||||
|
|
||||||
|
1. Bump the version number in `package.json` and `manifest.json`
|
||||||
|
2. Commit the version number bump
|
||||||
|
3. Create a git tag for the version: `git tag <version>`
|
||||||
|
4. Push the tag up to GitHub: `git push --tags`
|
||||||
|
|
||||||
|
#### Publish to AMO
|
||||||
|
|
||||||
|
1. `npm run-script build`
|
||||||
|
2. [Upload the `.zip` to AMO](https://addons.mozilla.org/en-US/developers/addon/multi-account-containers/versions/submit/)
|
||||||
|
|
||||||
|
#### Publish to GitHub
|
||||||
|
Finally, we also publish the release to GitHub for those followers.
|
||||||
|
|
||||||
|
1. Download the signed `.xpi` from [the addon versions page](https://addons.mozilla.org/en-US/developers/addon/multi-account-containers/versions)
|
||||||
|
2. [Make the new release on
|
||||||
|
GitHub](https://github.com/mozilla/multi-account-containers/releases/new)
|
||||||
|
* Use the version number for "Tag version" and "Release title"
|
||||||
|
* Release notes: copy the output of `git log --no-merges --pretty=format:"%h %s" <previous-version>..<new-version>`
|
||||||
|
* Attach binaries: select the signed `.xpi` file
|
||||||
|
|
||||||
### Links
|
### Links
|
||||||
|
|
||||||
|
Facebook & Twitter icons CC-Attrib https://fairheadcreative.com.
|
||||||
|
|
||||||
- [Licence](./LICENSE.txt)
|
- [Licence](./LICENSE.txt)
|
||||||
- [Contributing](./CONTRIBUTING.md)
|
- [Contributing](./CONTRIBUTING.md)
|
||||||
- [Code Of Conduct](./CODE_OF_CONDUCT.md)
|
- [Code Of Conduct](./CODE_OF_CONDUCT.md)
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
const PREFS = [
|
|
||||||
{
|
|
||||||
name: "privacy.userContext.enabled",
|
|
||||||
value: true,
|
|
||||||
type: "bool"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "privacy.userContext.longPressBehavior",
|
|
||||||
value: 2,
|
|
||||||
type: "int"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "privacy.userContext.ui.enabled",
|
|
||||||
value: true, // Post web ext we will be setting this true
|
|
||||||
type: "bool"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "privacy.usercontext.about_newtab_segregation.enabled",
|
|
||||||
value: true,
|
|
||||||
type: "bool"
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const Ci = Components.interfaces;
|
|
||||||
const Cu = Components.utils;
|
|
||||||
Cu.import("resource://gre/modules/Services.jsm");
|
|
||||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
||||||
const { TextDecoder, TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
|
|
||||||
|
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
|
||||||
"resource://gre/modules/osfile.jsm");
|
|
||||||
|
|
||||||
const JETPACK_DIR_BASENAME = "jetpack";
|
|
||||||
const EXTENSION_ID = "@testpilot-containers";
|
|
||||||
|
|
||||||
function filename() {
|
|
||||||
const storeFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
|
||||||
storeFile.append(JETPACK_DIR_BASENAME);
|
|
||||||
storeFile.append(EXTENSION_ID);
|
|
||||||
storeFile.append("simple-storage");
|
|
||||||
storeFile.append("store.json");
|
|
||||||
return storeFile.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getConfig() {
|
|
||||||
const bytes = await OS.File.read(filename());
|
|
||||||
const raw = new TextDecoder().decode(bytes) || "";
|
|
||||||
let savedConfig = {savedConfiguration: {}};
|
|
||||||
if (raw) {
|
|
||||||
savedConfig = JSON.parse(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
return savedConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initConfig() {
|
|
||||||
const savedConfig = await getConfig();
|
|
||||||
savedConfig.savedConfiguration.version = 2;
|
|
||||||
if (!("prefs" in savedConfig.savedConfiguration)) {
|
|
||||||
savedConfig.savedConfiguration.prefs = {};
|
|
||||||
PREFS.forEach((pref) => {
|
|
||||||
if ("int" === pref.type) {
|
|
||||||
savedConfig.savedConfiguration.prefs[pref.name] = Services.prefs.getIntPref(pref.name, pref.name);
|
|
||||||
} else {
|
|
||||||
savedConfig.savedConfiguration.prefs[pref.name] = Services.prefs.getBoolPref(pref.name, pref.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const serialized = JSON.stringify(savedConfig);
|
|
||||||
const bytes = new TextEncoder().encode(serialized) || "";
|
|
||||||
await OS.File.writeAtomic(filename(), bytes, { });
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPrefs() {
|
|
||||||
PREFS.forEach((pref) => {
|
|
||||||
if ("int" === pref.type) {
|
|
||||||
Services.prefs.setIntPref(pref.name, pref.value);
|
|
||||||
} else {
|
|
||||||
Services.prefs.setBoolPref(pref.name, pref.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
async function install() {
|
|
||||||
await initConfig();
|
|
||||||
setPrefs();
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
async function uninstall(aData, aReason) {
|
|
||||||
if (aReason === ADDON_UNINSTALL
|
|
||||||
|| aReason === ADDON_DISABLE) {
|
|
||||||
const config = await getConfig();
|
|
||||||
const storedPrefs = config.savedConfiguration.prefs;
|
|
||||||
PREFS.forEach((pref) => {
|
|
||||||
if (pref.name in storedPrefs) {
|
|
||||||
if ("int" === pref.type) {
|
|
||||||
Services.prefs.setIntPref(pref.name, storedPrefs[pref.name]);
|
|
||||||
} else {
|
|
||||||
Services.prefs.setBoolPref(pref.name, storedPrefs[pref.name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
function startup({webExtension}) {
|
|
||||||
// Reset prefs that may have changed, or are legacy
|
|
||||||
setPrefs();
|
|
||||||
// Start the embedded webextension.
|
|
||||||
webExtension.startup();
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
function shutdown() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,108 +1,3 @@
|
|||||||
/* HACK: Custom Container vars do not propigate correctly
|
|
||||||
until the container tab is blurred and refocused,
|
|
||||||
adding the data-identity-color with the default hex
|
|
||||||
value, or chrome url path as an alternate selector mitiages this bug.*/
|
|
||||||
[data-identity-color="blue"],
|
|
||||||
[data-identity-color="#00a7e0"] {
|
|
||||||
--identity-tab-color: #37adff;
|
|
||||||
--identity-icon-color: #37adff;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="turquoise"],
|
|
||||||
[data-identity-color="#01bdad"] {
|
|
||||||
--identity-tab-color: #00c79a;
|
|
||||||
--identity-icon-color: #00c79a;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="green"],
|
|
||||||
[data-identity-color="#7dc14c"] {
|
|
||||||
--identity-tab-color: #51cd00;
|
|
||||||
--identity-icon-color: #51cd00;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="yellow"],
|
|
||||||
[data-identity-color="#ffcb00"] {
|
|
||||||
--identity-tab-color: #ffcb00;
|
|
||||||
--identity-icon-color: #ffcb00;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="orange"],
|
|
||||||
[data-identity-color="#f89c24"] {
|
|
||||||
--identity-tab-color: #ff9f00;
|
|
||||||
--identity-icon-color: #ff9f00;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="red"],
|
|
||||||
[data-identity-color="#d92215"] {
|
|
||||||
--identity-tab-color: #ff613d;
|
|
||||||
--identity-icon-color: #ff613d;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="pink"],
|
|
||||||
[data-identity-color="#ee5195"] {
|
|
||||||
--identity-tab-color: #ff4bda;
|
|
||||||
--identity-icon-color: #ff4bda;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-color="purple"],
|
|
||||||
[data-identity-color="#7a2f7a"] {
|
|
||||||
--identity-tab-color: #af51f5;
|
|
||||||
--identity-icon-color: #af51f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="fingerprint"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/personal.svg"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#fingerprint");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="briefcase"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/work.svg"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#briefcase");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="dollar"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/banking.svg"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#dollar");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="cart"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/cart.svg"],
|
|
||||||
[data-identity-icon="chrome://browser/skin/usercontext/shopping.svg"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#cart");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="circle"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#circle");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="gift"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#gift");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="vacation"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#vacation");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="food"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#food");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="fruit"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#fruit");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="pet"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#pet");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="tree"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#tree");
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-identity-icon="chill"] {
|
|
||||||
--identity-icon: url("/data/usercontext.svg#chill");
|
|
||||||
}
|
|
||||||
|
|
||||||
#userContext-indicator {
|
#userContext-indicator {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
list-style-image: none !important;
|
list-style-image: none !important;
|
||||||
@@ -129,19 +24,6 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.userContext-icon,
|
|
||||||
.menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon,
|
|
||||||
.subviewbutton[usercontextid] > .toolbarbutton-icon,
|
|
||||||
#userContext-indicator {
|
|
||||||
background-image: var(--identity-icon) !important;
|
|
||||||
background-position: center center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
fill: var(--identity-icon-color) !important;
|
|
||||||
filter: url(/img/filters.svg#fill);
|
|
||||||
filter: url(/data/filters.svg#fill);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* containers experiment */
|
/* containers experiment */
|
||||||
|
|
||||||
/* reset nightly containers */
|
/* reset nightly containers */
|
||||||
|
|||||||
@@ -1,285 +0,0 @@
|
|||||||
# METRICS
|
|
||||||
|
|
||||||
## Data Analysis
|
|
||||||
The collected data will primarily be used to answer the following questions.
|
|
||||||
Images are used for visualization and are not composed of actual data.
|
|
||||||
|
|
||||||
### Do users install and run this?
|
|
||||||
|
|
||||||
What is the overall engagement of the Containers experiment?
|
|
||||||
**This is the standard Daily Active User (DAU) and Monthly Active User (MAU) analysis.**
|
|
||||||
|
|
||||||
This captures data from the users who have the add-on installed, regardless of
|
|
||||||
whether they are actively interacting with it.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Immediate Questions
|
|
||||||
|
|
||||||
* Do people use the containers feature & how do people create new container tabs?
|
|
||||||
* Click to create new container tab
|
|
||||||
* \+ `entry-point` value: "tab-bar" or "pop-up"
|
|
||||||
* Do people who use the containers feature continue to use it?
|
|
||||||
* Retention: opening a second container tab (second tab in the same container, or a tab in a second container?)
|
|
||||||
* What containers do people use?
|
|
||||||
* userContextId
|
|
||||||
* \+ Number of tabs in the container (when should we measure this? on every tab open?)
|
|
||||||
* Do people edit their containers?
|
|
||||||
* Click on "Edit Containers"
|
|
||||||
* Click to edit a single container
|
|
||||||
* Click "OK"
|
|
||||||
* Click to delete a single container
|
|
||||||
* Click "OK"
|
|
||||||
* Click to add a container
|
|
||||||
* Click "OK"
|
|
||||||
* Do people sort the tabs?
|
|
||||||
* Click sort
|
|
||||||
* \+ Number of tabs when clicked
|
|
||||||
* Average number of container tabs when sort was clicked
|
|
||||||
* Do users show and hide container tabs?
|
|
||||||
* Click hide
|
|
||||||
* \+ Number of tabs when clicked
|
|
||||||
* \+ Number of hidden containers when clicked
|
|
||||||
* Click show
|
|
||||||
* \+ Number of tabs when clicked
|
|
||||||
* \+ Number of shown containers when clicked
|
|
||||||
* Do users move container tabs to new windows?
|
|
||||||
* Click move
|
|
||||||
* \+ Number of tabs when clicked
|
|
||||||
* Average number of container tabs when new window was clicked
|
|
||||||
* How many containers do users have hidden at the same time? (when should we measure this? each time a container is hidden?)
|
|
||||||
* Do users pin container tabs? (do we have existing Telemetry for pinning?)
|
|
||||||
* Do users visit more pages in container tabs than non-container tabs?
|
|
||||||
|
|
||||||
### Follow-up Questions
|
|
||||||
|
|
||||||
What are some follow-up questions we anticipate we will ask based on any of the
|
|
||||||
above answers/data?
|
|
||||||
|
|
||||||
* What is the average lifespan of a container tab? Is that longer or shorter than a regular tab? (if we don't have data on the latter, the former probably isn't worth gathering data on since we will have nothing to compare it to).
|
|
||||||
|
|
||||||
## Data Collection
|
|
||||||
|
|
||||||
### Server Side
|
|
||||||
There is currently no server side component to Containers.
|
|
||||||
|
|
||||||
### Client Side
|
|
||||||
Containers will use Test Pilot Telemetry with no batching of data. Details
|
|
||||||
of when pings are sent are below, along with examples of the `payload` portion
|
|
||||||
of a `testpilottest` telemetry ping for each scenario.
|
|
||||||
|
|
||||||
* The user shows the new tab menu
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "show-plus-button-menu",
|
|
||||||
"eventSource": ["plus-button"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks on a container name to open a tab in that container
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
|
||||||
"event": "open-tab",
|
|
||||||
"eventSource": ["tab-bar"|"pop-up"|"file-menu"|"alltabs-menu"|"plus-button"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Edit Containers" in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "edit-containers"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks OK after clicking on a container edit icon in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "edit-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks OK after clicking on a container delete icon in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "delete-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks OK after clicking to add a container in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "add-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks the sort button/icon in the pop-up
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "sort-tabs",
|
|
||||||
"shownContainersCount": <number-of-containers-with-tabs-shown>,
|
|
||||||
"totalContainerTabsCount": <number-of-all-container-tabs>,
|
|
||||||
"totalNonContainerTabsCount": <number-of-all-non-container-tabs>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Hide these container tabs" in the popup
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
|
||||||
"event": "hide-tabs",
|
|
||||||
"hiddenContainersCount": <number-of-containers-with-tabs-hidden>,
|
|
||||||
"shownContainersCount": <number-of-containers-with-tabs-shown>,
|
|
||||||
"totalContainersCount": <number-of-containers-with-tabs-hidden-or-shown>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Show these container tabs" in the popup
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
|
||||||
"event": "show-tabs",
|
|
||||||
"hiddenContainersCount": <number-of-containers-with-tabs-hidden>,
|
|
||||||
"shownContainersCount": <number-of-containers-with-tabs-shown>,
|
|
||||||
"totalContainersCount": <number-of-containers-with-tabs-hidden-or-shown>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Move tabs to a new window" in the popup
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
|
||||||
"event": "move-tabs-to-window"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* When a user encounters the disabled "move" feature because of incompatible add-ons
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "incompatible-addons-detected"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user closes a tab
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "page-requests-completed-per-tab",
|
|
||||||
"pageRequestCount": <pageRequestCount>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user goes idle
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "page-requests-completed-per-activity",
|
|
||||||
"pageRequestCount": <pageRequestCount>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user chooses "Always Open in this Container" context menu option. (Note: We send two separate event names: one for assigning a site to a container, one for removing a site from a container.)
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "[added|removed]-container-assignment"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Firefox prompts the user to reload a site into a container after the user picked "Always Open in this Container".
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "prompt-reload-page-in-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Open in *assigned* container" to reload a site into a container after the user picked "Always Open in this Container".
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "click-to-reload-page-in-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* The user clicks "Open in *Current* container" to reload a site into a container after the user picked "Always Open in this Container".
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"event": "click-to-reload-page-in-same-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Firefox automatically reloads a site into a container after the user picked "Always Open in this Container".
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"uuid": <uuid>,
|
|
||||||
"userContextId": <userContextId>,
|
|
||||||
"event": "auto-reload-page-in-container"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### A Redshift schema for the payload:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local schema = {
|
|
||||||
-- column name field type length attributes field name
|
|
||||||
{"uuid", "VARCHAR", 255, nil, "Fields[payload.uuid]"},
|
|
||||||
{"userContextId", "INTEGER", 255, nil, "Fields[payload.userContextId]"},
|
|
||||||
{"clickedContainerTabCount", "INTEGER", 255, nil, "Fields[payload.clickedContainerTabCount]"},
|
|
||||||
{"eventSource", "VARCHAR", 255, nil, "Fields[payload.eventSource]"},
|
|
||||||
{"event", "VARCHAR", 255, nil, "Fields[payload.event]"},
|
|
||||||
{"pageRequestCount", "INTEGER", 255, nil, "Fields[payload.pageRequestCount]"}
|
|
||||||
{"hiddenContainersCount", "INTEGER", 255, nil, "Fields[payload.hiddenContainersCount]"},
|
|
||||||
{"shownContainersCount", "INTEGER", 255, nil, "Fields[payload.shownContainersCount]"},
|
|
||||||
{"totalContainersCount", "INTEGER", 255, nil, "Fields[payload.totalContainersCount]"},
|
|
||||||
{"totalContainerTabsCount", "INTEGER", 255, nil, "Fields[payload.totalContainerTabsCount]"},
|
|
||||||
{"totalNonContainerTabsCount", "INTEGER", 255, nil, "Fields[payload.totalNonContainerTabsCount]"}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Valid data should be enforced on the server side:
|
|
||||||
|
|
||||||
* `eventSource` should be one of `tab-bar`, `pop-up`, `file-menu`, "alltabs-nmenu" or "plus-button".
|
|
||||||
|
|
||||||
All Mozilla data is kept by default for 180 days and in accordance with our
|
|
||||||
privacy policies.
|
|
||||||
@@ -1,22 +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>@testpilot-containers</em:id>
|
|
||||||
<em:type>2</em:type>
|
|
||||||
<em:bootstrap>true</em:bootstrap>
|
|
||||||
<em:multiprocessCompatible>true</em:multiprocessCompatible>
|
|
||||||
<em:hasEmbeddedWebExtension>true</em:hasEmbeddedWebExtension>
|
|
||||||
<em:name>Testpilot containers</em:name>
|
|
||||||
<em:targetApplication>
|
|
||||||
<Description>
|
|
||||||
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
|
|
||||||
<em:minVersion>51.0a1</em:minVersion>
|
|
||||||
<em:maxVersion>*</em:maxVersion>
|
|
||||||
</Description>
|
|
||||||
</em:targetApplication>
|
|
||||||
<em:version>3.1.0</em:version>
|
|
||||||
<em:unpack>false</em:unpack>
|
|
||||||
</Description>
|
|
||||||
</RDF>
|
|
||||||
|
|
||||||
@@ -1,44 +1,47 @@
|
|||||||
{
|
{
|
||||||
"name": "testpilot-containers",
|
"name": "testpilot-containers",
|
||||||
"title": "Containers Experiment",
|
"title": "Multi-Account Containers",
|
||||||
"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.",
|
"description": "Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
|
||||||
"version": "3.1.0",
|
"version": "6.0.1",
|
||||||
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/mozilla/testpilot-containers/issues"
|
"url": "https://github.com/mozilla/multi-account-containers/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"addons-linter": "^0.15.14",
|
"addons-linter": "^1.3.2",
|
||||||
"deploy-txp": "^1.0.7",
|
"chai": "^4.1.2",
|
||||||
"eslint": "^3.17.1",
|
"eslint": "^3.17.1",
|
||||||
"eslint-plugin-no-unsanitized": "^2.0.0",
|
"eslint-plugin-no-unsanitized": "^2.0.0",
|
||||||
"eslint-plugin-promise": "^3.4.0",
|
"eslint-plugin-promise": "^3.4.0",
|
||||||
"htmllint-cli": "^0.0.5",
|
"htmllint-cli": "0.0.7",
|
||||||
"jpm": "^1.2.2",
|
"jsdom": "^11.6.2",
|
||||||
"json": "^9.0.6",
|
"json": "^9.0.6",
|
||||||
|
"mocha": "^5.0.0",
|
||||||
"npm-run-all": "^4.0.0",
|
"npm-run-all": "^4.0.0",
|
||||||
|
"sinon": "^4.4.0",
|
||||||
|
"sinon-chai": "^2.14.0",
|
||||||
"stylelint": "^7.9.0",
|
"stylelint": "^7.9.0",
|
||||||
"stylelint-config-standard": "^16.0.0",
|
"stylelint-config-standard": "^16.0.0",
|
||||||
"stylelint-order": "^0.3.0"
|
"stylelint-order": "^0.3.0",
|
||||||
|
"web-ext": "^2.2.2"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/mozilla/testpilot-containers#readme",
|
"homepage": "https://github.com/mozilla/multi-account-containers#readme",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/mozilla/testpilot-containers.git"
|
"url": "git+https://github.com/mozilla/multi-account-containers.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm test && jpm xpi",
|
"build": "npm test && cd src && web-ext build --overwrite-dest",
|
||||||
"deploy": "deploy-txp",
|
|
||||||
"lint": "npm-run-all lint:*",
|
"lint": "npm-run-all lint:*",
|
||||||
"lint:addon": "addons-linter webextension --self-hosted",
|
"lint:addon": "addons-linter src --self-hosted",
|
||||||
"lint:css": "stylelint webextension/css/*.css",
|
"lint:css": "stylelint src/css/*.css",
|
||||||
"lint:html": "htmllint webextension/*.html",
|
"lint:html": "htmllint *.html",
|
||||||
"lint:js": "eslint .",
|
"lint:js": "eslint .",
|
||||||
"package": "npm run build && mv testpilot-containers.xpi addon.xpi",
|
"package": "rm -rf src/web-ext-artifacts && npm run build && mv src/web-ext-artifacts/firefox_multi-account_containers-*.zip addon.xpi",
|
||||||
"test": "npm run lint"
|
"test": "npm run lint && mocha ./test/setup.js test/**/*.test.js",
|
||||||
},
|
"test-watch": "mocha ./test/setup.js test/**/*.test.js --watch"
|
||||||
"updateURL": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
<title>Containers confirm navigation</title>
|
<title>Multi-Account Containers Confirm Navigation</title>
|
||||||
<link xmlns="http://www.w3.org/1999/xhtml" rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
|
<link xmlns="http://www.w3.org/1999/xhtml" rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
|
||||||
<link rel="stylesheet" href="/css/confirm-page.css" />
|
<link rel="stylesheet" href="/css/confirm-page.css" />
|
||||||
</head>
|
</head>
|
||||||
@@ -45,6 +45,7 @@ body {
|
|||||||
--small-text-size: 0.833rem; /* 10px */
|
--small-text-size: 0.833rem; /* 10px */
|
||||||
--small-radius: 3px;
|
--small-radius: 3px;
|
||||||
--icon-button-size: calc(calc(var(--block-line-separation-size) * 2) + 1.66rem); /* 20px */
|
--icon-button-size: calc(calc(var(--block-line-separation-size) * 2) + 1.66rem); /* 20px */
|
||||||
|
--inactive-opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-resolution: 1dppx) {
|
@media (min-resolution: 1dppx) {
|
||||||
@@ -242,7 +243,8 @@ table {
|
|||||||
min-block-size: 400px;
|
min-block-size: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel.onboarding {
|
.panel.onboarding,
|
||||||
|
.achievement-panel {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
block-size: 360px;
|
block-size: 360px;
|
||||||
margin-block-end: 16px;
|
margin-block-end: 16px;
|
||||||
@@ -536,7 +538,7 @@ span ~ .panel-header-text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#current-tab > label > input:checked {
|
#current-tab > label > input:checked {
|
||||||
background-image: url("chrome://global/skin/in-content/check.svg#check-native");
|
background-image: url("/img/check.svg");
|
||||||
background-position: -1px -1px;
|
background-position: -1px -1px;
|
||||||
background-size: var(--icon-size);
|
background-size: var(--icon-size);
|
||||||
}
|
}
|
||||||
@@ -577,6 +579,11 @@ span ~ .panel-header-text {
|
|||||||
max-inline-size: 204px;
|
max-inline-size: 204px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disable-edit-containers {
|
||||||
|
opacity: var(--inactive-opacity);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.userContext-wrapper {
|
.userContext-wrapper {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -887,3 +894,53 @@ span ~ .panel-header-text {
|
|||||||
font-size: 14px !important;
|
font-size: 14px !important;
|
||||||
padding-block-end: 6px;
|
padding-block-end: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Achievement panel elements */
|
||||||
|
.share-ctas {
|
||||||
|
padding-block-end: 0.5em;
|
||||||
|
padding-block-start: 0.5em;
|
||||||
|
padding-inline-end: 0.5em;
|
||||||
|
padding-inline-start: 0.5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.7em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-block-end: 0.4em;
|
||||||
|
margin-block-start: 0.4em;
|
||||||
|
margin-inline-end: 0.4em;
|
||||||
|
margin-inline-start: 0.4em;
|
||||||
|
padding-block-end: 0.5em;
|
||||||
|
padding-block-start: 0.5em;
|
||||||
|
padding-inline-end: 0.5em;
|
||||||
|
padding-inline-start: 0.5em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-icon {
|
||||||
|
height: 18px;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fb-share-cta {
|
||||||
|
background: #375496;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fb-share-cta .cta-icon {
|
||||||
|
margin-block-start: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweet-cta {
|
||||||
|
background: #37bae7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amo-rate-cta {
|
||||||
|
background: #0f1126;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg width="32px" height="33px" viewBox="0 0 32 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch --> <desc>Created with Sketch.</desc> <defs> <linearGradient x1="74.0423237%" y1="18.5882821%" x2="0%" y2="100%" id="linearGradient-1"> <stop stop-color="#00FEFF" offset="0%"/> <stop stop-color="#3D85FF" offset="100%"/> </linearGradient> </defs> <g id="Specs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Header-Copy" transform="translate(-182.000000, -152.000000)" fill="url(#linearGradient-1)"> <path d="M205.58574,176.859518 L205.58574,169.287998 C205.58574,169.287998 205.800116,167.315137 207.086372,167.315137 C208.372629,167.315137 208.265441,169.394639 210.677171,169.394639 C211.909834,169.394639 214,168.754792 214,165.022352 C214,161.289912 211.909834,160.810027 210.677171,160.810027 C208.265441,160.810027 208.372629,162.782888 207.086372,162.782888 C205.800116,162.782888 205.58574,160.756707 205.58574,160.756707 L205.58574,157.664114 C205.58574,156.491061 204.621048,155.531291 203.44198,155.531291 L197.814608,155.531291 C197.814608,155.531291 195.992412,155.211368 195.992412,153.931674 C195.992412,152.65198 198.028985,152.545339 198.028985,150.145914 C198.028985,148.91954 197.332262,147 193.580682,147 C189.829101,147 189.293161,148.91954 189.293161,150.145914 C189.293161,152.545339 191.115357,152.65198 191.115357,153.931674 C191.115357,155.211368 189.293161,155.531291 189.293161,155.531291 L184.148135,155.531291 C182.969067,155.531291 182.004375,156.491061 182.004375,157.664114 L182.004375,161.823118 C182.004375,161.823118 181.789999,165.022352 184.362512,165.022352 C186.023926,165.022352 186.07752,162.836209 188.274874,162.836209 C189.346755,162.836209 190.418635,163.8493 190.418635,166.035443 C190.418635,168.274907 189.346755,169.394639 188.274874,169.394639 C186.131114,169.394639 186.023926,167.208496 184.362512,167.208496 C181.789999,167.208496 182.004375,170.301089 182.004375,170.301089 L182.004375,176.859518 C182.004375,178.032571 182.969067,178.992341 184.148135,178.992341 L191.115357,178.992341 C191.115357,178.992341 194.49178,179.205623 194.49178,176.646236 C194.49178,174.993299 192.348019,174.726696 192.348019,172.540552 C192.348019,171.474141 193.527088,170.141127 195.778036,170.141127 C198.028985,170.141127 199.315241,171.474141 199.315241,172.540552 C199.315241,174.673375 197.225074,174.993299 197.225074,176.646236 C197.225074,179.258944 200.601497,178.992341 200.601497,178.992341 L203.44198,178.992341 C204.621048,178.992341 205.58574,178.032571 205.58574,176.859518 Z" id="Shape-Copy-23" transform="translate(198.000000, 163.000000) rotate(-42.000000) translate(-198.000000, -163.000000) "/> </g> </g> </svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- 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/. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path fill="#3c3c3c" d="M6 14a1 1 0 0 1-.707-.293l-3-3a1 1 0 0 1 1.414-1.414l2.157 2.157 6.316-9.023a1 1 0 0 1 1.639 1.146l-7 10a1 1 0 0 1-.732.427A.863.863 0 0 1 6 14z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 477 B |
|
Before Width: | Height: | Size: 595 B After Width: | Height: | Size: 595 B |
|
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 520 B |
|
Before Width: | Height: | Size: 626 B After Width: | Height: | Size: 626 B |
|
Before Width: | Height: | Size: 603 B After Width: | Height: | Size: 603 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 883 B After Width: | Height: | Size: 883 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 342 B After Width: | Height: | Size: 342 B |
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
|
Before Width: | Height: | Size: 534 B After Width: | Height: | Size: 534 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 755 B After Width: | Height: | Size: 755 B |
|
Before Width: | Height: | Size: 399 B After Width: | Height: | Size: 399 B |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="-14 -14 48 48" enable-background="new -14 -14 48 48" xml:space="preserve">
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="90.0527" y1="-99.7603" x2="90.0527" y2="-106.3809" gradientTransform="matrix(7.2338 0 0 -7.2338 -641.4998 -735.5619)">
|
||||||
|
<stop offset="0" style="stop-color:#4B71B8"/>
|
||||||
|
<stop offset="1" style="stop-color:#293F7E"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path fill="url(#SVGID_1_)" d="M33.931,27.993c0,3.304-2.689,5.983-6.002,5.983H-8.082c-3.315,0-6.001-2.683-6.001-5.983V-7.928
|
||||||
|
c0-3.308,2.687-5.988,6.001-5.988h36.011c3.312,0,6.002,2.681,6.002,5.988V27.993z"/>
|
||||||
|
<path fill="#FFFFFF" d="M25.613-4.557c0,0-3.707,0-6.166,0c-3.662,0-7.732,1.535-7.732,6.835c0.019,1.845,0,3.613,0,5.603H7.481
|
||||||
|
v6.728h4.366v19.37h8.021V14.48h5.295l0.479-6.618h-5.913c0,0,0.016-2.946,0-3.8c0-2.093,2.184-1.974,2.312-1.974
|
||||||
|
c1.042,0,3.059,0.003,3.578,0v-6.646H25.613z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="23.9995" y1="0" x2="23.9995" y2="48.0005">
|
||||||
|
<stop offset="0" style="stop-color:#4BD0EF"/>
|
||||||
|
<stop offset="1" style="stop-color:#29AAE1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" fill="url(#SVGID_1_)" d="M48,42c0,3.313-2.687,6-6,6H6c-3.313,0-6-2.687-6-6V6
|
||||||
|
c0-3.313,2.687-6,6-6h36c3.313,0,6,2.687,6,6V42z"/>
|
||||||
|
<path fill="#29AAE1" d="M40.231,13.413c-1.12,0.497-2.323,0.833-3.588,0.984c1.291-0.774,2.28-1.998,2.747-3.457
|
||||||
|
c-1.206,0.716-2.543,1.236-3.968,1.516c-1.139-1.214-2.763-1.972-4.56-1.972c-3.449,0-6.246,2.796-6.246,6.247
|
||||||
|
c0,0.49,0.055,0.966,0.161,1.424c-5.192-0.261-9.795-2.749-12.876-6.528c-0.538,0.923-0.846,1.996-0.846,3.141
|
||||||
|
c0,2.167,1.103,4.08,2.779,5.199c-1.024-0.032-1.987-0.313-2.83-0.781c0,0.026,0,0.053,0,0.079c0,3.026,2.153,5.551,5.011,6.125
|
||||||
|
c-0.525,0.143-1.076,0.219-1.646,0.219c-0.403,0-0.794-0.038-1.176-0.11c0.795,2.48,3.102,4.287,5.835,4.338
|
||||||
|
c-2.138,1.675-4.832,2.675-7.758,2.675c-0.504,0-1.002-0.03-1.491-0.089c2.765,1.773,6.048,2.808,9.576,2.808
|
||||||
|
c11.49,0,17.774-9.519,17.774-17.774c0-0.271-0.006-0.54-0.019-0.809C38.334,15.766,39.394,14.666,40.231,13.413z"/>
|
||||||
|
<path fill="#FFFFFF" d="M40.231,14.739c-1.12,0.497-2.323,0.833-3.588,0.984c1.291-0.773,2.28-1.998,2.747-3.456
|
||||||
|
c-1.206,0.716-2.543,1.236-3.968,1.516c-1.139-1.214-2.763-1.972-4.56-1.972c-3.449,0-6.246,2.796-6.246,6.247
|
||||||
|
c0,0.489,0.055,0.966,0.161,1.424c-5.192-0.261-9.795-2.748-12.876-6.527c-0.538,0.923-0.846,1.996-0.846,3.141
|
||||||
|
c0,2.167,1.103,4.079,2.779,5.199c-1.024-0.032-1.987-0.313-2.83-0.781c0,0.026,0,0.052,0,0.079c0,3.027,2.153,5.551,5.011,6.125
|
||||||
|
c-0.525,0.144-1.076,0.219-1.646,0.219c-0.403,0-0.794-0.038-1.176-0.11c0.795,2.481,3.102,4.287,5.835,4.338
|
||||||
|
c-2.138,1.676-4.832,2.675-7.758,2.675c-0.504,0-1.002-0.03-1.491-0.089c2.765,1.773,6.048,2.808,9.576,2.808
|
||||||
|
c11.49,0,17.774-9.519,17.774-17.774c0-0.271-0.006-0.54-0.019-0.808C38.334,17.092,39.394,15.992,40.231,14.739z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -7,7 +7,6 @@ module.exports = {
|
|||||||
"badge": true,
|
"badge": true,
|
||||||
"backgroundLogic": true,
|
"backgroundLogic": true,
|
||||||
"identityState": true,
|
"identityState": true,
|
||||||
"messageHandler": true,
|
"messageHandler": true
|
||||||
"tabPageCounter": true
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
const assignManager = {
|
const assignManager = {
|
||||||
MENU_ASSIGN_ID: "open-in-this-container",
|
MENU_ASSIGN_ID: "open-in-this-container",
|
||||||
MENU_REMOVE_ID: "remove-open-in-this-container",
|
MENU_REMOVE_ID: "remove-open-in-this-container",
|
||||||
|
MENU_SEPARATOR_ID: "separator",
|
||||||
|
MENU_HIDE_ID: "hide-container",
|
||||||
|
MENU_MOVE_ID: "move-to-new-window-container",
|
||||||
|
|
||||||
storageArea: {
|
storageArea: {
|
||||||
area: browser.storage.local,
|
area: browser.storage.local,
|
||||||
exemptedTabs: {},
|
exemptedTabs: {},
|
||||||
@@ -109,69 +113,161 @@ const assignManager = {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Before a request is handled by the browser we decide if we should route through a different container
|
||||||
|
async onBeforeRequest(options) {
|
||||||
|
if (options.frameId !== 0 || options.tabId === -1) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
this.removeContextMenu();
|
||||||
|
const [tab, siteSettings] = await Promise.all([
|
||||||
|
browser.tabs.get(options.tabId),
|
||||||
|
this.storageArea.get(options.url)
|
||||||
|
]);
|
||||||
|
let container;
|
||||||
|
try {
|
||||||
|
container = await browser.contextualIdentities.get(backgroundLogic.cookieStoreId(siteSettings.userContextId));
|
||||||
|
} catch (e) {
|
||||||
|
container = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The container we have in the assignment map isn't present any more so lets remove it
|
||||||
|
// then continue the existing load
|
||||||
|
if (siteSettings && !container) {
|
||||||
|
this.deleteContainer(siteSettings.userContextId);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||||
|
if (!siteSettings
|
||||||
|
|| userContextId === siteSettings.userContextId
|
||||||
|
|| tab.incognito
|
||||||
|
|| this.storageArea.isExempted(options.url, tab.id)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const removeTab = backgroundLogic.NEW_TAB_PAGES.has(tab.url)
|
||||||
|
|| (messageHandler.lastCreatedTab
|
||||||
|
&& messageHandler.lastCreatedTab.id === tab.id);
|
||||||
|
const openTabId = removeTab ? tab.openerTabId : tab.id;
|
||||||
|
|
||||||
|
if (!this.canceledRequests[tab.id]) {
|
||||||
|
// we decided to cancel the request at this point, register canceled request
|
||||||
|
this.canceledRequests[tab.id] = {
|
||||||
|
requestIds: {
|
||||||
|
[options.requestId]: true
|
||||||
|
},
|
||||||
|
urls: {
|
||||||
|
[options.url]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// since webRequest onCompleted and onErrorOccurred are not 100% reliable (see #1120)
|
||||||
|
// we register a timer here to cleanup canceled requests, just to make sure we don't
|
||||||
|
// end up in a situation where certain urls in a tab.id stay canceled
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.canceledRequests[tab.id]) {
|
||||||
|
delete this.canceledRequests[tab.id];
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
let cancelEarly = false;
|
||||||
|
if (this.canceledRequests[tab.id].requestIds[options.requestId] ||
|
||||||
|
this.canceledRequests[tab.id].urls[options.url]) {
|
||||||
|
// same requestId or url from the same tab
|
||||||
|
// this is a redirect that we have to cancel early to prevent opening two tabs
|
||||||
|
cancelEarly = true;
|
||||||
|
}
|
||||||
|
// we decided to cancel the request at this point, register canceled request
|
||||||
|
this.canceledRequests[tab.id].requestIds[options.requestId] = true;
|
||||||
|
this.canceledRequests[tab.id].urls[options.url] = true;
|
||||||
|
if (cancelEarly) {
|
||||||
|
return {
|
||||||
|
cancel: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reloadPageInContainer(
|
||||||
|
options.url,
|
||||||
|
userContextId,
|
||||||
|
siteSettings.userContextId,
|
||||||
|
tab.index + 1,
|
||||||
|
tab.active,
|
||||||
|
siteSettings.neverAsk,
|
||||||
|
openTabId
|
||||||
|
);
|
||||||
|
this.calculateContextMenu(tab);
|
||||||
|
|
||||||
|
/* Removal of existing tabs:
|
||||||
|
We aim to open the new assigned container tab / warning prompt in it's own tab:
|
||||||
|
- As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back()
|
||||||
|
- When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421
|
||||||
|
If we are coming from an internal url that are used for the new tab page (NEW_TAB_PAGES), we can safely close as user is unlikely losing history
|
||||||
|
Detecting redirects on "new tab" opening actions is pretty hard as we don't get tab history:
|
||||||
|
- Redirects happen from Short URLs and tracking links that act as a gateway
|
||||||
|
- Extensions don't provide a way to history crawl for tabs, we could inject content scripts to do this
|
||||||
|
however they don't run on about:blank so this would likely be just as hacky.
|
||||||
|
We capture the time the tab was created and close if it was within the timeout to try to capture pages which haven't had user interaction or history.
|
||||||
|
*/
|
||||||
|
if (removeTab) {
|
||||||
|
browser.tabs.remove(tab.id);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
cancel: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||||
this._onClickedHandler(info, tab);
|
this._onClickedHandler(info, tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Before a request is handled by the browser we decide if we should route through a different container
|
// Before a request is handled by the browser we decide if we should route through a different container
|
||||||
|
this.canceledRequests = {};
|
||||||
browser.webRequest.onBeforeRequest.addListener((options) => {
|
browser.webRequest.onBeforeRequest.addListener((options) => {
|
||||||
if (options.frameId !== 0 || options.tabId === -1) {
|
return this.onBeforeRequest(options);
|
||||||
return {};
|
|
||||||
}
|
|
||||||
this.removeContextMenu();
|
|
||||||
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
|
|
||||||
|| this.storageArea.isExempted(options.url, tab.id)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reloadPageInContainer(options.url, userContextId, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk);
|
|
||||||
this.calculateContextMenu(tab);
|
|
||||||
|
|
||||||
/* Removal of existing tabs:
|
|
||||||
We aim to open the new assigned container tab / warning prompt in it's own tab:
|
|
||||||
- As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back()
|
|
||||||
- When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421
|
|
||||||
If we are coming from an internal url that are used for the new tab page (NEW_TAB_PAGES), we can safely close as user is unlikely losing history
|
|
||||||
Detecting redirects on "new tab" opening actions is pretty hard as we don't get tab history:
|
|
||||||
- Redirects happen from Short URLs and tracking links that act as a gateway
|
|
||||||
- Extensions don't provide a way to history crawl for tabs, we could inject content scripts to do this
|
|
||||||
however they don't run on about:blank so this would likely be just as hacky.
|
|
||||||
We capture the time the tab was created and close if it was within the timeout to try to capture pages which haven't had user interaction or history.
|
|
||||||
*/
|
|
||||||
if (backgroundLogic.NEW_TAB_PAGES.has(tab.url)
|
|
||||||
|| (messageHandler.lastCreatedTab
|
|
||||||
&& messageHandler.lastCreatedTab.id === tab.id)) {
|
|
||||||
browser.tabs.remove(tab.id);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
cancel: true,
|
|
||||||
};
|
|
||||||
}).catch((e) => {
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
|
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
|
||||||
|
|
||||||
|
// Clean up canceled requests
|
||||||
|
browser.webRequest.onCompleted.addListener((options) => {
|
||||||
|
if (this.canceledRequests[options.tabId]) {
|
||||||
|
delete this.canceledRequests[options.tabId];
|
||||||
|
}
|
||||||
|
},{urls: ["<all_urls>"], types: ["main_frame"]});
|
||||||
|
browser.webRequest.onErrorOccurred.addListener((options) => {
|
||||||
|
if (this.canceledRequests[options.tabId]) {
|
||||||
|
delete this.canceledRequests[options.tabId];
|
||||||
|
}
|
||||||
|
},{urls: ["<all_urls>"], types: ["main_frame"]});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async _onClickedHandler(info, tab) {
|
async _onClickedHandler(info, tab) {
|
||||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||||
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
||||||
|
let remove;
|
||||||
if (userContextId) {
|
if (userContextId) {
|
||||||
// let actionName;
|
switch (info.menuItemId) {
|
||||||
let remove;
|
case this.MENU_ASSIGN_ID:
|
||||||
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
case this.MENU_REMOVE_ID:
|
||||||
remove = false;
|
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
||||||
} else {
|
remove = false;
|
||||||
remove = true;
|
} else {
|
||||||
|
remove = true;
|
||||||
|
}
|
||||||
|
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove);
|
||||||
|
break;
|
||||||
|
case this.MENU_MOVE_ID:
|
||||||
|
backgroundLogic.moveTabsToWindow({
|
||||||
|
cookieStoreId: tab.cookieStoreId,
|
||||||
|
windowId: tab.windowId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case this.MENU_HIDE_ID:
|
||||||
|
backgroundLogic.hideTabs({
|
||||||
|
cookieStoreId: tab.cookieStoreId,
|
||||||
|
windowId: tab.windowId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -260,6 +356,9 @@ const assignManager = {
|
|||||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
|
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
|
||||||
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
|
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
|
||||||
browser.contextMenus.remove(this.MENU_REMOVE_ID);
|
browser.contextMenus.remove(this.MENU_REMOVE_ID);
|
||||||
|
browser.contextMenus.remove(this.MENU_SEPARATOR_ID);
|
||||||
|
browser.contextMenus.remove(this.MENU_HIDE_ID);
|
||||||
|
browser.contextMenus.remove(this.MENU_MOVE_ID);
|
||||||
},
|
},
|
||||||
|
|
||||||
async calculateContextMenu(tab) {
|
async calculateContextMenu(tab) {
|
||||||
@@ -270,32 +369,57 @@ const assignManager = {
|
|||||||
if (siteSettings === false) {
|
if (siteSettings === false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418
|
let checked = false;
|
||||||
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;
|
let menuId = this.MENU_ASSIGN_ID;
|
||||||
const tabUserContextId = this.getUserContextIdFromCookieStore(tab);
|
const tabUserContextId = this.getUserContextIdFromCookieStore(tab);
|
||||||
if (siteSettings &&
|
if (siteSettings &&
|
||||||
Number(siteSettings.userContextId) === Number(tabUserContextId)) {
|
Number(siteSettings.userContextId) === Number(tabUserContextId)) {
|
||||||
prefix = "✓";
|
checked = true;
|
||||||
menuId = this.MENU_REMOVE_ID;
|
menuId = this.MENU_REMOVE_ID;
|
||||||
}
|
}
|
||||||
browser.contextMenus.create({
|
browser.contextMenus.create({
|
||||||
id: menuId,
|
id: menuId,
|
||||||
title: `${prefix} Always Open in This Container`,
|
title: "Always Open in This Container",
|
||||||
checked: true,
|
checked,
|
||||||
|
type: "checkbox",
|
||||||
|
contexts: ["all"],
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: this.MENU_SEPARATOR_ID,
|
||||||
|
type: "separator",
|
||||||
|
contexts: ["all"],
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: this.MENU_HIDE_ID,
|
||||||
|
title: "Hide This Container",
|
||||||
|
contexts: ["all"],
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: this.MENU_MOVE_ID,
|
||||||
|
title: "Move Tabs to a New Window",
|
||||||
contexts: ["all"],
|
contexts: ["all"],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
reloadPageInContainer(url, currentUserContextId, userContextId, index, neverAsk = false) {
|
encodeURLProperty(url) {
|
||||||
|
return encodeURIComponent(url).replace(/[!'()*]/g, (c) => {
|
||||||
|
const charCode = c.charCodeAt(0).toString(16);
|
||||||
|
return `%${charCode}`;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
reloadPageInContainer(url, currentUserContextId, userContextId, index, active, neverAsk = false, openerTabId = null) {
|
||||||
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
|
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
|
||||||
const loadPage = browser.extension.getURL("confirm-page.html");
|
const loadPage = browser.extension.getURL("confirm-page.html");
|
||||||
// False represents assignment is not permitted
|
// False represents assignment is not permitted
|
||||||
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
|
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
|
||||||
if (neverAsk) {
|
if (neverAsk) {
|
||||||
browser.tabs.create({url, cookieStoreId, index});
|
browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
|
||||||
} else {
|
} else {
|
||||||
let confirmUrl = `${loadPage}?url=${encodeURIComponent(url)}&cookieStoreId=${cookieStoreId}`;
|
let confirmUrl = `${loadPage}?url=${this.encodeURLProperty(url)}&cookieStoreId=${cookieStoreId}`;
|
||||||
let currentCookieStoreId;
|
let currentCookieStoreId;
|
||||||
if (currentUserContextId) {
|
if (currentUserContextId) {
|
||||||
currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId);
|
currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId);
|
||||||
@@ -304,7 +428,9 @@ const assignManager = {
|
|||||||
browser.tabs.create({
|
browser.tabs.create({
|
||||||
url: confirmUrl,
|
url: confirmUrl,
|
||||||
cookieStoreId: currentCookieStoreId,
|
cookieStoreId: currentCookieStoreId,
|
||||||
index
|
openerTabId,
|
||||||
|
index,
|
||||||
|
active
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// We don't want to sync this URL ever nor clutter the users history
|
// We don't want to sync this URL ever nor clutter the users history
|
||||||
browser.history.deleteUrl({url: confirmUrl});
|
browser.history.deleteUrl({url: confirmUrl});
|
||||||
@@ -25,13 +25,12 @@ const backgroundLogic = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteContainer(userContextId) {
|
async deleteContainer(userContextId, removed = false) {
|
||||||
await this._closeTabs(userContextId);
|
await this._closeTabs(userContextId);
|
||||||
await browser.contextualIdentities.remove(this.cookieStoreId(userContextId));
|
if (!removed) {
|
||||||
|
await browser.contextualIdentities.remove(this.cookieStoreId(userContextId));
|
||||||
|
}
|
||||||
assignManager.deleteContainer(userContextId);
|
assignManager.deleteContainer(userContextId);
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
method: "forgetIdentityAndRefresh"
|
|
||||||
});
|
|
||||||
return {done: true, userContextId};
|
return {done: true, userContextId};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ const backgroundLogic = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async openTab(options) {
|
async openNewTab(options) {
|
||||||
let url = options.url || undefined;
|
let url = options.url || undefined;
|
||||||
const userContextId = ("userContextId" in options) ? options.userContextId : 0;
|
const userContextId = ("userContextId" in options) ? options.userContextId : 0;
|
||||||
const active = ("nofocus" in options) ? options.nofocus : true;
|
const active = ("nofocus" in options) ? options.nofocus : true;
|
||||||
@@ -64,10 +63,10 @@ const backgroundLogic = {
|
|||||||
url = undefined;
|
url = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unhide all hidden tabs
|
if (!this.isPermissibleURL(url)) {
|
||||||
this.showTabs({
|
return;
|
||||||
cookieStoreId
|
}
|
||||||
});
|
|
||||||
return browser.tabs.create({
|
return browser.tabs.create({
|
||||||
url,
|
url,
|
||||||
active,
|
active,
|
||||||
@@ -76,61 +75,94 @@ const backgroundLogic = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async getTabs(options) {
|
isPermissibleURL(url) {
|
||||||
if (!("cookieStoreId" in options)) {
|
const protocol = new URL(url).protocol;
|
||||||
return new Error("getTabs must be called with cookieStoreId argument.");
|
// We can't open these we just have to throw them away
|
||||||
|
if (protocol === "about:"
|
||||||
|
|| protocol === "chrome:"
|
||||||
|
|| protocol === "moz-extension:") {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
checkArgs(requiredArguments, options, methodName) {
|
||||||
await identityState.remapTabsIfMissing(options.cookieStoreId);
|
requiredArguments.forEach((argument) => {
|
||||||
const isKnownContainer = await identityState._isKnownContainer(userContextId);
|
if (!(argument in options)) {
|
||||||
if (!isKnownContainer) {
|
return new Error(`${methodName} must be called with ${argument} argument.`);
|
||||||
return [];
|
}
|
||||||
}
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTabs(options) {
|
||||||
|
const requiredArguments = ["cookieStoreId", "windowId"];
|
||||||
|
this.checkArgs(requiredArguments, options, "getTabs");
|
||||||
|
const { cookieStoreId, windowId } = options;
|
||||||
|
|
||||||
const list = [];
|
const list = [];
|
||||||
const tabs = await this._containerTabs(options.cookieStoreId);
|
const tabs = await browser.tabs.query({
|
||||||
|
cookieStoreId,
|
||||||
|
windowId
|
||||||
|
});
|
||||||
tabs.forEach((tab) => {
|
tabs.forEach((tab) => {
|
||||||
list.push(identityState._createTabObject(tab));
|
list.push(identityState._createTabObject(tab));
|
||||||
});
|
});
|
||||||
|
|
||||||
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
const containerState = await identityState.storageArea.get(cookieStoreId);
|
||||||
return list.concat(containerState.hiddenTabs);
|
return list.concat(containerState.hiddenTabs);
|
||||||
},
|
},
|
||||||
|
|
||||||
async moveTabsToWindow(options) {
|
async moveTabsToWindow(options) {
|
||||||
if (!("cookieStoreId" in options)) {
|
const requiredArguments = ["cookieStoreId", "windowId"];
|
||||||
return new Error("moveTabsToWindow must be called with cookieStoreId argument.");
|
this.checkArgs(requiredArguments, options, "moveTabsToWindow");
|
||||||
}
|
const { cookieStoreId, windowId } = options;
|
||||||
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
const list = await browser.tabs.query({
|
||||||
await identityState.remapTabsIfMissing(options.cookieStoreId);
|
cookieStoreId,
|
||||||
if (!identityState._isKnownContainer(userContextId)) {
|
windowId
|
||||||
return null;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const list = await identityState._matchTabsByContainer(options.cookieStoreId);
|
const containerState = await identityState.storageArea.get(cookieStoreId);
|
||||||
|
|
||||||
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
if (list.length === 0 &&
|
if (list.length === 0 &&
|
||||||
containerState.hiddenTabs.length === 0) {
|
containerState.hiddenTabs.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const window = await browser.windows.create({
|
let newWindowObj;
|
||||||
tabId: list.shift().id
|
let hiddenDefaultTabToClose;
|
||||||
});
|
if (list.length) {
|
||||||
browser.tabs.move(list, {
|
newWindowObj = await browser.windows.create();
|
||||||
windowId: window.id,
|
|
||||||
index: -1
|
// Pin the default tab in the new window so existing pinned tabs can be moved after it.
|
||||||
});
|
// From the docs (https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/tabs/move):
|
||||||
|
// Note that you can't move pinned tabs to a position after any unpinned tabs in a window, or move any unpinned tabs to a position before any pinned tabs.
|
||||||
|
await browser.tabs.update(newWindowObj.tabs[0].id, { pinned: true });
|
||||||
|
|
||||||
|
browser.tabs.move(list.map((tab) => tab.id), {
|
||||||
|
windowId: newWindowObj.id,
|
||||||
|
index: -1
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//As we get a blank tab here we will need to await the tabs creation
|
||||||
|
newWindowObj = await browser.windows.create({
|
||||||
|
});
|
||||||
|
hiddenDefaultTabToClose = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showHiddenPromises = [];
|
||||||
|
|
||||||
// Let's show the hidden tabs.
|
// Let's show the hidden tabs.
|
||||||
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
||||||
browser.tabs.create(object.url || DEFAULT_TAB, {
|
showHiddenPromises.push(browser.tabs.create({
|
||||||
windowId: window.id,
|
url: object.url || DEFAULT_TAB,
|
||||||
cookieStoreId: options.cookieStoreId
|
windowId: newWindowObj.id,
|
||||||
});
|
cookieStoreId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hiddenDefaultTabToClose) {
|
||||||
|
// Lets wait for hidden tabs to show before closing the others
|
||||||
|
await showHiddenPromises;
|
||||||
}
|
}
|
||||||
|
|
||||||
containerState.hiddenTabs = [];
|
containerState.hiddenTabs = [];
|
||||||
@@ -138,31 +170,46 @@ const backgroundLogic = {
|
|||||||
// Let's close all the normal tab in the new window. In theory it
|
// Let's close all the normal tab in the new window. In theory it
|
||||||
// should be only the first tab, but maybe there are addons doing
|
// should be only the first tab, but maybe there are addons doing
|
||||||
// crazy stuff.
|
// crazy stuff.
|
||||||
const tabs = browser.tabs.query({windowId: window.id});
|
const tabs = await browser.tabs.query({windowId: newWindowObj.id});
|
||||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
for (let tab of tabs) { // eslint-disable-line prefer-const
|
||||||
if (tabs.cookieStoreId !== options.cookieStoreId) {
|
if (tab.cookieStoreId !== cookieStoreId) {
|
||||||
browser.tabs.remove(tab.id);
|
browser.tabs.remove(tab.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return await identityState.storageArea.set(options.cookieStoreId, containerState);
|
return await identityState.storageArea.set(cookieStoreId, containerState);
|
||||||
},
|
},
|
||||||
|
|
||||||
async _closeTabs(userContextId) {
|
async _closeTabs(userContextId, windowId = false) {
|
||||||
const cookieStoreId = this.cookieStoreId(userContextId);
|
const cookieStoreId = this.cookieStoreId(userContextId);
|
||||||
const tabs = await this._containerTabs(cookieStoreId);
|
let tabs;
|
||||||
|
/* if we have no windowId we are going to close all this container (used for deleting) */
|
||||||
|
if (windowId !== false) {
|
||||||
|
tabs = await browser.tabs.query({
|
||||||
|
cookieStoreId,
|
||||||
|
windowId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tabs = await browser.tabs.query({
|
||||||
|
cookieStoreId
|
||||||
|
});
|
||||||
|
}
|
||||||
const tabIds = tabs.map((tab) => tab.id);
|
const tabIds = tabs.map((tab) => tab.id);
|
||||||
return browser.tabs.remove(tabIds);
|
return browser.tabs.remove(tabIds);
|
||||||
},
|
},
|
||||||
|
|
||||||
async queryIdentitiesState() {
|
async queryIdentitiesState(windowId) {
|
||||||
const identities = await browser.contextualIdentities.query({});
|
const identities = await browser.contextualIdentities.query({});
|
||||||
const identitiesOutput = {};
|
const identitiesOutput = {};
|
||||||
const identitiesPromise = identities.map(async function (identity) {
|
const identitiesPromise = identities.map(async function (identity) {
|
||||||
await identityState.remapTabsIfMissing(identity.cookieStoreId);
|
const { cookieStoreId } = identity;
|
||||||
const containerState = await identityState.storageArea.get(identity.cookieStoreId);
|
const containerState = await identityState.storageArea.get(cookieStoreId);
|
||||||
identitiesOutput[identity.cookieStoreId] = {
|
const openTabs = await browser.tabs.query({
|
||||||
|
cookieStoreId,
|
||||||
|
windowId
|
||||||
|
});
|
||||||
|
identitiesOutput[cookieStoreId] = {
|
||||||
hasHiddenTabs: !!containerState.hiddenTabs.length,
|
hasHiddenTabs: !!containerState.hiddenTabs.length,
|
||||||
hasOpenTabs: !!containerState.openTabs
|
hasOpenTabs: !!openTabs.length
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
@@ -172,15 +219,15 @@ const backgroundLogic = {
|
|||||||
|
|
||||||
async sortTabs() {
|
async sortTabs() {
|
||||||
const windows = await browser.windows.getAll();
|
const windows = await browser.windows.getAll();
|
||||||
for (let window of windows) { // eslint-disable-line prefer-const
|
for (let windowObj of windows) { // eslint-disable-line prefer-const
|
||||||
// First the pinned tabs, then the normal ones.
|
// First the pinned tabs, then the normal ones.
|
||||||
await this._sortTabsInternal(window, true);
|
await this._sortTabsInternal(windowObj, true);
|
||||||
await this._sortTabsInternal(window, false);
|
await this._sortTabsInternal(windowObj, false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async _sortTabsInternal(window, pinnedTabs) {
|
async _sortTabsInternal(windowObj, pinnedTabs) {
|
||||||
const tabs = await browser.tabs.query({windowId: window.id});
|
const tabs = await browser.tabs.query({windowId: windowObj.id});
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
|
|
||||||
// Let's collect UCIs/tabs for this window.
|
// Let's collect UCIs/tabs for this window.
|
||||||
@@ -212,28 +259,22 @@ const backgroundLogic = {
|
|||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
++pos;
|
++pos;
|
||||||
browser.tabs.move(tab.id, {
|
browser.tabs.move(tab.id, {
|
||||||
windowId: window.id,
|
windowId: windowObj.id,
|
||||||
index: pos
|
index: pos
|
||||||
});
|
});
|
||||||
//xulWindow.gBrowser.moveTabTo(tab, pos++);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async hideTabs(options) {
|
async hideTabs(options) {
|
||||||
if (!("cookieStoreId" in options)) {
|
const requiredArguments = ["cookieStoreId", "windowId"];
|
||||||
return new Error("hideTabs must be called with cookieStoreId option.");
|
this.checkArgs(requiredArguments, options, "hideTabs");
|
||||||
}
|
const { cookieStoreId, windowId } = options;
|
||||||
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(cookieStoreId);
|
||||||
await identityState.remapTabsIfMissing(options.cookieStoreId);
|
|
||||||
const isKnownContainer = await identityState._isKnownContainer(userContextId);
|
|
||||||
if (!isKnownContainer) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const containerState = await identityState.storeHidden(options.cookieStoreId);
|
const containerState = await identityState.storeHidden(cookieStoreId, windowId);
|
||||||
await this._closeTabs(userContextId);
|
await this._closeTabs(userContextId, windowId);
|
||||||
return containerState;
|
return containerState;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -243,17 +284,12 @@ const backgroundLogic = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
||||||
await identityState.remapTabsIfMissing(options.cookieStoreId);
|
|
||||||
if (!identityState._isKnownContainer(userContextId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
||||||
|
|
||||||
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
||||||
promises.push(this.openTab({
|
promises.push(this.openNewTab({
|
||||||
userContextId: userContextId,
|
userContextId: userContextId,
|
||||||
url: object.url,
|
url: object.url,
|
||||||
nofocus: options.nofocus || false,
|
nofocus: options.nofocus || false,
|
||||||
@@ -269,12 +305,6 @@ const backgroundLogic = {
|
|||||||
|
|
||||||
cookieStoreId(userContextId) {
|
cookieStoreId(userContextId) {
|
||||||
return `firefox-container-${userContextId}`;
|
return `firefox-container-${userContextId}`;
|
||||||
},
|
}
|
||||||
|
|
||||||
_containerTabs(cookieStoreId) {
|
|
||||||
return browser.tabs.query({
|
|
||||||
cookieStoreId
|
|
||||||
}).catch((e) => {throw e;});
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
|
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
|
||||||
const badge = {
|
const badge = {
|
||||||
init() {
|
async init() {
|
||||||
this.displayBrowserActionBadge();
|
const currentWindow = await browser.windows.getCurrent();
|
||||||
|
this.displayBrowserActionBadge(currentWindow.incognito);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
disableAddon(tabId) {
|
||||||
|
browser.browserAction.disable(tabId);
|
||||||
|
browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" });
|
||||||
|
},
|
||||||
|
|
||||||
async displayBrowserActionBadge() {
|
async displayBrowserActionBadge() {
|
||||||
const extensionInfo = await backgroundLogic.getExtensionInfo();
|
const extensionInfo = await backgroundLogic.getExtensionInfo();
|
||||||
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
const identityState = {
|
||||||
|
storageArea: {
|
||||||
|
area: browser.storage.local,
|
||||||
|
|
||||||
|
getContainerStoreKey(cookieStoreId) {
|
||||||
|
const storagePrefix = "identitiesState@@_";
|
||||||
|
return `${storagePrefix}${cookieStoreId}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
async get(cookieStoreId) {
|
||||||
|
const storeKey = this.getContainerStoreKey(cookieStoreId);
|
||||||
|
const storageResponse = await this.area.get([storeKey]);
|
||||||
|
if (storageResponse && storeKey in storageResponse) {
|
||||||
|
return storageResponse[storeKey];
|
||||||
|
}
|
||||||
|
const defaultContainerState = identityState._createIdentityState();
|
||||||
|
await this.set(cookieStoreId, defaultContainerState);
|
||||||
|
|
||||||
|
return defaultContainerState;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(cookieStoreId, data) {
|
||||||
|
const storeKey = this.getContainerStoreKey(cookieStoreId);
|
||||||
|
return this.area.set({
|
||||||
|
[storeKey]: data
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(cookieStoreId) {
|
||||||
|
const storeKey = this.getContainerStoreKey(cookieStoreId);
|
||||||
|
return this.area.remove([storeKey]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_createTabObject(tab) {
|
||||||
|
return Object.assign({}, tab);
|
||||||
|
},
|
||||||
|
|
||||||
|
async storeHidden(cookieStoreId, windowId) {
|
||||||
|
const containerState = await this.storageArea.get(cookieStoreId);
|
||||||
|
const tabsByContainer = await browser.tabs.query({cookieStoreId, windowId});
|
||||||
|
tabsByContainer.forEach((tab) => {
|
||||||
|
const tabObject = this._createTabObject(tab);
|
||||||
|
if (!backgroundLogic.isPermissibleURL(tab.url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// This tab is going to be closed. Let's mark this tabObject as
|
||||||
|
// non-active.
|
||||||
|
tabObject.active = false;
|
||||||
|
tabObject.hiddenState = true;
|
||||||
|
containerState.hiddenTabs.push(tabObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.storageArea.set(cookieStoreId, containerState);
|
||||||
|
},
|
||||||
|
|
||||||
|
_createIdentityState() {
|
||||||
|
return {
|
||||||
|
hiddenTabs: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -11,8 +11,6 @@
|
|||||||
"js/background/badge.js",
|
"js/background/badge.js",
|
||||||
"js/background/identityState.js",
|
"js/background/identityState.js",
|
||||||
"js/background/messageHandler.js",
|
"js/background/messageHandler.js",
|
||||||
"js/background/tabPageCounter.js",
|
|
||||||
"js/backdround/init.js"
|
|
||||||
]
|
]
|
||||||
-->
|
-->
|
||||||
<script type="text/javascript" src="backgroundLogic.js"></script>
|
<script type="text/javascript" src="backgroundLogic.js"></script>
|
||||||
@@ -20,7 +18,5 @@
|
|||||||
<script type="text/javascript" src="badge.js"></script>
|
<script type="text/javascript" src="badge.js"></script>
|
||||||
<script type="text/javascript" src="identityState.js"></script>
|
<script type="text/javascript" src="identityState.js"></script>
|
||||||
<script type="text/javascript" src="messageHandler.js"></script>
|
<script type="text/javascript" src="messageHandler.js"></script>
|
||||||
<script type="text/javascript" src="tabPageCounter.js"></script>
|
|
||||||
<script type="text/javascript" src="init.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
const messageHandler = {
|
||||||
|
// After the timer completes we assume it's a tab the user meant to keep open
|
||||||
|
// We use this to catch redirected tabs that have just opened
|
||||||
|
// If this were in platform we would change how the tab opens based on "new tab" link navigations such as ctrl+click
|
||||||
|
LAST_CREATED_TAB_TIMER: 2000,
|
||||||
|
unhideQueue: [],
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Handles messages from webextension code
|
||||||
|
browser.runtime.onMessage.addListener((m) => {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
switch (m.method) {
|
||||||
|
case "deleteContainer":
|
||||||
|
response = backgroundLogic.deleteContainer(m.message.userContextId);
|
||||||
|
break;
|
||||||
|
case "createOrUpdateContainer":
|
||||||
|
response = backgroundLogic.createOrUpdateContainer(m.message);
|
||||||
|
break;
|
||||||
|
case "neverAsk":
|
||||||
|
assignManager._neverAsk(m);
|
||||||
|
break;
|
||||||
|
case "getAssignment":
|
||||||
|
response = browser.tabs.get(m.tabId).then((tab) => {
|
||||||
|
return assignManager._getAssignment(tab);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "getAssignmentObjectByContainer":
|
||||||
|
response = assignManager._getByContainer(m.message.userContextId);
|
||||||
|
break;
|
||||||
|
case "setOrRemoveAssignment":
|
||||||
|
// m.tabId is used for where to place the in content message
|
||||||
|
// m.url is the assignment to be removed/added
|
||||||
|
response = browser.tabs.get(m.tabId).then((tab) => {
|
||||||
|
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "sortTabs":
|
||||||
|
backgroundLogic.sortTabs();
|
||||||
|
break;
|
||||||
|
case "showTabs":
|
||||||
|
this.unhideContainer(m.cookieStoreId);
|
||||||
|
break;
|
||||||
|
case "hideTabs":
|
||||||
|
backgroundLogic.hideTabs({
|
||||||
|
cookieStoreId: m.cookieStoreId,
|
||||||
|
windowId: m.windowId
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "checkIncompatibleAddons":
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
|
case "moveTabsToWindow":
|
||||||
|
response = backgroundLogic.moveTabsToWindow({
|
||||||
|
cookieStoreId: m.cookieStoreId,
|
||||||
|
windowId: m.windowId
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "getTabs":
|
||||||
|
response = backgroundLogic.getTabs({
|
||||||
|
cookieStoreId: m.cookieStoreId,
|
||||||
|
windowId: m.windowId
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "queryIdentitiesState":
|
||||||
|
response = backgroundLogic.queryIdentitiesState(m.message.windowId);
|
||||||
|
break;
|
||||||
|
case "exemptContainerAssignment":
|
||||||
|
response = assignManager._exemptTab(m);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handles external messages from webextensions
|
||||||
|
const externalExtensionAllowed = {};
|
||||||
|
browser.runtime.onMessageExternal.addListener(async (message, sender) => {
|
||||||
|
if (!externalExtensionAllowed[sender.id]) {
|
||||||
|
const extensionInfo = await browser.management.get(sender.id);
|
||||||
|
if (!extensionInfo.permissions.includes("contextualIdentities")) {
|
||||||
|
throw new Error("Missing contextualIdentities permission");
|
||||||
|
}
|
||||||
|
externalExtensionAllowed[sender.id] = true;
|
||||||
|
}
|
||||||
|
let response;
|
||||||
|
switch (message.method) {
|
||||||
|
case "getAssignment":
|
||||||
|
if (typeof message.url === "undefined") {
|
||||||
|
throw new Error("Missing message.url");
|
||||||
|
}
|
||||||
|
response = assignManager.storageArea.get(message.url);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Unknown message.method");
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
// Delete externalExtensionAllowed if add-on installs/updates; permissions might change
|
||||||
|
browser.management.onInstalled.addListener(extensionInfo => {
|
||||||
|
if (externalExtensionAllowed[extensionInfo.id]) {
|
||||||
|
delete externalExtensionAllowed[extensionInfo.id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Delete externalExtensionAllowed if add-on uninstalls; not needed anymore
|
||||||
|
browser.management.onUninstalled.addListener(extensionInfo => {
|
||||||
|
if (externalExtensionAllowed[extensionInfo.id]) {
|
||||||
|
delete externalExtensionAllowed[extensionInfo.id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (browser.contextualIdentities.onRemoved) {
|
||||||
|
browser.contextualIdentities.onRemoved.addListener(({contextualIdentity}) => {
|
||||||
|
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(contextualIdentity.cookieStoreId);
|
||||||
|
backgroundLogic.deleteContainer(userContextId, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.tabs.onActivated.addListener((info) => {
|
||||||
|
assignManager.removeContextMenu();
|
||||||
|
browser.tabs.get(info.tabId).then((tab) => {
|
||||||
|
assignManager.calculateContextMenu(tab);
|
||||||
|
}).catch((e) => {
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.windows.onFocusChanged.addListener((windowId) => {
|
||||||
|
this.onFocusChangedCallback(windowId);
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.webRequest.onCompleted.addListener((details) => {
|
||||||
|
if (details.frameId !== 0 || details.tabId === -1) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
assignManager.removeContextMenu();
|
||||||
|
|
||||||
|
browser.tabs.get(details.tabId).then((tab) => {
|
||||||
|
assignManager.calculateContextMenu(tab);
|
||||||
|
}).catch((e) => {
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}, {urls: ["<all_urls>"], types: ["main_frame"]});
|
||||||
|
|
||||||
|
browser.tabs.onCreated.addListener((tab) => {
|
||||||
|
if (tab.incognito) {
|
||||||
|
badge.disableAddon(tab.id);
|
||||||
|
}
|
||||||
|
// lets remember the last tab created so we can close it if it looks like a redirect
|
||||||
|
this.lastCreatedTab = tab;
|
||||||
|
if (tab.cookieStoreId) {
|
||||||
|
// Don't count firefox-default, firefox-private, nor our own confirm page loads
|
||||||
|
if (tab.cookieStoreId !== "firefox-default" &&
|
||||||
|
tab.cookieStoreId !== "firefox-private" &&
|
||||||
|
!tab.url.startsWith("moz-extension")) {
|
||||||
|
// increment the counter of container tabs opened
|
||||||
|
this.incrementCountOfContainerTabsOpened();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unhideContainer(tab.cookieStoreId);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.lastCreatedTab = null;
|
||||||
|
}, this.LAST_CREATED_TAB_TIMER);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async incrementCountOfContainerTabsOpened() {
|
||||||
|
const key = "containerTabsOpened";
|
||||||
|
const count = await browser.storage.local.get({[key]: 0});
|
||||||
|
const countOfContainerTabsOpened = ++count[key];
|
||||||
|
browser.storage.local.set({[key]: countOfContainerTabsOpened});
|
||||||
|
|
||||||
|
// When the user opens their _ tab, give them the achievement
|
||||||
|
if (countOfContainerTabsOpened === 100) {
|
||||||
|
const storage = await browser.storage.local.get({achievements: []});
|
||||||
|
storage.achievements.push({"name": "manyContainersOpened", "done": false});
|
||||||
|
// use set and spread to create a unique array
|
||||||
|
const achievements = [...new Set(storage.achievements)];
|
||||||
|
browser.storage.local.set({achievements});
|
||||||
|
browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"});
|
||||||
|
browser.browserAction.setBadgeText({text: "NEW"});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async unhideContainer(cookieStoreId) {
|
||||||
|
if (!this.unhideQueue.includes(cookieStoreId)) {
|
||||||
|
this.unhideQueue.push(cookieStoreId);
|
||||||
|
// Unhide all hidden tabs
|
||||||
|
await backgroundLogic.showTabs({
|
||||||
|
cookieStoreId
|
||||||
|
});
|
||||||
|
this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onFocusChangedCallback(windowId) {
|
||||||
|
assignManager.removeContextMenu();
|
||||||
|
// browserAction loses background color in new windows ...
|
||||||
|
// https://bugzil.la/1314674
|
||||||
|
// https://github.com/mozilla/testpilot-containers/issues/608
|
||||||
|
// ... so re-call displayBrowserActionBadge on window changes
|
||||||
|
badge.displayBrowserActionBadge();
|
||||||
|
browser.tabs.query({active: true, windowId}).then((tabs) => {
|
||||||
|
if (tabs && tabs[0]) {
|
||||||
|
assignManager.calculateContextMenu(tabs[0]);
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lets do this last as theme manager did a check before connecting before
|
||||||
|
messageHandler.init();
|
||||||
@@ -20,11 +20,15 @@ async function doAnimation(element, property, value) {
|
|||||||
async function addMessage(message) {
|
async function addMessage(message) {
|
||||||
const divElement = document.createElement("div");
|
const divElement = document.createElement("div");
|
||||||
divElement.classList.add("container-notification");
|
divElement.classList.add("container-notification");
|
||||||
// For the eager eyed, this is an experiment. It is however likely that a website will know it is "contained" anyway
|
// Ideally we would use https://bugzilla.mozilla.org/show_bug.cgi?id=1340930 when this is available
|
||||||
divElement.innerText = message.text;
|
divElement.innerText = message.text;
|
||||||
|
|
||||||
const imageElement = document.createElement("img");
|
const imageElement = document.createElement("img");
|
||||||
imageElement.src = browser.extension.getURL("/img/container-site-d-24.png");
|
const imagePath = browser.extension.getURL("/img/container-site-d-24.png");
|
||||||
|
const response = await fetch(imagePath);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
|
imageElement.src = objectUrl;
|
||||||
divElement.prepend(imageElement);
|
divElement.prepend(imageElement);
|
||||||
|
|
||||||
document.body.appendChild(divElement);
|
document.body.appendChild(divElement);
|
||||||
@@ -22,6 +22,7 @@ const P_CONTAINERS_EDIT = "containersEdit";
|
|||||||
const P_CONTAINER_INFO = "containerInfo";
|
const P_CONTAINER_INFO = "containerInfo";
|
||||||
const P_CONTAINER_EDIT = "containerEdit";
|
const P_CONTAINER_EDIT = "containerEdit";
|
||||||
const P_CONTAINER_DELETE = "containerDelete";
|
const P_CONTAINER_DELETE = "containerDelete";
|
||||||
|
const P_CONTAINERS_ACHIEVEMENT = "containersAchievement";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escapes any occurances of &, ", <, > or / with XML entities.
|
* Escapes any occurances of &, ", <, > or / with XML entities.
|
||||||
@@ -90,27 +91,16 @@ const Logic = {
|
|||||||
|
|
||||||
// Routing to the correct panel.
|
// Routing to the correct panel.
|
||||||
// If localStorage is disabled, we don't show the onboarding.
|
// If localStorage is disabled, we don't show the onboarding.
|
||||||
const data = await browser.storage.local.get([ONBOARDING_STORAGE_KEY]);
|
const onboardingData = await browser.storage.local.get([ONBOARDING_STORAGE_KEY]);
|
||||||
let onboarded = data[ONBOARDING_STORAGE_KEY];
|
let onboarded = onboardingData[ONBOARDING_STORAGE_KEY];
|
||||||
if (!onboarded) {
|
if (!onboarded) {
|
||||||
// Legacy local storage used before panel 5
|
onboarded = 0;
|
||||||
if (localStorage.getItem("onboarded4")) {
|
|
||||||
onboarded = 4;
|
|
||||||
} else if (localStorage.getItem("onboarded3")) {
|
|
||||||
onboarded = 3;
|
|
||||||
} else if (localStorage.getItem("onboarded2")) {
|
|
||||||
onboarded = 2;
|
|
||||||
} else if (localStorage.getItem("onboarded1")) {
|
|
||||||
onboarded = 1;
|
|
||||||
} else {
|
|
||||||
onboarded = 0;
|
|
||||||
}
|
|
||||||
this.setOnboardingStage(onboarded);
|
this.setOnboardingStage(onboarded);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (onboarded) {
|
switch (onboarded) {
|
||||||
case 5:
|
case 5:
|
||||||
this.showPanel(P_CONTAINERS_LIST);
|
this.showAchievementOrContainersListPanel();
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
this.showPanel(P_ONBOARDING_5);
|
this.showPanel(P_ONBOARDING_5);
|
||||||
@@ -132,6 +122,37 @@ const Logic = {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async showAchievementOrContainersListPanel() {
|
||||||
|
// Do we need to show an achievement panel?
|
||||||
|
let showAchievements = false;
|
||||||
|
const achievementsStorage = await browser.storage.local.get({achievements: []});
|
||||||
|
for (const achievement of achievementsStorage.achievements) {
|
||||||
|
if (!achievement.done) {
|
||||||
|
showAchievements = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showAchievements) {
|
||||||
|
this.showPanel(P_CONTAINERS_ACHIEVEMENT);
|
||||||
|
} else {
|
||||||
|
this.showPanel(P_CONTAINERS_LIST);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// In case the user wants to click multiple actions,
|
||||||
|
// they have to click the "Done" button to stop the panel
|
||||||
|
// from showing
|
||||||
|
async setAchievementDone(achievementName) {
|
||||||
|
const achievementsStorage = await browser.storage.local.get({achievements: []});
|
||||||
|
const achievements = achievementsStorage.achievements;
|
||||||
|
achievements.forEach((achievement, index, achievementsArray) => {
|
||||||
|
if (achievement.name === achievementName) {
|
||||||
|
achievement.done = true;
|
||||||
|
achievementsArray[index] = achievement;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
browser.storage.local.set({achievements});
|
||||||
|
},
|
||||||
|
|
||||||
setOnboardingStage(stage) {
|
setOnboardingStage(stage) {
|
||||||
return browser.storage.local.set({
|
return browser.storage.local.set({
|
||||||
[ONBOARDING_STORAGE_KEY]: stage
|
[ONBOARDING_STORAGE_KEY]: stage
|
||||||
@@ -141,10 +162,14 @@ const Logic = {
|
|||||||
async clearBrowserActionBadge() {
|
async clearBrowserActionBadge() {
|
||||||
const extensionInfo = await getExtensionInfo();
|
const extensionInfo = await getExtensionInfo();
|
||||||
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
||||||
browser.browserAction.setBadgeBackgroundColor({color: ""});
|
browser.browserAction.setBadgeBackgroundColor({color: null});
|
||||||
browser.browserAction.setBadgeText({text: ""});
|
browser.browserAction.setBadgeText({text: ""});
|
||||||
storage.browserActionBadgesClicked.push(extensionInfo.version);
|
storage.browserActionBadgesClicked.push(extensionInfo.version);
|
||||||
browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked});
|
// use set and spread to create a unique array
|
||||||
|
const browserActionBadgesClicked = [...new Set(storage.browserActionBadgesClicked)];
|
||||||
|
browser.storage.local.set({
|
||||||
|
browserActionBadgesClicked
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async identity(cookieStoreId) {
|
async identity(cookieStoreId) {
|
||||||
@@ -168,6 +193,7 @@ const Logic = {
|
|||||||
});
|
});
|
||||||
element.addEventListener("keydown", (e) => {
|
element.addEventListener("keydown", (e) => {
|
||||||
if (e.keyCode === 13) {
|
if (e.keyCode === 13) {
|
||||||
|
e.preventDefault();
|
||||||
handler(e);
|
handler(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -186,11 +212,35 @@ const Logic = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async numTabs() {
|
||||||
|
const activeTabs = await browser.tabs.query({windowId: browser.windows.WINDOW_ID_CURRENT});
|
||||||
|
return activeTabs.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
_disableMoveTabs(message) {
|
||||||
|
const moveTabsEl = document.querySelector("#container-info-movetabs");
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const incompatEl = document.createElement("div");
|
||||||
|
|
||||||
|
moveTabsEl.classList.remove("clickable");
|
||||||
|
moveTabsEl.setAttribute("title", message);
|
||||||
|
|
||||||
|
fragment.appendChild(incompatEl);
|
||||||
|
incompatEl.setAttribute("id", "container-info-movetabs-incompat");
|
||||||
|
incompatEl.textContent = message;
|
||||||
|
incompatEl.classList.add("container-info-tab-row");
|
||||||
|
|
||||||
|
moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling);
|
||||||
|
},
|
||||||
|
|
||||||
async refreshIdentities() {
|
async refreshIdentities() {
|
||||||
const [identities, state] = await Promise.all([
|
const [identities, state] = await Promise.all([
|
||||||
browser.contextualIdentities.query({}),
|
browser.contextualIdentities.query({}),
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: "queryIdentitiesState"
|
method: "queryIdentitiesState",
|
||||||
|
message: {
|
||||||
|
windowId: browser.windows.WINDOW_ID_CURRENT
|
||||||
|
}
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
this._identities = identities.map((identity) => {
|
this._identities = identities.map((identity) => {
|
||||||
@@ -451,13 +501,15 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||||||
panelSelector: "#container-panel",
|
panelSelector: "#container-panel",
|
||||||
|
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
async initialize() {
|
||||||
Logic.addEnterHandler(document.querySelector("#container-add-link"), () => {
|
Logic.addEnterHandler(document.querySelector("#container-add-link"), () => {
|
||||||
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
|
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
|
||||||
});
|
});
|
||||||
|
|
||||||
Logic.addEnterHandler(document.querySelector("#edit-containers-link"), () => {
|
Logic.addEnterHandler(document.querySelector("#edit-containers-link"), (e) => {
|
||||||
Logic.showPanel(P_CONTAINERS_EDIT);
|
if (!e.target.classList.contains("disable-edit-containers")){
|
||||||
|
Logic.showPanel(P_CONTAINERS_EDIT);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Logic.addEnterHandler(document.querySelector("#sort-containers-link"), async function () {
|
Logic.addEnterHandler(document.querySelector("#sort-containers-link"), async function () {
|
||||||
@@ -494,6 +546,15 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||||||
case 38:
|
case 38:
|
||||||
previous();
|
previous();
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
if ((e.keyCode >= 49 && e.keyCode <= 57) &&
|
||||||
|
Logic._currentPanel === "containersList") {
|
||||||
|
const element = selectables[e.keyCode - 48];
|
||||||
|
if (element) {
|
||||||
|
element.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -599,12 +660,8 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||||||
|| e.target.parentNode.matches(".open-newtab")
|
|| e.target.parentNode.matches(".open-newtab")
|
||||||
|| e.type === "keydown") {
|
|| e.type === "keydown") {
|
||||||
try {
|
try {
|
||||||
await browser.runtime.sendMessage({
|
browser.tabs.create({
|
||||||
method: "openTab",
|
cookieStoreId: identity.cookieStoreId
|
||||||
message: {
|
|
||||||
userContextId: Logic.userContextId(identity.cookieStoreId),
|
|
||||||
source: "pop-up"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
window.close();
|
window.close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -632,6 +689,13 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||||||
document.addEventListener("mousedown", () => {
|
document.addEventListener("mousedown", () => {
|
||||||
document.removeEventListener("focus", focusHandler);
|
document.removeEventListener("focus", focusHandler);
|
||||||
});
|
});
|
||||||
|
/* If no container is present disable the Edit Containers button */
|
||||||
|
const editContainer = document.querySelector("#edit-containers-link");
|
||||||
|
if (Logic.identities().length === 0) {
|
||||||
|
editContainer.classList.add("disable-edit-containers");
|
||||||
|
} else {
|
||||||
|
editContainer.classList.remove("disable-edit-containers");
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
@@ -654,6 +718,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
|||||||
try {
|
try {
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
||||||
|
windowId: browser.windows.WINDOW_ID_CURRENT,
|
||||||
cookieStoreId: Logic.currentCookieStoreId()
|
cookieStoreId: Logic.currentCookieStoreId()
|
||||||
});
|
});
|
||||||
window.close();
|
window.close();
|
||||||
@@ -663,36 +728,31 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check if the user has incompatible add-ons installed
|
// Check if the user has incompatible add-ons installed
|
||||||
|
let incompatible = false;
|
||||||
try {
|
try {
|
||||||
const incompatible = await browser.runtime.sendMessage({
|
incompatible = await browser.runtime.sendMessage({
|
||||||
method: "checkIncompatibleAddons"
|
method: "checkIncompatibleAddons"
|
||||||
});
|
});
|
||||||
const moveTabsEl = document.querySelector("#container-info-movetabs");
|
|
||||||
if (incompatible) {
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
const incompatEl = document.createElement("div");
|
|
||||||
|
|
||||||
moveTabsEl.classList.remove("clickable");
|
|
||||||
moveTabsEl.setAttribute("title", "Moving container tabs is incompatible with Pulse, PageShot, and SnoozeTabs.");
|
|
||||||
|
|
||||||
fragment.appendChild(incompatEl);
|
|
||||||
incompatEl.setAttribute("id", "container-info-movetabs-incompat");
|
|
||||||
incompatEl.textContent = "Incompatible with other Experiments.";
|
|
||||||
incompatEl.classList.add("container-info-tab-row");
|
|
||||||
|
|
||||||
moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling);
|
|
||||||
} else {
|
|
||||||
Logic.addEnterHandler(moveTabsEl, async function () {
|
|
||||||
await browser.runtime.sendMessage({
|
|
||||||
method: "moveTabsToWindow",
|
|
||||||
cookieStoreId: Logic.currentIdentity().cookieStoreId,
|
|
||||||
});
|
|
||||||
window.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error("Could not check for incompatible add-ons.");
|
throw new Error("Could not check for incompatible add-ons.");
|
||||||
}
|
}
|
||||||
|
const moveTabsEl = document.querySelector("#container-info-movetabs");
|
||||||
|
const numTabs = await Logic.numTabs();
|
||||||
|
if (incompatible) {
|
||||||
|
Logic._disableMoveTabs("Moving container tabs is incompatible with Pulse, PageShot, and SnoozeTabs.");
|
||||||
|
return;
|
||||||
|
} else if (numTabs === 1) {
|
||||||
|
Logic._disableMoveTabs("Cannot move a tab from a single-tab window.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Logic.addEnterHandler(moveTabsEl, async function () {
|
||||||
|
await browser.runtime.sendMessage({
|
||||||
|
method: "moveTabsToWindow",
|
||||||
|
windowId: browser.windows.WINDOW_ID_CURRENT,
|
||||||
|
cookieStoreId: Logic.currentIdentity().cookieStoreId,
|
||||||
|
});
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// This method is called when the panel is shown.
|
// This method is called when the panel is shown.
|
||||||
@@ -726,6 +786,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
|||||||
// Let's retrieve the list of tabs.
|
// Let's retrieve the list of tabs.
|
||||||
const tabs = await browser.runtime.sendMessage({
|
const tabs = await browser.runtime.sendMessage({
|
||||||
method: "getTabs",
|
method: "getTabs",
|
||||||
|
windowId: browser.windows.WINDOW_ID_CURRENT,
|
||||||
cookieStoreId: Logic.currentIdentity().cookieStoreId
|
cookieStoreId: Logic.currentIdentity().cookieStoreId
|
||||||
});
|
});
|
||||||
return this.buildInfoTable(tabs);
|
return this.buildInfoTable(tabs);
|
||||||
@@ -800,7 +861,7 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
|
|||||||
</td>`;
|
</td>`;
|
||||||
tr.querySelector(".container-name").textContent = identity.name;
|
tr.querySelector(".container-name").textContent = identity.name;
|
||||||
tr.querySelector(".edit-container").setAttribute("title", `Edit ${identity.name} container`);
|
tr.querySelector(".edit-container").setAttribute("title", `Edit ${identity.name} container`);
|
||||||
tr.querySelector(".remove-container").setAttribute("title", `Delete ${identity.name} container`);
|
tr.querySelector(".remove-container").setAttribute("title", `Remove ${identity.name} container`);
|
||||||
|
|
||||||
|
|
||||||
Logic.addEnterHandler(tr, e => {
|
Logic.addEnterHandler(tr, e => {
|
||||||
@@ -964,6 +1025,11 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
|||||||
|
|
||||||
document.querySelector("#edit-container-panel-name-input").value = identity.name || "";
|
document.querySelector("#edit-container-panel-name-input").value = identity.name || "";
|
||||||
document.querySelector("#edit-container-panel-usercontext-input").value = userContextId || NEW_CONTAINER_ID;
|
document.querySelector("#edit-container-panel-usercontext-input").value = userContextId || NEW_CONTAINER_ID;
|
||||||
|
const containerName = document.querySelector("#edit-container-panel-name-input");
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
containerName.select();
|
||||||
|
containerName.focus();
|
||||||
|
});
|
||||||
[...document.querySelectorAll("[name='container-color']")].forEach(colorInput => {
|
[...document.querySelectorAll("[name='container-color']")].forEach(colorInput => {
|
||||||
colorInput.checked = colorInput.value === identity.color;
|
colorInput.checked = colorInput.value === identity.color;
|
||||||
});
|
});
|
||||||
@@ -1019,4 +1085,25 @@ Logic.registerPanel(P_CONTAINER_DELETE, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// P_CONTAINERS_ACHIEVEMENT: Page for achievement.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Logic.registerPanel(P_CONTAINERS_ACHIEVEMENT, {
|
||||||
|
panelSelector: ".achievement-panel",
|
||||||
|
|
||||||
|
// This method is called when the object is registered.
|
||||||
|
initialize() {
|
||||||
|
// Set done and move to the containers list panel.
|
||||||
|
Logic.addEnterHandler(document.querySelector("#achievement-done-button"), async function () {
|
||||||
|
await Logic.setAchievementDone("manyContainersOpened");
|
||||||
|
Logic.showPanel(P_CONTAINERS_LIST);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// This method is called when the panel is shown.
|
||||||
|
prepare() {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
Logic.init();
|
Logic.init();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const DEFAULT_FAVICON = "moz-icon://goat?size=16";
|
const DEFAULT_FAVICON = "/img/blank-favicon.svg";
|
||||||
|
|
||||||
// TODO use export here instead of globals
|
// TODO use export here instead of globals
|
||||||
window.Utils = {
|
window.Utils = {
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Containers Experiment",
|
"name": "Firefox Multi-Account Containers",
|
||||||
"version": "3.1.0",
|
"version": "6.0.1",
|
||||||
|
|
||||||
"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.",
|
"description": "Multi-Account Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
|
||||||
"icons": {
|
"icons": {
|
||||||
"48": "img/container-site-d-48.png",
|
"48": "img/container-site-d-48.png",
|
||||||
"96": "img/container-site-d-96.png"
|
"96": "img/container-site-d-96.png"
|
||||||
@@ -11,12 +11,12 @@
|
|||||||
|
|
||||||
"applications": {
|
"applications": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
"strict_min_version": "51.0",
|
"id": "@testpilot-containers",
|
||||||
"update_url": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"
|
"strict_min_version": "57.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"homepage_url": "https://testpilot.firefox.com/",
|
"homepage_url": "https://github.com/mozilla/multi-account-containers#readme",
|
||||||
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"<all_urls>",
|
"<all_urls>",
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"contextualIdentities",
|
"contextualIdentities",
|
||||||
"history",
|
"history",
|
||||||
"idle",
|
"idle",
|
||||||
|
"management",
|
||||||
"storage",
|
"storage",
|
||||||
"tabs",
|
"tabs",
|
||||||
"webRequestBlocking",
|
"webRequestBlocking",
|
||||||
@@ -45,7 +46,7 @@
|
|||||||
"browser_action": {
|
"browser_action": {
|
||||||
"browser_style": true,
|
"browser_style": true,
|
||||||
"default_icon": "img/container-site.svg",
|
"default_icon": "img/container-site.svg",
|
||||||
"default_title": "Containers",
|
"default_title": "Multi-Account Containers",
|
||||||
"default_popup": "popup.html"
|
"default_popup": "popup.html"
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
<title>Containers browserAction Popup</title>
|
<title>Multi-Account Containers</title>
|
||||||
<link rel="stylesheet" href="/css/popup.css">
|
<link rel="stylesheet" href="css/popup.css">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -67,6 +67,34 @@
|
|||||||
<a href="#" id="onboarding-longpress-button" class="onboarding-button">Done</a>
|
<a href="#" id="onboarding-longpress-button" class="onboarding-button">Done</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="panel achievement-panel hide" id="achievement-panel">
|
||||||
|
<img class="onboarding-img" alt="You achieved a Containers milestone!" src="/img/onboarding-3.png" />
|
||||||
|
<h3 class="onboarding-title">100 tabs!</h3>
|
||||||
|
<p>You've opened 100 Container tabs.</p>
|
||||||
|
<p>If you enjoy Containers, help us spread the word!</p>
|
||||||
|
<p class="share-ctas">
|
||||||
|
<a class="cta-link" href="https://mzl.la/2gJtIZ4" id="achievement-rate-button" target="_blank">
|
||||||
|
<span class="cta amo-rate-cta">
|
||||||
|
<img src="/img/amo-icon.svg" class="cta-icon" alt="addons.mozilla.org Icon">
|
||||||
|
Rate
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a class="cta-link" href="https://bit.ly/fb-share-mac-addon" target="_blank">
|
||||||
|
<span class="cta fb-share-cta">
|
||||||
|
<img src="/img/webicon-facebook.svg" class="cta-icon" alt="Facebook Icon">
|
||||||
|
Share
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a class="cta-link" href="http://bit.ly/tweet-100-tabs-mac-addon" target="_blank">
|
||||||
|
<span class="cta tweet-cta">
|
||||||
|
<img src="/img/webicon-twitter.svg" class="cta-icon" alt="Twitter Icon">
|
||||||
|
Tweet
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<a href="#" id="achievement-done-button" class="onboarding-button">Done</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel container-panel hide" id="container-panel">
|
<div class="panel container-panel hide" id="container-panel">
|
||||||
<div id="current-tab">
|
<div id="current-tab">
|
||||||
<h3>Current Tab</h3>
|
<h3>Current Tab</h3>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
"node": true,
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
"sinon": false,
|
||||||
|
"expect": false,
|
||||||
|
"nextTick": false,
|
||||||
|
"buildBackgroundDom": false,
|
||||||
|
"background": false,
|
||||||
|
"buildPopupDom": false,
|
||||||
|
"popup": false,
|
||||||
|
"helper": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
module.exports = () => {
|
||||||
|
const _storage = {};
|
||||||
|
|
||||||
|
// could maybe be replaced by https://github.com/acvetkov/sinon-chrome
|
||||||
|
const browserMock = {
|
||||||
|
_storage,
|
||||||
|
runtime: {
|
||||||
|
onMessage: {
|
||||||
|
addListener: sinon.stub(),
|
||||||
|
},
|
||||||
|
onMessageExternal: {
|
||||||
|
addListener: sinon.stub(),
|
||||||
|
},
|
||||||
|
sendMessage: sinon.stub().resolves(),
|
||||||
|
},
|
||||||
|
webRequest: {
|
||||||
|
onBeforeRequest: {
|
||||||
|
addListener: sinon.stub()
|
||||||
|
},
|
||||||
|
onCompleted: {
|
||||||
|
addListener: sinon.stub()
|
||||||
|
},
|
||||||
|
onErrorOccurred: {
|
||||||
|
addListener: sinon.stub()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
windows: {
|
||||||
|
getCurrent: sinon.stub().resolves({}),
|
||||||
|
onFocusChanged: {
|
||||||
|
addListener: sinon.stub(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
onActivated: {
|
||||||
|
addListener: sinon.stub()
|
||||||
|
},
|
||||||
|
onCreated: {
|
||||||
|
addListener: sinon.stub()
|
||||||
|
},
|
||||||
|
onUpdated: {
|
||||||
|
addListener: sinon.stub()
|
||||||
|
},
|
||||||
|
sendMessage: sinon.stub(),
|
||||||
|
query: sinon.stub().resolves([{}]),
|
||||||
|
get: sinon.stub(),
|
||||||
|
create: sinon.stub().resolves({}),
|
||||||
|
remove: sinon.stub().resolves()
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
deleteUrl: sinon.stub()
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
local: {
|
||||||
|
get: sinon.stub(),
|
||||||
|
set: sinon.stub()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contextualIdentities: {
|
||||||
|
create: sinon.stub(),
|
||||||
|
get: sinon.stub(),
|
||||||
|
query: sinon.stub().resolves([])
|
||||||
|
},
|
||||||
|
contextMenus: {
|
||||||
|
create: sinon.stub(),
|
||||||
|
remove: sinon.stub(),
|
||||||
|
onClicked: {
|
||||||
|
addListener: sinon.stub()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
browserAction: {
|
||||||
|
setBadgeBackgroundColor: sinon.stub(),
|
||||||
|
setBadgeText: sinon.stub()
|
||||||
|
},
|
||||||
|
management: {
|
||||||
|
get: sinon.stub(),
|
||||||
|
onInstalled: {
|
||||||
|
addListener: sinon.stub()
|
||||||
|
},
|
||||||
|
onUninstalled: {
|
||||||
|
addListener: sinon.stub()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extension: {
|
||||||
|
getURL: sinon.stub().returns("moz-extension://multi-account-containers/confirm-page.html")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// inmemory local storage
|
||||||
|
browserMock.storage.local = {
|
||||||
|
get: sinon.spy(async key => {
|
||||||
|
if (!key) {
|
||||||
|
return _storage;
|
||||||
|
}
|
||||||
|
let result = {};
|
||||||
|
if (Array.isArray(key)) {
|
||||||
|
key.map(akey => {
|
||||||
|
if (typeof _storage[akey] !== "undefined") {
|
||||||
|
result[akey] = _storage[akey];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (typeof key === "object") {
|
||||||
|
// TODO support nested objects
|
||||||
|
Object.keys(key).map(oKey => {
|
||||||
|
if (typeof _storage[oKey] !== "undefined") {
|
||||||
|
result[oKey] = _storage[oKey];
|
||||||
|
} else {
|
||||||
|
result[oKey] = key[oKey];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result = _storage[key];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}),
|
||||||
|
set: sinon.spy(async (key, value) => {
|
||||||
|
if (typeof key === "object") {
|
||||||
|
// TODO support nested objects
|
||||||
|
Object.keys(key).map(oKey => {
|
||||||
|
_storage[oKey] = key[oKey];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_storage[key] = value;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
remove: sinon.spy(async (key) => {
|
||||||
|
if (Array.isArray(key)) {
|
||||||
|
key.map(aKey => {
|
||||||
|
delete _storage[aKey];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
delete _storage[key];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return browserMock;
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
describe("Assignment Feature", () => {
|
||||||
|
const activeTab = {
|
||||||
|
id: 1,
|
||||||
|
cookieStoreId: "firefox-container-1",
|
||||||
|
url: "http://example.com",
|
||||||
|
index: 0
|
||||||
|
};
|
||||||
|
beforeEach(async () => {
|
||||||
|
await helper.browser.initializeWithTab(activeTab);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("click the 'Always open in' checkbox in the popup", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// popup click to set assignment for activeTab.url
|
||||||
|
await helper.popup.clickElementById("container-page-assigned");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("open new Tab with the assigned URL in the default container", () => {
|
||||||
|
const newTab = {
|
||||||
|
id: 2,
|
||||||
|
cookieStoreId: "firefox-default",
|
||||||
|
url: activeTab.url,
|
||||||
|
index: 1,
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
beforeEach(async () => {
|
||||||
|
// new Tab opening activeTab.url in default container
|
||||||
|
await helper.browser.openNewTab(newTab);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should open the confirm page", async () => {
|
||||||
|
// should have created a new tab with the confirm page
|
||||||
|
background.browser.tabs.create.should.have.been.calledWith({
|
||||||
|
url: "moz-extension://multi-account-containers/confirm-page.html?" +
|
||||||
|
`url=${encodeURIComponent(activeTab.url)}` +
|
||||||
|
`&cookieStoreId=${activeTab.cookieStoreId}`,
|
||||||
|
cookieStoreId: undefined,
|
||||||
|
openerTabId: null,
|
||||||
|
index: 2,
|
||||||
|
active: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove the new Tab that got opened in the default container", () => {
|
||||||
|
background.browser.tabs.remove.should.have.been.calledWith(newTab.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("click the 'Always open in' checkbox in the popup again", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// popup click to remove assignment for activeTab.url
|
||||||
|
await helper.popup.clickElementById("container-page-assigned");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("open new Tab with the no longer assigned URL in the default container", () => {
|
||||||
|
const newTab = {
|
||||||
|
id: 3,
|
||||||
|
cookieStoreId: "firefox-default",
|
||||||
|
url: activeTab.url,
|
||||||
|
index: 3,
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
beforeEach(async () => {
|
||||||
|
// new Tab opening activeTab.url in default container
|
||||||
|
await helper.browser.openNewTab(newTab);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not open the confirm page", async () => {
|
||||||
|
// should not have created a new tab
|
||||||
|
background.browser.tabs.create.should.not.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
describe("External Webextensions", () => {
|
||||||
|
const activeTab = {
|
||||||
|
id: 1,
|
||||||
|
cookieStoreId: "firefox-container-1",
|
||||||
|
url: "http://example.com",
|
||||||
|
index: 0
|
||||||
|
};
|
||||||
|
beforeEach(async () => {
|
||||||
|
await helper.browser.initializeWithTab(activeTab);
|
||||||
|
await helper.popup.clickElementById("container-page-assigned");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with contextualIdentities permissions", () => {
|
||||||
|
it("should be able to get assignments", async () => {
|
||||||
|
background.browser.management.get.resolves({
|
||||||
|
permissions: ["contextualIdentities"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
method: "getAssignment",
|
||||||
|
url: "http://example.com"
|
||||||
|
};
|
||||||
|
const sender = {
|
||||||
|
id: "external-webextension"
|
||||||
|
};
|
||||||
|
|
||||||
|
// currently not possible to get the return value of yielding with sinon
|
||||||
|
// so we expect that if no error is thrown and the storage was called, everything is ok
|
||||||
|
// maybe i get around to provide a PR https://github.com/sinonjs/sinon/issues/903
|
||||||
|
//
|
||||||
|
// the alternative would be to expose the actual messageHandler and call it directly
|
||||||
|
// but personally i think that goes against the black-box-ish nature of these feature tests
|
||||||
|
const rejectionStub = sinon.stub();
|
||||||
|
process.on("unhandledRejection", rejectionStub);
|
||||||
|
background.browser.runtime.onMessageExternal.addListener.yield(message, sender);
|
||||||
|
await nextTick();
|
||||||
|
process.removeListener("unhandledRejection", rejectionStub);
|
||||||
|
rejectionStub.should.not.have.been.called;
|
||||||
|
background.browser.storage.local.get.should.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("without contextualIdentities permissions", () => {
|
||||||
|
it("should throw an error", async () => {
|
||||||
|
background.browser.management.get.resolves({
|
||||||
|
permissions: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
method: "getAssignment",
|
||||||
|
url: "http://example.com"
|
||||||
|
};
|
||||||
|
const sender = {
|
||||||
|
id: "external-webextension"
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectionStub = sinon.spy();
|
||||||
|
process.on("unhandledRejection", rejectionStub);
|
||||||
|
background.browser.runtime.onMessageExternal.addListener.yield(message, sender);
|
||||||
|
await nextTick();
|
||||||
|
process.removeListener("unhandledRejection", rejectionStub);
|
||||||
|
rejectionStub.should.have.been.calledWith(sinon.match({
|
||||||
|
message: "Missing contextualIdentities permission"
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
module.exports = {
|
||||||
|
browser: {
|
||||||
|
async initializeWithTab(tab) {
|
||||||
|
await buildBackgroundDom({
|
||||||
|
beforeParse(window) {
|
||||||
|
window.browser.tabs.get.resolves(tab);
|
||||||
|
window.browser.tabs.query.resolves([tab]);
|
||||||
|
window.browser.contextualIdentities.get.resolves({
|
||||||
|
cookieStoreId: tab.cookieStoreId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await buildPopupDom({
|
||||||
|
beforeParse(window) {
|
||||||
|
window.browser.tabs.get.resolves(tab);
|
||||||
|
window.browser.tabs.query.resolves([tab]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async openNewTab(tab, options = {}) {
|
||||||
|
if (options.resetHistory) {
|
||||||
|
background.browser.tabs.create.resetHistory();
|
||||||
|
background.browser.tabs.remove.resetHistory();
|
||||||
|
}
|
||||||
|
background.browser.tabs.get.resolves(tab);
|
||||||
|
background.browser.tabs.onCreated.addListener.yield(tab);
|
||||||
|
const [promise] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: tab.id,
|
||||||
|
url: tab.url,
|
||||||
|
requestId: options.requestId
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
popup: {
|
||||||
|
async clickElementById(id) {
|
||||||
|
const clickEvent = popup.document.createEvent("HTMLEvents");
|
||||||
|
clickEvent.initEvent("click");
|
||||||
|
popup.document.getElementById(id).dispatchEvent(clickEvent);
|
||||||
|
await nextTick();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
describe("#940", () => {
|
||||||
|
describe("when other onBeforeRequestHandlers are faster and redirect with the same requestId", () => {
|
||||||
|
it("should not open two confirm pages", async () => {
|
||||||
|
// init
|
||||||
|
const activeTab = {
|
||||||
|
id: 1,
|
||||||
|
cookieStoreId: "firefox-container-1",
|
||||||
|
url: "http://example.com",
|
||||||
|
index: 0
|
||||||
|
};
|
||||||
|
await helper.browser.initializeWithTab(activeTab);
|
||||||
|
// assign the activeTab.url
|
||||||
|
await helper.popup.clickElementById("container-page-assigned");
|
||||||
|
|
||||||
|
// start request and don't await the requests at all
|
||||||
|
// so the second request below is actually comparable to an actual redirect that also fires immediately
|
||||||
|
const newTab = {
|
||||||
|
id: 2,
|
||||||
|
cookieStoreId: "firefox-default",
|
||||||
|
url: activeTab.url,
|
||||||
|
index: 1,
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
helper.browser.openNewTab(newTab, {
|
||||||
|
requestId: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// other addon sees the same request
|
||||||
|
// and redirects to the https version of activeTab.url
|
||||||
|
// since it's a redirect the request has the same requestId
|
||||||
|
background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: newTab.id,
|
||||||
|
url: "https://example.com",
|
||||||
|
requestId: 1
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
background.browser.tabs.create.should.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when redirects change requestId midflight", () => {
|
||||||
|
let promiseResults;
|
||||||
|
beforeEach(async () => {
|
||||||
|
// init
|
||||||
|
const activeTab = {
|
||||||
|
id: 1,
|
||||||
|
cookieStoreId: "firefox-container-1",
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
index: 0
|
||||||
|
};
|
||||||
|
await helper.browser.initializeWithTab(activeTab);
|
||||||
|
// assign the activeTab.url
|
||||||
|
await helper.popup.clickElementById("container-page-assigned");
|
||||||
|
|
||||||
|
// http://youtube.com
|
||||||
|
const newTab = {
|
||||||
|
id: 2,
|
||||||
|
cookieStoreId: "firefox-default",
|
||||||
|
url: "http://youtube.com",
|
||||||
|
index: 1,
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
const promise1 = helper.browser.openNewTab(newTab, {
|
||||||
|
requestId: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://youtube.com
|
||||||
|
const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: newTab.id,
|
||||||
|
url: "https://youtube.com",
|
||||||
|
requestId: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://www.youtube.com
|
||||||
|
const [promise3] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: newTab.id,
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
requestId: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://www.youtube.com
|
||||||
|
const [promise4] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: newTab.id,
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
requestId: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
promiseResults = await Promise.all([promise1, promise2, promise3, promise4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not open two confirm pages", async () => {
|
||||||
|
// http://youtube.com is not assigned, no cancel, no reopening
|
||||||
|
expect(promiseResults[0]).to.deep.equal({});
|
||||||
|
|
||||||
|
// https://youtube.com is not assigned, no cancel, no reopening
|
||||||
|
expect(promiseResults[1]).to.deep.equal({});
|
||||||
|
|
||||||
|
// https://www.youtube.com is assigned, this triggers reopening, cancel
|
||||||
|
expect(promiseResults[2]).to.deep.equal({
|
||||||
|
cancel: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://www.youtube.com is assigned, this was a redirect, cancel early, no reopening
|
||||||
|
expect(promiseResults[3]).to.deep.equal({
|
||||||
|
cancel: true
|
||||||
|
});
|
||||||
|
|
||||||
|
background.browser.tabs.create.should.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should uncancel after webRequest.onCompleted", async () => {
|
||||||
|
const [promise1] = background.browser.webRequest.onCompleted.addListener.yield({
|
||||||
|
tabId: 2
|
||||||
|
});
|
||||||
|
await promise1;
|
||||||
|
|
||||||
|
const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: 2,
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
requestId: 123
|
||||||
|
});
|
||||||
|
await promise2;
|
||||||
|
|
||||||
|
background.browser.tabs.create.should.have.been.calledTwice;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should uncancel after webRequest.onErrorOccurred", async () => {
|
||||||
|
const [promise1] = background.browser.webRequest.onErrorOccurred.addListener.yield({
|
||||||
|
tabId: 2
|
||||||
|
});
|
||||||
|
await promise1;
|
||||||
|
|
||||||
|
// request to assigned url in same tab
|
||||||
|
const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: 2,
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
requestId: 123
|
||||||
|
});
|
||||||
|
await promise2;
|
||||||
|
|
||||||
|
background.browser.tabs.create.should.have.been.calledTwice;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should uncancel after 2 seconds", async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
// request to assigned url in same tab
|
||||||
|
const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({
|
||||||
|
frameId: 0,
|
||||||
|
tabId: 2,
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
requestId: 123
|
||||||
|
});
|
||||||
|
await promise2;
|
||||||
|
|
||||||
|
background.browser.tabs.create.should.have.been.calledTwice;
|
||||||
|
}).timeout(2002);
|
||||||
|
|
||||||
|
it("should not influence the canceled url in other tabs", async () => {
|
||||||
|
const newTab = {
|
||||||
|
id: 123,
|
||||||
|
cookieStoreId: "firefox-default",
|
||||||
|
url: "https://www.youtube.com",
|
||||||
|
index: 10,
|
||||||
|
active: true
|
||||||
|
};
|
||||||
|
await helper.browser.openNewTab(newTab, {
|
||||||
|
requestId: 321
|
||||||
|
});
|
||||||
|
|
||||||
|
background.browser.tabs.create.should.have.been.calledTwice;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
if (!process.listenerCount("unhandledRejection")) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
process.on("unhandledRejection", r => console.log(r));
|
||||||
|
}
|
||||||
|
const jsdom = require("jsdom");
|
||||||
|
const path = require("path");
|
||||||
|
const chai = require("chai");
|
||||||
|
const sinonChai = require("sinon-chai");
|
||||||
|
global.sinon = require("sinon");
|
||||||
|
global.expect = chai.expect;
|
||||||
|
chai.should();
|
||||||
|
chai.use(sinonChai);
|
||||||
|
global.nextTick = () => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
process.nextTick(resolve);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
global.helper = require("./helper");
|
||||||
|
const browserMock = require("./browser.mock");
|
||||||
|
const srcBasePath = path.resolve(path.join(__dirname, "..", "src"));
|
||||||
|
const srcJsBackgroundPath = path.join(srcBasePath, "js", "background");
|
||||||
|
global.buildBackgroundDom = async (options = {}) => {
|
||||||
|
const dom = await jsdom.JSDOM.fromFile(path.join(srcJsBackgroundPath, "index.html"), {
|
||||||
|
runScripts: "dangerously",
|
||||||
|
resources: "usable",
|
||||||
|
virtualConsole: (new jsdom.VirtualConsole).sendTo(console),
|
||||||
|
beforeParse(window) {
|
||||||
|
window.browser = browserMock();
|
||||||
|
window.fetch = sinon.stub().resolves({
|
||||||
|
json: sinon.stub().resolves({})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.beforeParse) {
|
||||||
|
options.beforeParse(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await new Promise(resolve => {
|
||||||
|
dom.window.document.addEventListener("DOMContentLoaded", resolve);
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
global.background = {
|
||||||
|
dom,
|
||||||
|
browser: dom.window.browser
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
global.buildPopupDom = async (options = {}) => {
|
||||||
|
const dom = await jsdom.JSDOM.fromFile(path.join(srcBasePath, "popup.html"), {
|
||||||
|
runScripts: "dangerously",
|
||||||
|
resources: "usable",
|
||||||
|
virtualConsole: (new jsdom.VirtualConsole).sendTo(console),
|
||||||
|
beforeParse(window) {
|
||||||
|
window.browser = browserMock();
|
||||||
|
window.browser.storage.local.set("browserActionBadgesClicked", []);
|
||||||
|
window.browser.storage.local.set("onboarding-stage", 5);
|
||||||
|
window.browser.storage.local.set("achievements", []);
|
||||||
|
window.browser.storage.local.set.resetHistory();
|
||||||
|
window.fetch = sinon.stub().resolves({
|
||||||
|
json: sinon.stub().resolves({})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.beforeParse) {
|
||||||
|
options.beforeParse(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await new Promise(resolve => {
|
||||||
|
dom.window.document.addEventListener("DOMContentLoaded", resolve);
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
dom.window.browser.runtime.sendMessage.resetHistory();
|
||||||
|
|
||||||
|
if (global.background) {
|
||||||
|
dom.window.browser.runtime.sendMessage = sinon.spy(function() {
|
||||||
|
global.background.browser.runtime.onMessage.addListener.yield(...arguments);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
global.popup = {
|
||||||
|
dom,
|
||||||
|
document: dom.window.document,
|
||||||
|
browser: dom.window.browser
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
global.afterEach(() => {
|
||||||
|
if (global.background) {
|
||||||
|
global.background.dom.window.close();
|
||||||
|
delete global.background;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global.popup) {
|
||||||
|
global.popup.dom.window.close();
|
||||||
|
delete global.popup;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
const main = require("../");
|
|
||||||
|
|
||||||
exports["test main"] = function(assert) {
|
|
||||||
assert.pass("Unit test running!");
|
|
||||||
};
|
|
||||||
|
|
||||||
exports["test main async"] = function(assert, done) {
|
|
||||||
assert.pass("async Unit test running!");
|
|
||||||
done();
|
|
||||||
};
|
|
||||||
|
|
||||||
exports["test dummy"] = function(assert, done) {
|
|
||||||
main.dummy("foo", function(text) {
|
|
||||||
assert.ok((text === "foo"), "Is the text actually 'foo'");
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
require("sdk/test").run(exports);
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
const identityState = {
|
|
||||||
storageArea: {
|
|
||||||
area: browser.storage.local,
|
|
||||||
|
|
||||||
getContainerStoreKey(cookieStoreId) {
|
|
||||||
const storagePrefix = "identitiesState@@_";
|
|
||||||
return `${storagePrefix}${cookieStoreId}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
async get(cookieStoreId) {
|
|
||||||
const storeKey = this.getContainerStoreKey(cookieStoreId);
|
|
||||||
const storageResponse = await this.area.get([storeKey]);
|
|
||||||
if (storageResponse && storeKey in storageResponse) {
|
|
||||||
return storageResponse[storeKey];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
set(cookieStoreId, data) {
|
|
||||||
const storeKey = this.getContainerStoreKey(cookieStoreId);
|
|
||||||
return this.area.set({
|
|
||||||
[storeKey]: data
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
remove(cookieStoreId) {
|
|
||||||
const storeKey = this.getContainerStoreKey(cookieStoreId);
|
|
||||||
return this.area.remove([storeKey]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async _isKnownContainer(userContextId) {
|
|
||||||
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
|
|
||||||
const state = await this.storageArea.get(cookieStoreId);
|
|
||||||
return !!state;
|
|
||||||
},
|
|
||||||
|
|
||||||
_createTabObject(tab) {
|
|
||||||
return Object.assign({}, tab);
|
|
||||||
},
|
|
||||||
|
|
||||||
async storeHidden(cookieStoreId) {
|
|
||||||
const containerState = await this.storageArea.get(cookieStoreId);
|
|
||||||
const tabsByContainer = await this._matchTabsByContainer(cookieStoreId);
|
|
||||||
tabsByContainer.forEach((tab) => {
|
|
||||||
const tabObject = this._createTabObject(tab);
|
|
||||||
// This tab is going to be closed. Let's mark this tabObject as
|
|
||||||
// non-active.
|
|
||||||
tabObject.active = false;
|
|
||||||
tabObject.hiddenState = true;
|
|
||||||
containerState.hiddenTabs.push(tabObject);
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.storageArea.set(cookieStoreId, containerState);
|
|
||||||
},
|
|
||||||
|
|
||||||
async containersCounts() {
|
|
||||||
let containersCounts = { // eslint-disable-line prefer-const
|
|
||||||
"shown": 0,
|
|
||||||
"hidden": 0,
|
|
||||||
"total": 0
|
|
||||||
};
|
|
||||||
const containers = await browser.contextualIdentities.query({});
|
|
||||||
for (const id in containers) {
|
|
||||||
const container = containers[id];
|
|
||||||
await this.remapTabsIfMissing(container.cookieStoreId);
|
|
||||||
const containerState = await this.storageArea.get(container.cookieStoreId);
|
|
||||||
if (containerState.openTabs > 0) {
|
|
||||||
++containersCounts.shown;
|
|
||||||
++containersCounts.total;
|
|
||||||
continue;
|
|
||||||
} else if (containerState.hiddenTabs.length > 0) {
|
|
||||||
++containersCounts.hidden;
|
|
||||||
++containersCounts.total;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return containersCounts;
|
|
||||||
},
|
|
||||||
|
|
||||||
async containerTabCount(cookieStoreId) {
|
|
||||||
// Returns the total of open and hidden tabs with this userContextId
|
|
||||||
let containerTabsCount = 0;
|
|
||||||
await identityState.remapTabsIfMissing(cookieStoreId);
|
|
||||||
const containerState = await this.storageArea.get(cookieStoreId);
|
|
||||||
containerTabsCount += containerState.openTabs;
|
|
||||||
containerTabsCount += containerState.hiddenTabs.length;
|
|
||||||
return containerTabsCount;
|
|
||||||
},
|
|
||||||
|
|
||||||
async totalContainerTabsCount() {
|
|
||||||
// Returns the number of total open tabs across ALL containers
|
|
||||||
let totalContainerTabsCount = 0;
|
|
||||||
const containers = await browser.contextualIdentities.query({});
|
|
||||||
for (const id in containers) {
|
|
||||||
const container = containers[id];
|
|
||||||
const cookieStoreId = container.cookieStoreId;
|
|
||||||
await identityState.remapTabsIfMissing(cookieStoreId);
|
|
||||||
totalContainerTabsCount += await this.storageArea.get(cookieStoreId).openTabs;
|
|
||||||
}
|
|
||||||
return totalContainerTabsCount;
|
|
||||||
},
|
|
||||||
|
|
||||||
async totalNonContainerTabsCount() {
|
|
||||||
// Returns the number of open tabs NOT IN a container
|
|
||||||
let totalNonContainerTabsCount = 0;
|
|
||||||
const tabs = await browser.tabs.query({});
|
|
||||||
for (const tab of tabs) {
|
|
||||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId);
|
|
||||||
if (userContextId === 0) {
|
|
||||||
++totalNonContainerTabsCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return totalNonContainerTabsCount;
|
|
||||||
},
|
|
||||||
|
|
||||||
async remapTabsIfMissing(cookieStoreId) {
|
|
||||||
// We already know this cookieStoreId.
|
|
||||||
const containerState = await this.storageArea.get(cookieStoreId) || this._createIdentityState();
|
|
||||||
|
|
||||||
await this.storageArea.set(cookieStoreId, containerState);
|
|
||||||
await this.remapTabsFromUserContextId(cookieStoreId);
|
|
||||||
},
|
|
||||||
|
|
||||||
_matchTabsByContainer(cookieStoreId) {
|
|
||||||
return browser.tabs.query({cookieStoreId});
|
|
||||||
},
|
|
||||||
|
|
||||||
async remapTabsFromUserContextId(cookieStoreId) {
|
|
||||||
const tabsByContainer = await this._matchTabsByContainer(cookieStoreId);
|
|
||||||
const containerState = await this.storageArea.get(cookieStoreId);
|
|
||||||
containerState.openTabs = tabsByContainer.length;
|
|
||||||
await this.storageArea.set(cookieStoreId, containerState);
|
|
||||||
},
|
|
||||||
|
|
||||||
_createIdentityState() {
|
|
||||||
return {
|
|
||||||
hiddenTabs: [],
|
|
||||||
openTabs: 0
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
browser.runtime.sendMessage({
|
|
||||||
method: "getPreference",
|
|
||||||
pref: "browser.privatebrowsing.autostart"
|
|
||||||
}).then(pbAutoStart => {
|
|
||||||
|
|
||||||
// We don't want to disable the addon if we are in auto private-browsing.
|
|
||||||
if (!pbAutoStart) {
|
|
||||||
browser.tabs.onCreated.addListener(tab => {
|
|
||||||
if (tab.incognito) {
|
|
||||||
disableAddon(tab.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.tabs.query({}).then(tabs => {
|
|
||||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
|
||||||
if (tab.incognito) {
|
|
||||||
disableAddon(tab.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
}).catch(() => {});
|
|
||||||
|
|
||||||
function disableAddon(tabId) {
|
|
||||||
browser.browserAction.disable(tabId);
|
|
||||||
browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" });
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
const messageHandler = {
|
|
||||||
// After the timer completes we assume it's a tab the user meant to keep open
|
|
||||||
// We use this to catch redirected tabs that have just opened
|
|
||||||
// If this were in platform we would change how the tab opens based on "new tab" link navigations such as ctrl+click
|
|
||||||
LAST_CREATED_TAB_TIMER: 2000,
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// Handles messages from webextension code
|
|
||||||
browser.runtime.onMessage.addListener((m) => {
|
|
||||||
let response;
|
|
||||||
|
|
||||||
switch (m.method) {
|
|
||||||
case "deleteContainer":
|
|
||||||
response = backgroundLogic.deleteContainer(m.message.userContextId);
|
|
||||||
break;
|
|
||||||
case "createOrUpdateContainer":
|
|
||||||
response = backgroundLogic.createOrUpdateContainer(m.message);
|
|
||||||
break;
|
|
||||||
case "openTab":
|
|
||||||
// Same as open-tab for index.js
|
|
||||||
response = backgroundLogic.openTab(m.message);
|
|
||||||
break;
|
|
||||||
case "neverAsk":
|
|
||||||
assignManager._neverAsk(m);
|
|
||||||
break;
|
|
||||||
case "getAssignment":
|
|
||||||
response = browser.tabs.get(m.tabId).then((tab) => {
|
|
||||||
return assignManager._getAssignment(tab);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "getAssignmentObjectByContainer":
|
|
||||||
response = assignManager._getByContainer(m.message.userContextId);
|
|
||||||
break;
|
|
||||||
case "setOrRemoveAssignment":
|
|
||||||
// m.tabId is used for where to place the in content message
|
|
||||||
// m.url is the assignment to be removed/added
|
|
||||||
response = browser.tabs.get(m.tabId).then((tab) => {
|
|
||||||
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "sortTabs":
|
|
||||||
backgroundLogic.sortTabs();
|
|
||||||
break;
|
|
||||||
case "showTabs":
|
|
||||||
backgroundLogic.showTabs({cookieStoreId: m.cookieStoreId});
|
|
||||||
break;
|
|
||||||
case "hideTabs":
|
|
||||||
backgroundLogic.hideTabs({cookieStoreId: m.cookieStoreId});
|
|
||||||
break;
|
|
||||||
case "checkIncompatibleAddons":
|
|
||||||
// TODO
|
|
||||||
break;
|
|
||||||
case "moveTabsToWindow":
|
|
||||||
response = backgroundLogic.moveTabsToWindow({
|
|
||||||
cookieStoreId: m.cookieStoreId
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "getTabs":
|
|
||||||
response = backgroundLogic.getTabs({
|
|
||||||
cookieStoreId: m.cookieStoreId
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "queryIdentitiesState":
|
|
||||||
response = backgroundLogic.queryIdentitiesState();
|
|
||||||
break;
|
|
||||||
case "exemptContainerAssignment":
|
|
||||||
response = assignManager._exemptTab(m);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handles messages from sdk code
|
|
||||||
const port = browser.runtime.connect();
|
|
||||||
port.onMessage.addListener(m => {
|
|
||||||
switch (m.type) {
|
|
||||||
case "open-tab":
|
|
||||||
backgroundLogic.openTab(m.message);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unhandled message type: ${m.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.tabs.onCreated.addListener((tab) => {
|
|
||||||
// This works at capturing the tabs as they are created
|
|
||||||
// However we need onFocusChanged and onActivated to capture the initial tab
|
|
||||||
if (tab.id === -1) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
tabPageCounter.initTabCounter(tab);
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.tabs.onRemoved.addListener((tabId) => {
|
|
||||||
if (tabId === -1) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
tabPageCounter.sendTabCountAndDelete(tabId);
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.tabs.onActivated.addListener((info) => {
|
|
||||||
assignManager.removeContextMenu();
|
|
||||||
browser.tabs.get(info.tabId).then((tab) => {
|
|
||||||
tabPageCounter.initTabCounter(tab);
|
|
||||||
assignManager.calculateContextMenu(tab);
|
|
||||||
}).catch((e) => {
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.windows.onFocusChanged.addListener((windowId) => {
|
|
||||||
assignManager.removeContextMenu();
|
|
||||||
// browserAction loses background color in new windows ...
|
|
||||||
// https://bugzil.la/1314674
|
|
||||||
// https://github.com/mozilla/testpilot-containers/issues/608
|
|
||||||
// ... so re-call displayBrowserActionBadge on window changes
|
|
||||||
badge.displayBrowserActionBadge();
|
|
||||||
browser.tabs.query({active: true, windowId}).then((tabs) => {
|
|
||||||
if (tabs && tabs[0]) {
|
|
||||||
tabPageCounter.initTabCounter(tabs[0]);
|
|
||||||
assignManager.calculateContextMenu(tabs[0]);
|
|
||||||
}
|
|
||||||
}).catch((e) => {
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.idle.onStateChanged.addListener((newState) => {
|
|
||||||
browser.tabs.query({}).then(tabs => {
|
|
||||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
|
||||||
if (newState === "idle") {
|
|
||||||
tabPageCounter.sendTabCountAndDelete(tab.id, "user-went-idle");
|
|
||||||
} else if (newState === "active" && tab.active) {
|
|
||||||
tabPageCounter.initTabCounter(tab);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).catch(e => {
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.webRequest.onCompleted.addListener((details) => {
|
|
||||||
if (details.frameId !== 0 || details.tabId === -1) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
assignManager.removeContextMenu();
|
|
||||||
|
|
||||||
browser.tabs.get(details.tabId).then((tab) => {
|
|
||||||
tabPageCounter.incrementTabCount(tab);
|
|
||||||
assignManager.calculateContextMenu(tab);
|
|
||||||
}).catch((e) => {
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
}, {urls: ["<all_urls>"], types: ["main_frame"]});
|
|
||||||
|
|
||||||
// lets remember the last tab created so we can close it if it looks like a redirect
|
|
||||||
browser.tabs.onCreated.addListener((details) => {
|
|
||||||
this.lastCreatedTab = details;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.lastCreatedTab = null;
|
|
||||||
}, this.LAST_CREATED_TAB_TIMER);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lets do this last as theme manager did a check before connecting before
|
|
||||||
messageHandler.init();
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const tabPageCounter = {
|
|
||||||
counters: {},
|
|
||||||
|
|
||||||
initTabCounter(tab) {
|
|
||||||
if (tab.id in this.counters) {
|
|
||||||
if (!("activity" in this.counters[tab.id])) {
|
|
||||||
this.counters[tab.id].activity = {
|
|
||||||
"cookieStoreId": tab.cookieStoreId,
|
|
||||||
"pageRequests": 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!("tab" in this.counters[tab.id])) {
|
|
||||||
this.counters[tab.id].tab = {
|
|
||||||
"cookieStoreId": tab.cookieStoreId,
|
|
||||||
"pageRequests": 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.counters[tab.id] = {};
|
|
||||||
this.counters[tab.id].tab = {
|
|
||||||
"cookieStoreId": tab.cookieStoreId,
|
|
||||||
"pageRequests": 0
|
|
||||||
};
|
|
||||||
this.counters[tab.id].activity = {
|
|
||||||
"cookieStoreId": tab.cookieStoreId,
|
|
||||||
"pageRequests": 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
sendTabCountAndDelete(tabId, why = "user-closed-tab") {
|
|
||||||
if (!(this.counters[tabId])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (why === "user-closed-tab" && this.counters[tabId].tab) {
|
|
||||||
// When we send the ping because the user closed the tab,
|
|
||||||
// delete both the 'tab' and 'activity' counters
|
|
||||||
delete this.counters[tabId];
|
|
||||||
} else if (why === "user-went-idle" && this.counters[tabId].activity) {
|
|
||||||
// When we send the ping because the user went idle,
|
|
||||||
// only reset the 'activity' counter
|
|
||||||
this.counters[tabId].activity = {
|
|
||||||
"cookieStoreId": this.counters[tabId].tab.cookieStoreId,
|
|
||||||
"pageRequests": 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
incrementTabCount(tab) {
|
|
||||||
this.initTabCounter(tab);
|
|
||||||
this.counters[tab.id].tab.pageRequests++;
|
|
||||||
this.counters[tab.id].activity.pageRequests++;
|
|
||||||
}
|
|
||||||
};
|
|
||||||