Compare commits
229 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 532abbf032 | |||
| d8f99ada77 | |||
| 35cf2c95d4 | |||
| 19e694d8eb | |||
| ae1f80b8b5 | |||
| 358551feda | |||
| ca98b56ad7 | |||
| 5bbf902b47 | |||
| d654810d41 | |||
| 8172f291dd | |||
| 9e12014c07 | |||
| da79841201 | |||
| 00ace352e8 | |||
| 8cdfe0191f | |||
| f56f9b0ea8 | |||
| 4bc6fad83a | |||
| beb027f1a1 | |||
| 06b43c47c0 | |||
| 7ed5085f10 | |||
| c300751958 | |||
| e6f5bf76c2 | |||
| 88d8edce7e | |||
| 419be23c9c | |||
| 405e605ba3 | |||
| d7b1e7274f | |||
| 53baee1d5c | |||
| 0447e54b1c | |||
| 5b8cfa14ae | |||
| d35712e9f9 | |||
| f981200921 | |||
| 85c403bef5 | |||
| 268c638508 | |||
| 61b5c2e4b2 | |||
| 3aa2902cde | |||
| d7b66eca52 | |||
| 361e49d553 | |||
| 60bfdffdd6 | |||
| d7c0a3d9e1 | |||
| a7d3d6d848 | |||
| 26028cac20 | |||
| 33909d147a | |||
| 10d08f2ac9 | |||
| 5ae7a395a1 | |||
| 9de6df6157 | |||
| 6184dbb656 | |||
| 34180aa2d5 | |||
| 3a7744b41b | |||
| 53f7dc4915 | |||
| 53a901d023 | |||
| c522d36a66 | |||
| f7b20f97b8 | |||
| d033292784 | |||
| 26cd3c3cc8 | |||
| be3904cee8 | |||
| 0252f9d1c3 | |||
| 19dce3ba45 | |||
| 0bdf8558f6 | |||
| 0e45f06338 | |||
| 063b7509bd | |||
| 136aa3ce0e | |||
| a7f6659204 | |||
| f4024bba66 | |||
| e65c88cde2 | |||
| 27225df281 | |||
| 88e32fc72f | |||
| d61ea16db1 | |||
| adc1f5ffe6 | |||
| e8f8123c4c | |||
| 9788e159ae | |||
| d98d0f1697 | |||
| efb83255fd | |||
| f5993add6f | |||
| 24c960bed0 | |||
| dc9e8f6399 | |||
| e37440720f | |||
| 703a7cd3c2 | |||
| 5eb79949e9 | |||
| 77fd843b3d | |||
| 96ab4685e9 | |||
| 426e81b88b | |||
| 3c2dda5100 | |||
| fca5caaa00 | |||
| b0661f79aa | |||
| e44fcdf606 | |||
| 645fb6832c | |||
| f653b5420f | |||
| f159e42029 | |||
| a8aafc7064 | |||
| 3284bfd7f3 | |||
| da24febe6e | |||
| 3e5334e9b0 | |||
| 8dd9927539 | |||
| c3da573722 | |||
| f40fd82518 | |||
| ef894d847e | |||
| 7c08cf3190 | |||
| 6f94ae1500 | |||
| bd579fe907 | |||
| 87a2aa5f1e | |||
| 1133d48103 | |||
| dda5c9eeff | |||
| e7824f367b | |||
| 29a0277398 | |||
| 510a188dee | |||
| 7fa2d494c4 | |||
| 7e4950b184 | |||
| c832cf73aa | |||
| 9ff1a14e0e | |||
| 384ac486d9 | |||
| 2cddbefb63 | |||
| dae376500b | |||
| c4650d12bd | |||
| 18bc8eb5aa | |||
| 8eceb0d298 | |||
| 27d51f89c7 | |||
| d04252a5ad | |||
| 6b02258295 | |||
| 97169d6f17 | |||
| a874c9c628 | |||
| c28d77e7cd | |||
| 397a3e4970 | |||
| a4c578adde | |||
| db8b7ea05f | |||
| f33b3b39c0 | |||
| a328e5bf91 | |||
| 46a55da277 | |||
| 1746d8379b | |||
| f57cf92f41 | |||
| 54659f5c77 | |||
| 2badd22f41 | |||
| 21b7386a94 | |||
| 529b9bb482 | |||
| 3adb333022 | |||
| cc988a303e | |||
| 46349e1a70 | |||
| acff80a234 | |||
| bbe655d879 | |||
| 7cbace9cc9 | |||
| 9372899bfa | |||
| 4d42a74e66 | |||
| 76f7a64cb8 | |||
| 1af8cf8222 | |||
| 11a3b2facd | |||
| bd993d2f84 | |||
| cf9683174d | |||
| f617ca26bb | |||
| 41686fdf6c | |||
| c67a985847 | |||
| 639399925c | |||
| 2ded900188 | |||
| 3ae1803420 | |||
| 17b2d8c773 | |||
| 7025c98e7b | |||
| 6f4d3c4327 | |||
| 7833a6a79f | |||
| c2f2d69ba1 | |||
| 42b0312790 | |||
| cc42beeb5a | |||
| ea5669911b | |||
| b4c2da5474 | |||
| 6d7086d541 | |||
| db0dba66b2 | |||
| 5b58168999 | |||
| 4ed453d58e | |||
| 66a9116524 | |||
| cb7ac6ca5e | |||
| a2995b6c66 | |||
| ed383c8dfc | |||
| df9b900db6 | |||
| 8e611de605 | |||
| 8af4c36fd0 | |||
| abc4e0cdcf | |||
| b6dd32f683 | |||
| 0a437ff303 | |||
| f7f4c320a6 | |||
| 5813621fb9 | |||
| 56fc7407da | |||
| 220b902144 | |||
| c15eee22c6 | |||
| dcc3b76cda | |||
| 752d18ffca | |||
| 97559dd08a | |||
| 0e7363a87f | |||
| 6c62c2f599 | |||
| 884e419a7c | |||
| d7586dd4c2 | |||
| aada0419eb | |||
| 1ea04587d9 | |||
| 3d1dcd33d1 | |||
| bfdbd8199f | |||
| 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 |
@@ -1 +1,2 @@
|
||||
lib/testpilot/*.js
|
||||
coverage
|
||||
@@ -0,0 +1,27 @@
|
||||
<!--
|
||||
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 information.
|
||||
|
||||
Also please make sure that:
|
||||
- "Firefox will: Never remember history" in the Firefox Preferences/Options under "Privacy & Security > History" is NOT selected
|
||||
- You are NOT using Firefox in a Private Window
|
||||
- You can see a grayed out but ticked Checkbox with the description "Enable Container Tabs" in the Firefox Preferences/Options under "Tabs"
|
||||
-->
|
||||
- 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
|
||||
@@ -9,3 +9,10 @@ README.html
|
||||
addon.env
|
||||
|
||||
src/web-ext-artifacts/*
|
||||
|
||||
# JetBrains IDE files
|
||||
.idea
|
||||
|
||||
# IstanbulJS
|
||||
.nyc_output
|
||||
coverage
|
||||
@@ -1,6 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6.1"
|
||||
- "lts/*"
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
# Code Of Conduct
|
||||
# Community Participation Guidelines
|
||||
|
||||
This add-on follows the [Mozilla Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/) for our code of conduct.
|
||||
This repository is governed by Mozilla's code of conduct and etiquette guidelines.
|
||||
For more details, please read the
|
||||
[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
|
||||
|
||||
## How to Report
|
||||
For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page.
|
||||
|
||||
<!--
|
||||
## Project Specific Etiquette
|
||||
|
||||
In some cases, there will be additional project etiquette i.e.: (https://bugzilla.mozilla.org/page.cgi?id=etiquette.html).
|
||||
Please update for your project.
|
||||
-->
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
2. Click "Copy raw data to clipboard" and paste into the bug. Alternatively copy the following sections into the issue:
|
||||
|
||||
@@ -19,10 +19,18 @@ For more info, see:
|
||||
## Development
|
||||
|
||||
1. `npm install`
|
||||
2. `./node_modules/.bin/web-ext run -s src/
|
||||
2. `./node_modules/web-ext/bin/web-ext run -s src/`
|
||||
|
||||
### Testing
|
||||
TBD
|
||||
`npm run test`
|
||||
|
||||
or
|
||||
|
||||
`npm run lint`
|
||||
|
||||
for just the linter
|
||||
|
||||
There is a timeout test that sometimes fails on certain machines, so make sure to run the tests on your clone before you make any changes to see if you have this problem.
|
||||
|
||||
### Distributing
|
||||
#### Make the new version
|
||||
@@ -49,8 +57,8 @@ Finally, we also publish the release to GitHub for those followers.
|
||||
|
||||
### Links
|
||||
|
||||
Facebook & Twitter icons CC-Attrib http://fairheadcreative.com.
|
||||
Facebook & Twitter icons CC-Attrib https://fairheadcreative.com.
|
||||
|
||||
- [Licence](./LICENSE.txt)
|
||||
- [License](./LICENSE.txt)
|
||||
- [Contributing](./CONTRIBUTING.md)
|
||||
- [Code Of Conduct](./CODE_OF_CONDUCT.md)
|
||||
|
||||
@@ -2,42 +2,51 @@
|
||||
"name": "testpilot-containers",
|
||||
"title": "Multi-Account Containers",
|
||||
"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": "5.0.0",
|
||||
"version": "6.2.5",
|
||||
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mozilla/testpilot-containers/issues"
|
||||
"url": "https://github.com/mozilla/multi-account-containers/issues"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"addons-linter": "^0.15.14",
|
||||
"deploy-txp": "^1.0.7",
|
||||
"eslint": "^3.17.1",
|
||||
"addons-linter": "^1.3.2",
|
||||
"ajv": "^6.6.3",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-plugin-no-unsanitized": "^2.0.0",
|
||||
"eslint-plugin-promise": "^3.4.0",
|
||||
"htmllint-cli": "^0.0.5",
|
||||
"htmllint-cli": "0.0.7",
|
||||
"json": "^9.0.6",
|
||||
"mocha": "^6.2.2",
|
||||
"npm-run-all": "^4.0.0",
|
||||
"nyc": "^15.0.0",
|
||||
"sinon": "^7.5.0",
|
||||
"sinon-chai": "^3.3.0",
|
||||
"stylelint": "^7.9.0",
|
||||
"stylelint-config-standard": "^16.0.0",
|
||||
"stylelint-order": "^0.3.0",
|
||||
"web-ext": "^2.2.2"
|
||||
"web-ext": "^2.9.3",
|
||||
"webextensions-jsdom": "^1.2.1"
|
||||
},
|
||||
"homepage": "https://github.com/mozilla/testpilot-containers#readme",
|
||||
"homepage": "https://github.com/mozilla/multi-account-containers#readme",
|
||||
"license": "MPL-2.0",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mozilla/testpilot-containers.git"
|
||||
"url": "git+https://github.com/mozilla/multi-account-containers.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm test && cd src && web-ext build --overwrite-dest",
|
||||
"deploy": "deploy-txp",
|
||||
"webext": "web-ext run -s src/",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:addon": "addons-linter src --self-hosted",
|
||||
"lint:css": "stylelint src/css/*.css",
|
||||
"lint:html": "htmllint *.html",
|
||||
"lint:js": "eslint .",
|
||||
"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 && npm run coverage",
|
||||
"test:once": "mocha test/**/*.test.js",
|
||||
"test:watch": "npm run test:once -- --watch",
|
||||
"coverage": "nyc --reporter=html --reporter=text mocha test/**/*.test.js --timeout 60000"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ html {
|
||||
}
|
||||
|
||||
#redirect-url {
|
||||
background: #efefef;
|
||||
background: #efedf0; /* Grey 20 */
|
||||
border-radius: 2px;
|
||||
line-height: 1.5;
|
||||
padding-block-end: 0.5rem;
|
||||
@@ -56,6 +56,15 @@ html {
|
||||
padding-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
/* stylelint-disable media-feature-name-no-unknown */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#redirect-url {
|
||||
background: #38383d; /* Grey 70 */
|
||||
color: #eee; /* White 20 */
|
||||
}
|
||||
}
|
||||
/* stylelint-enable */
|
||||
|
||||
#redirect-url img {
|
||||
block-size: 16px;
|
||||
inline-size: 16px;
|
||||
|
||||
@@ -19,8 +19,13 @@ html {
|
||||
|
||||
body {
|
||||
font-family: Roboto, Noto, "San Francisco", Ubuntu, "Segoe UI", "Fira Sans", message-box, Arial, sans-serif;
|
||||
inline-size: 300px;
|
||||
max-inline-size: 300px;
|
||||
inline-size: calc(var(--overflow-size) + 299px);
|
||||
max-inline-size: calc(var(--overflow-size) + 299px);
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
block-size: 100%; /* Bugfix: issue 948 */
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -45,6 +50,10 @@ body {
|
||||
--small-text-size: 0.833rem; /* 10px */
|
||||
--small-radius: 3px;
|
||||
--icon-button-size: calc(calc(var(--block-line-separation-size) * 2) + 1.66rem); /* 20px */
|
||||
--column-panel-inline-size: calc(var(--overflow-size) + 267px);
|
||||
--inactive-opacity: 0.3;
|
||||
--overflow-size: 1px;
|
||||
--icon-fit: 8;
|
||||
}
|
||||
|
||||
@media (min-resolution: 1dppx) {
|
||||
@@ -234,7 +243,7 @@ table {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Panels keep everything togethert */
|
||||
/* Panels keep everything together */
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -266,7 +275,7 @@ table {
|
||||
.column-panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
inline-size: 268px;
|
||||
inline-size: var(--column-panel-inline-size);
|
||||
}
|
||||
|
||||
.column-panel-content .panel-footer {
|
||||
@@ -347,11 +356,45 @@ table {
|
||||
transition: background-color 75ms;
|
||||
}
|
||||
|
||||
.half-button-wrapper {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 44px;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.half-onboarding-button {
|
||||
align-items: center;
|
||||
background-color: #0996f8;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
font-size: 14px;
|
||||
height: 44px;
|
||||
inline-size: 50%;
|
||||
justify-content: center;
|
||||
margin-inline-end: 4px;
|
||||
text-decoration: none;
|
||||
transition: background-color 75ms;
|
||||
}
|
||||
|
||||
.grey-button {
|
||||
background-color: #e3e3e3;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.onboarding-button:hover,
|
||||
.onboarding-button:active {
|
||||
background-color: #0675d3;
|
||||
}
|
||||
|
||||
.onboarding-button:focus,
|
||||
.half-onboarding-button:focus {
|
||||
box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Pop buttons are the square shaped buttons used to
|
||||
manage things like container crud */
|
||||
.pop-button {
|
||||
@@ -537,7 +580,7 @@ span ~ .panel-header-text {
|
||||
}
|
||||
|
||||
#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-size: var(--icon-size);
|
||||
}
|
||||
@@ -575,7 +618,12 @@ span ~ .panel-header-text {
|
||||
}
|
||||
|
||||
.edit-containers-panel .userContext-wrapper {
|
||||
max-inline-size: 204px;
|
||||
max-inline-size: calc(var(--overflow-size) + 203px);
|
||||
}
|
||||
|
||||
.disable-edit-containers {
|
||||
opacity: var(--inactive-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.userContext-wrapper {
|
||||
@@ -653,7 +701,11 @@ span ~ .panel-header-text {
|
||||
|
||||
/* Container info list */
|
||||
.container-info-tab-title {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.container-info-tab-row:hover .container-info-tab-title .truncate-text {
|
||||
inline-size: calc(var(--column-panel-inline-size) - 58px);
|
||||
}
|
||||
|
||||
#container-info-hideorshow {
|
||||
@@ -670,6 +722,21 @@ span ~ .panel-header-text {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.container-close-tab {
|
||||
transform: scale(0.7);
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
.container-info-tab-row:hover .container-close-tab {
|
||||
opacity: 0.5;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.container-info-tab-row .container-close-tab:hover {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.container-info-has-tabs,
|
||||
.container-info-tab-row {
|
||||
align-items: center;
|
||||
@@ -696,10 +763,6 @@ span ~ .panel-header-text {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
.container-info-tab-row td {
|
||||
max-inline-size: 200px;
|
||||
}
|
||||
|
||||
.container-info-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -725,21 +788,24 @@ span ~ .panel-header-text {
|
||||
background: var(--primary-action-color);
|
||||
block-size: 100%;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
display: inline-block;
|
||||
justify-content: center;
|
||||
padding-block-start: 6px;
|
||||
padding-inline-start: 30%;
|
||||
}
|
||||
|
||||
.exit-edit-mode-link::before {
|
||||
background: url('/img/container-arrow.svg') no-repeat;
|
||||
.edit-containers-panel-footer {
|
||||
background: var(--primary-action-color);
|
||||
}
|
||||
|
||||
.exit-edit-mode-link img {
|
||||
block-size: 16px;
|
||||
content: "";
|
||||
display: block;
|
||||
display: inline;
|
||||
filter: grayscale(100%) brightness(5);
|
||||
float: left;
|
||||
inline-size: 16px;
|
||||
margin-inline-end: 5px;
|
||||
transform: scaleX(-1);
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.delete-container-confirm {
|
||||
@@ -761,8 +827,12 @@ span ~ .panel-header-text {
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
.edit-container-panel .columns {
|
||||
overflow: hidden; /* Bugfix: issue 948 */
|
||||
}
|
||||
|
||||
#edit-sites-assigned {
|
||||
flex: 1;
|
||||
flex: 1000; /* Bugfix: issue 948 */
|
||||
}
|
||||
|
||||
#edit-sites-assigned h3 {
|
||||
@@ -800,7 +870,7 @@ span ~ .panel-header-text {
|
||||
align-items: center;
|
||||
block-size: 29px;
|
||||
display: flex;
|
||||
flex: 0 0 calc(100% / 8);
|
||||
flex: 0 0 calc(100% / var(--icon-fit));
|
||||
}
|
||||
|
||||
.radio-choice > .radio-container > label {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 160"><defs><style>.cls-1{fill:#6a57a5;}.cls-2{fill:#5a4a9e;}.cls-3{fill:#e7dfff;}</style></defs><title>account</title><path class="cls-1" d="M110,138.89A58.89,58.89,0,1,1,168.89,80,59,59,0,0,1,110,138.89Z"/><path class="cls-2" d="M110,130.27A50.27,50.27,0,1,1,160.27,80,50.33,50.33,0,0,1,110,130.27Z"/><circle class="cls-3" cx="110.39" cy="65.12" r="23.27" transform="translate(-12.01 27.1) rotate(-13.28)"/><path class="cls-3" d="M141.78,92.87c-8.2-9.46-19.58,3.28-31.39,3.28S87.2,83.41,79,92.87a7.83,7.83,0,0,0-.53,9.53,38.43,38.43,0,0,0,63.83,0A7.83,7.83,0,0,0,141.78,92.87Z"/></svg>
|
||||
|
After Width: | Height: | Size: 676 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 160"><defs><style>.cls-1{fill:#9f9fad;}.cls-2{fill:#5a4a9e;}.cls-3{fill:#6a57a5;}.cls-4{fill:#8f8f9d;}.cls-5{fill:none;stroke:#80808e;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.6px;}.cls-6{fill:#231f20;opacity:0.4;}.cls-7{fill:#ee3389;}.cls-8{fill:#7661aa;}</style></defs><title>Sync</title><path class="cls-1" d="M119.16,122.69v4.81H19.76v-4.81l12.83-3.21h72.15Z"/><rect class="cls-1" x="24.57" y="55.35" width="89.79" height="67.34" rx="3"/><path class="cls-2" d="M79.08,65l-49.7,49.7a1.61,1.61,0,0,0,1.6,1.61h77a1.62,1.62,0,0,0,1.61-1.61V65Z"/><polygon class="cls-3" points="29.38 64.97 29.38 114.67 79.08 64.97 29.38 64.97"/><path class="cls-2" d="M107.94,60.16H31a1.6,1.6,0,0,0-1.6,1.6V65h80.17V61.76A1.61,1.61,0,0,0,107.94,60.16Z"/><path class="cls-4" d="M108.74,121.09H30.18a.81.81,0,0,1,0-1.61h78.56a.81.81,0,1,1,0,1.61Z"/><line class="cls-5" x1="63.61" y1="124.18" x2="74.83" y2="124.18"/><path class="cls-6" d="M114.35,127.35H102.2V71.64a5.53,5.53,0,0,1,5.52-5.53h6.63Z"/><path class="cls-1" d="M200.24,134.72v4.81h-99.4v-4.81l12.82-3.21h72.15Z"/><rect class="cls-1" x="105.65" y="67.38" width="89.79" height="67.34" rx="3"/><path class="cls-2" d="M160.16,77l-49.71,49.7a1.61,1.61,0,0,0,1.61,1.6h77a1.6,1.6,0,0,0,1.6-1.6V77Z"/><polygon class="cls-3" points="110.45 77 110.45 126.7 160.16 77 110.45 77"/><path class="cls-2" d="M189,72.19h-77a1.61,1.61,0,0,0-1.61,1.6V77h80.17V73.79A1.6,1.6,0,0,0,189,72.19Z"/><path class="cls-4" d="M189.82,133.11H111.26a.8.8,0,1,1,0-1.6h78.56a.8.8,0,0,1,0,1.6Z"/><line class="cls-5" x1="144.69" y1="136.2" x2="155.91" y2="136.2"/><path class="cls-7" d="M136.85,50l-3-.55a3,3,0,0,0-3.51,2.37l-.27,1.45c-1.59,8.36-9.86,14.42-19.66,14.42a21,21,0,0,1-15.93-6.89H103a3,3,0,0,0,3-3v-3a3,3,0,0,0-3-3H84.86a3,3,0,0,0-3,3V73.64a3,3,0,0,0,3,3h3a3,3,0,0,0,3-3V69.72a30.8,30.8,0,0,0,19.57,6.87c14.15,0,26.15-9.11,28.54-21.66l.27-1.45A2.94,2.94,0,0,0,136.85,50Z"/><path class="cls-8" d="M84.06,47l3,.54a3.41,3.41,0,0,0,.55,0,3,3,0,0,0,3-2.41l.27-1.45h0c1.59-8.36,9.86-14.42,19.65-14.42a21,21,0,0,1,15.94,6.89H117.9a3,3,0,0,0-3,3v3a3,3,0,0,0,3,3h18.15a3,3,0,0,0,3-3V23.43a3,3,0,0,0-3-3h-3a3,3,0,0,0-3,3v3.92a30.82,30.82,0,0,0-19.58-6.88c-14.14,0-26.14,9.11-28.53,21.67l-.27,1.45A3,3,0,0,0,84.06,47Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 61 KiB |
@@ -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 |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 7 7">
|
||||
<polygon fill="#4c4c4c" points="5.8,0 3.5,2.4 1.2,0 0,1.2 2.4,3.5 0.1,5.8 1.2,7 3.5,4.7 5.8,7 7,5.8 4.7,3.5 7,1.2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 183 B |
@@ -0,0 +1 @@
|
||||
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="rgba(249, 249, 250, 0.8)" fill-rule="evenodd"><rect height="6" rx="1" width="6" x="1" y="1"/><path d="m11 1h2v6h-2z" transform="matrix(0 1 -1 0 16 -8)"/><path d="m11 1h2v6h-2z"/><rect height="6" rx="1" width="6" x="1" y="9"/><rect height="6" rx="1" width="6" x="9" y="9"/></g></svg>
|
||||
|
After Width: | Height: | Size: 375 B |
@@ -1,3 +1 @@
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="context-fill #4c4c4c" fill-opacity="context-fill-opacity" d="M12.9137931,3.0862069 L12.9137931,1.27586207 C12.9137931,0.84736528 12.5664278,0.5 12.137931,0.5 C11.7094342,0.5 11.362069,0.84736528 11.362069,1.27586207 L11.362069,1.27586207 L11.362069,3.0862069 L9.55172414,3.0862069 C9.12322735,3.0862069 8.77586207,3.43357218 8.77586207,3.86206897 C8.77586207,4.29056575 9.12322735,4.63793103 9.55172414,4.63793103 L11.362069,4.63793103 L11.362069,6.44827586 C11.362069,6.87677265 11.7094342,7.22413793 12.137931,7.22413793 L12.137931,7.22413793 C12.5664278,7.22413793 12.9137931,6.87677265 12.9137931,6.44827586 L12.9137931,6.44827586 L12.9137931,4.63793103 L14.7241379,4.63793103 C15.1526347,4.63793103 15.5,4.29056575 15.5,3.86206897 L15.5,3.86206897 C15.5,3.43357218 15.1526347,3.0862069 14.7241379,3.0862069 L14.7241379,3.0862069 L12.9137931,3.0862069 Z M0.5,9.76803178 C0.5,9.22007158 0.94118947,8.77586207 1.49216971,8.77586207 L6.23196822,8.77586207 C6.77992842,8.77586207 7.22413793,9.21705154 7.22413793,9.76803178 L7.22413793,14.5078303 C7.22413793,15.0557905 6.78294846,15.5 6.23196822,15.5 L1.49216971,15.5 C0.94420951,15.5 0.5,15.0588105 0.5,14.5078303 L0.5,9.76803178 Z M8.77586207,9.76803178 C8.77586207,9.22007158 9.21705154,8.77586207 9.76803178,8.77586207 L14.5078303,8.77586207 C15.0557905,8.77586207 15.5,9.21705154 15.5,9.76803178 L15.5,14.5078303 C15.5,15.0557905 15.0588105,15.5 14.5078303,15.5 L9.76803178,15.5 C9.22007158,15.5 8.77586207,15.0588105 8.77586207,14.5078303 L8.77586207,9.76803178 Z M0.5,1.49216971 C0.5,0.94420951 0.94118947,0.5 1.49216971,0.5 L6.23196822,0.5 C6.77992842,0.5 7.22413793,0.94118947 7.22413793,1.49216971 L7.22413793,6.23196822 C7.22413793,6.77992842 6.78294846,7.22413793 6.23196822,7.22413793 L1.49216971,7.22413793 C0.94420951,7.22413793 0.5,6.78294846 0.5,6.23196822 L0.5,1.49216971 Z"></path>
|
||||
</svg>
|
||||
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="rgba(12, 12, 13, 0.8)" fill-rule="evenodd"><rect height="6" rx="1" width="6" x="1" y="1"/><path d="m11 1h2v6h-2z" transform="matrix(0 1 -1 0 16 -8)"/><path d="m11 1h2v6h-2z"/><rect height="6" rx="1" width="6" x="1" y="9"/><rect height="6" rx="1" width="6" x="9" y="9"/></g></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 372 B |
@@ -7,6 +7,7 @@ module.exports = {
|
||||
"badge": true,
|
||||
"backgroundLogic": true,
|
||||
"identityState": true,
|
||||
"messageHandler": true
|
||||
"messageHandler": true,
|
||||
"sync": true
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,43 +1,57 @@
|
||||
const assignManager = {
|
||||
window.assignManager = {
|
||||
MENU_ASSIGN_ID: "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",
|
||||
|
||||
OPEN_IN_CONTAINER: "open-bookmark-in-container-tab",
|
||||
storageArea: {
|
||||
area: browser.storage.local,
|
||||
exemptedTabs: {},
|
||||
|
||||
getSiteStoreKey(pageUrl) {
|
||||
const url = new window.URL(pageUrl);
|
||||
getSiteStoreKey(pageUrlorUrlKey) {
|
||||
if (pageUrlorUrlKey.includes("siteContainerMap@@_")) return pageUrlorUrlKey;
|
||||
const url = new window.URL(pageUrlorUrlKey);
|
||||
const storagePrefix = "siteContainerMap@@_";
|
||||
return `${storagePrefix}${url.hostname}`;
|
||||
if (url.port === "80" || url.port === "443") {
|
||||
return `${storagePrefix}${url.hostname}`;
|
||||
} else {
|
||||
return `${storagePrefix}${url.hostname}${url.port}`;
|
||||
}
|
||||
},
|
||||
|
||||
setExempted(pageUrl, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
setExempted(pageUrlorUrlKey, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
if (!(siteStoreKey in this.exemptedTabs)) {
|
||||
this.exemptedTabs[siteStoreKey] = [];
|
||||
}
|
||||
this.exemptedTabs[siteStoreKey].push(tabId);
|
||||
},
|
||||
|
||||
removeExempted(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
removeExempted(pageUrlorUrlKey) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
this.exemptedTabs[siteStoreKey] = [];
|
||||
},
|
||||
|
||||
isExempted(pageUrl, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
isExempted(pageUrlorUrlKey, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
if (!(siteStoreKey in this.exemptedTabs)) {
|
||||
return false;
|
||||
}
|
||||
return this.exemptedTabs[siteStoreKey].includes(tabId);
|
||||
},
|
||||
|
||||
get(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
get(pageUrlorUrlKey) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
return this.getByUrlKey(siteStoreKey);
|
||||
},
|
||||
|
||||
async getSyncEnabled() {
|
||||
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
|
||||
return !!syncEnabled;
|
||||
},
|
||||
|
||||
getByUrlKey(siteStoreKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.area.get([siteStoreKey]).then((storageResponse) => {
|
||||
if (storageResponse && siteStoreKey in storageResponse) {
|
||||
@@ -50,51 +64,103 @@ const assignManager = {
|
||||
});
|
||||
},
|
||||
|
||||
set(pageUrl, data, exemptedTabIds) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
async set(pageUrlorUrlKey, data, exemptedTabIds, backup = true) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
if (exemptedTabIds) {
|
||||
exemptedTabIds.forEach((tabId) => {
|
||||
this.setExempted(pageUrl, tabId);
|
||||
this.setExempted(pageUrlorUrlKey, tabId);
|
||||
});
|
||||
}
|
||||
return this.area.set({
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
data.identityMacAddonUUID =
|
||||
await identityState.lookupMACaddonUUID(data.userContextId);
|
||||
await this.area.set({
|
||||
[siteStoreKey]: data
|
||||
});
|
||||
const syncEnabled = await this.getSyncEnabled();
|
||||
if (backup && syncEnabled) {
|
||||
await sync.storageArea.backup({undeleteSiteStoreKey: siteStoreKey});
|
||||
}
|
||||
return;
|
||||
},
|
||||
|
||||
remove(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
async remove(pageUrlorUrlKey) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
// When we remove an assignment we should clear all the exemptions
|
||||
this.removeExempted(pageUrl);
|
||||
return this.area.remove([siteStoreKey]);
|
||||
this.removeExempted(pageUrlorUrlKey);
|
||||
await this.area.remove([siteStoreKey]);
|
||||
const syncEnabled = await this.getSyncEnabled();
|
||||
if (syncEnabled) await sync.storageArea.backup({siteStoreKey});
|
||||
return;
|
||||
},
|
||||
|
||||
async deleteContainer(userContextId) {
|
||||
const sitesByContainer = await this.getByContainer(userContextId);
|
||||
const sitesByContainer = await this.getAssignedSites(userContextId);
|
||||
this.area.remove(Object.keys(sitesByContainer));
|
||||
},
|
||||
|
||||
async getByContainer(userContextId) {
|
||||
async getAssignedSites(userContextId = null) {
|
||||
const sites = {};
|
||||
const siteConfigs = await this.area.get();
|
||||
Object.keys(siteConfigs).forEach((key) => {
|
||||
// For some reason this is stored as string... lets check them both as that
|
||||
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
|
||||
const site = siteConfigs[key];
|
||||
for(const urlKey of Object.keys(siteConfigs)) {
|
||||
if (urlKey.includes("siteContainerMap@@_")) {
|
||||
// For some reason this is stored as string... lets check
|
||||
// them both as that
|
||||
if (!!userContextId &&
|
||||
String(siteConfigs[urlKey].userContextId)
|
||||
!== String(userContextId)) {
|
||||
continue;
|
||||
}
|
||||
const site = siteConfigs[urlKey];
|
||||
// In hindsight we should have stored this
|
||||
// TODO file a follow up to clean the storage onLoad
|
||||
site.hostname = key.replace(/^siteContainerMap@@_/, "");
|
||||
sites[key] = site;
|
||||
site.hostname = urlKey.replace(/^siteContainerMap@@_/, "");
|
||||
sites[urlKey] = site;
|
||||
}
|
||||
});
|
||||
}
|
||||
return sites;
|
||||
},
|
||||
|
||||
/*
|
||||
* Looks for abandoned site assignments. If there is no identity with
|
||||
* the site assignment's userContextId (cookieStoreId), then the assignment
|
||||
* is removed.
|
||||
*/
|
||||
async upgradeData() {
|
||||
const identitiesList = await browser.contextualIdentities.query({});
|
||||
const macConfigs = await this.area.get();
|
||||
for(const configKey of Object.keys(macConfigs)) {
|
||||
if (configKey.includes("siteContainerMap@@_")) {
|
||||
const cookieStoreId =
|
||||
"firefox-container-" + macConfigs[configKey].userContextId;
|
||||
const match = identitiesList.find(
|
||||
localIdentity => localIdentity.cookieStoreId === cookieStoreId
|
||||
);
|
||||
if (!match) {
|
||||
await this.remove(configKey);
|
||||
continue;
|
||||
}
|
||||
const updatedSiteAssignment = macConfigs[configKey];
|
||||
updatedSiteAssignment.identityMacAddonUUID =
|
||||
await identityState.lookupMACaddonUUID(match.cookieStoreId);
|
||||
await this.set(
|
||||
configKey,
|
||||
updatedSiteAssignment,
|
||||
false,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
_neverAsk(m) {
|
||||
const pageUrl = m.pageUrl;
|
||||
if (m.neverAsk === true) {
|
||||
// If we have existing data and for some reason it hasn't been deleted etc lets update it
|
||||
// If we have existing data and for some reason it hasn't been
|
||||
// deleted etc lets update it
|
||||
this.storageArea.get(pageUrl).then((siteSettings) => {
|
||||
if (siteSettings) {
|
||||
siteSettings.neverAsk = true;
|
||||
@@ -109,11 +175,12 @@ const assignManager = {
|
||||
// We return here so the confirm page can load the tab when exempted
|
||||
async _exemptTab(m) {
|
||||
const pageUrl = m.pageUrl;
|
||||
this.storageArea.setExempted(pageUrl, m.tabId);
|
||||
await this.storageArea.setExempted(pageUrl, m.tabId);
|
||||
return true;
|
||||
},
|
||||
|
||||
// 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
|
||||
async onBeforeRequest(options) {
|
||||
if (options.frameId !== 0 || options.tabId === -1) {
|
||||
return {};
|
||||
@@ -125,42 +192,106 @@ const assignManager = {
|
||||
]);
|
||||
let container;
|
||||
try {
|
||||
container = await browser.contextualIdentities.get(backgroundLogic.cookieStoreId(siteSettings.userContextId));
|
||||
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 (!container) {
|
||||
// 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;
|
||||
|
||||
this.reloadPageInContainer(options.url, userContextId, siteSettings.userContextId, tab.index + 1, tab.active, siteSettings.neverAsk);
|
||||
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.
|
||||
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)) {
|
||||
if (removeTab) {
|
||||
browser.tabs.remove(tab.id);
|
||||
}
|
||||
return {
|
||||
@@ -170,13 +301,84 @@ const assignManager = {
|
||||
|
||||
init() {
|
||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||
this._onClickedHandler(info, tab);
|
||||
info.bookmarkId ?
|
||||
this._onClickedBookmark(info) :
|
||||
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) => {
|
||||
return this.onBeforeRequest(options);
|
||||
},{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"]});
|
||||
|
||||
this.resetBookmarksMenuItem();
|
||||
},
|
||||
|
||||
async resetBookmarksMenuItem() {
|
||||
const hasPermission = await browser.permissions.contains({
|
||||
permissions: ["bookmarks"]
|
||||
});
|
||||
if (this.hadBookmark === hasPermission) {
|
||||
return;
|
||||
}
|
||||
this.hadBookmark = hasPermission;
|
||||
if (hasPermission) {
|
||||
this.initBookmarksMenu();
|
||||
browser.contextualIdentities.onCreated
|
||||
.addListener(this.contextualIdentityCreated);
|
||||
browser.contextualIdentities.onUpdated
|
||||
.addListener(this.contextualIdentityUpdated);
|
||||
browser.contextualIdentities.onRemoved
|
||||
.addListener(this.contextualIdentityRemoved);
|
||||
} else {
|
||||
this.removeBookmarksMenu();
|
||||
browser.contextualIdentities.onCreated
|
||||
.removeListener(this.contextualIdentityCreated);
|
||||
browser.contextualIdentities.onUpdated
|
||||
.removeListener(this.contextualIdentityUpdated);
|
||||
browser.contextualIdentities.onRemoved
|
||||
.removeListener(this.contextualIdentityRemoved);
|
||||
}
|
||||
},
|
||||
|
||||
contextualIdentityCreated(changeInfo) {
|
||||
browser.contextMenus.create({
|
||||
parentId: assignManager.OPEN_IN_CONTAINER,
|
||||
id: changeInfo.contextualIdentity.cookieStoreId,
|
||||
title: changeInfo.contextualIdentity.name,
|
||||
icons: { "16": `img/usercontext.svg#${
|
||||
changeInfo.contextualIdentity.icon
|
||||
}` }
|
||||
});
|
||||
},
|
||||
|
||||
contextualIdentityUpdated(changeInfo) {
|
||||
browser.contextMenus.update(
|
||||
changeInfo.contextualIdentity.cookieStoreId, {
|
||||
title: changeInfo.contextualIdentity.name,
|
||||
icons: { "16": `img/usercontext.svg#${
|
||||
changeInfo.contextualIdentity.icon}` }
|
||||
});
|
||||
},
|
||||
|
||||
contextualIdentityRemoved(changeInfo) {
|
||||
browser.contextMenus.remove(
|
||||
changeInfo.contextualIdentity.cookieStoreId
|
||||
);
|
||||
},
|
||||
|
||||
async _onClickedHandler(info, tab) {
|
||||
@@ -192,7 +394,9 @@ const assignManager = {
|
||||
} else {
|
||||
remove = true;
|
||||
}
|
||||
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove);
|
||||
await this._setOrRemoveAssignment(
|
||||
tab.id, info.pageUrl, userContextId, remove
|
||||
);
|
||||
break;
|
||||
case this.MENU_MOVE_ID:
|
||||
backgroundLogic.moveTabsToWindow({
|
||||
@@ -210,6 +414,41 @@ const assignManager = {
|
||||
}
|
||||
},
|
||||
|
||||
async _onClickedBookmark(info) {
|
||||
|
||||
async function _getBookmarksFromInfo(info) {
|
||||
const [bookmarkTreeNode] =
|
||||
await browser.bookmarks.get(info.bookmarkId);
|
||||
if (bookmarkTreeNode.type === "folder") {
|
||||
return browser.bookmarks.getChildren(bookmarkTreeNode.id);
|
||||
}
|
||||
return [bookmarkTreeNode];
|
||||
}
|
||||
|
||||
const bookmarks = await _getBookmarksFromInfo(info);
|
||||
for (const bookmark of bookmarks) {
|
||||
// Some checks on the urls from
|
||||
// https://github.com/Rob--W/bookmark-container-tab/ thanks!
|
||||
if ( !/^(javascript|place):/i.test(bookmark.url) &&
|
||||
bookmark.type !== "folder") {
|
||||
const openInReaderMode = bookmark.url.startsWith("about:reader");
|
||||
if(openInReaderMode) {
|
||||
try {
|
||||
const parsed = new URL(bookmark.url);
|
||||
bookmark.url = parsed.searchParams.get("url") + parsed.hash;
|
||||
} catch (err) {
|
||||
return err.message;
|
||||
}
|
||||
}
|
||||
browser.tabs.create({
|
||||
cookieStoreId: info.menuItemId,
|
||||
url: bookmark.url,
|
||||
openInReaderMode: openInReaderMode
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
deleteContainer(userContextId) {
|
||||
this.storageArea.deleteContainer(userContextId);
|
||||
@@ -219,16 +458,16 @@ const assignManager = {
|
||||
if (!("cookieStoreId" in tab)) {
|
||||
return false;
|
||||
}
|
||||
return backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId);
|
||||
return backgroundLogic.getUserContextIdFromCookieStoreId(
|
||||
tab.cookieStoreId
|
||||
);
|
||||
},
|
||||
|
||||
isTabPermittedAssign(tab) {
|
||||
// Ensure we are not an important about url
|
||||
// Ensure we are not in incognito mode
|
||||
const url = new URL(tab.url);
|
||||
if (url.protocol === "about:"
|
||||
|| url.protocol === "moz-extension:"
|
||||
|| tab.incognito) {
|
||||
|| url.protocol === "moz-extension:") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -278,13 +517,13 @@ const assignManager = {
|
||||
// Ensure we have a cookieStore to assign to
|
||||
if (cookieStore
|
||||
&& this.isTabPermittedAssign(tab)) {
|
||||
return await this.storageArea.get(tab.url);
|
||||
return this.storageArea.get(tab.url);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_getByContainer(userContextId) {
|
||||
return this.storageArea.getByContainer(userContextId);
|
||||
return this.storageArea.getAssignedSites(userContextId);
|
||||
},
|
||||
|
||||
removeContextMenu() {
|
||||
@@ -350,13 +589,13 @@ const assignManager = {
|
||||
});
|
||||
},
|
||||
|
||||
reloadPageInContainer(url, currentUserContextId, userContextId, index, active, neverAsk = false) {
|
||||
reloadPageInContainer(url, currentUserContextId, userContextId, index, active, neverAsk = false, openerTabId = null) {
|
||||
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
|
||||
const loadPage = browser.extension.getURL("confirm-page.html");
|
||||
// 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 (neverAsk) {
|
||||
browser.tabs.create({url, cookieStoreId, index, active});
|
||||
browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
|
||||
} else {
|
||||
let confirmUrl = `${loadPage}?url=${this.encodeURLProperty(url)}&cookieStoreId=${cookieStoreId}`;
|
||||
let currentCookieStoreId;
|
||||
@@ -367,6 +606,7 @@ const assignManager = {
|
||||
browser.tabs.create({
|
||||
url: confirmUrl,
|
||||
cookieStoreId: currentCookieStoreId,
|
||||
openerTabId,
|
||||
index,
|
||||
active
|
||||
}).then(() => {
|
||||
@@ -376,7 +616,33 @@ const assignManager = {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async initBookmarksMenu() {
|
||||
browser.contextMenus.create({
|
||||
id: this.OPEN_IN_CONTAINER,
|
||||
title: "Open Bookmark in Container Tab",
|
||||
contexts: ["bookmark"],
|
||||
});
|
||||
|
||||
const identities = await browser.contextualIdentities.query({});
|
||||
for (const identity of identities) {
|
||||
browser.contextMenus.create({
|
||||
parentId: this.OPEN_IN_CONTAINER,
|
||||
id: identity.cookieStoreId,
|
||||
title: identity.name,
|
||||
icons: { "16": `img/usercontext.svg#${identity.icon}` }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async removeBookmarksMenu() {
|
||||
browser.contextMenus.remove(this.OPEN_IN_CONTAINER);
|
||||
const identities = await browser.contextualIdentities.query({});
|
||||
for (const identity of identities) {
|
||||
browser.contextMenus.remove(identity.cookieStoreId);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
assignManager.init();
|
||||
|
||||
@@ -6,6 +6,7 @@ const backgroundLogic = {
|
||||
"about:home",
|
||||
"about:blank"
|
||||
]),
|
||||
unhideQueue: [],
|
||||
|
||||
async getExtensionInfo() {
|
||||
const manifestPath = browser.extension.getURL("manifest.json");
|
||||
@@ -45,15 +46,13 @@ const backgroundLogic = {
|
||||
donePromise = browser.contextualIdentities.create(options.params);
|
||||
}
|
||||
await donePromise;
|
||||
browser.runtime.sendMessage({
|
||||
method: "refreshNeeded"
|
||||
});
|
||||
},
|
||||
|
||||
async openNewTab(options) {
|
||||
let url = options.url || undefined;
|
||||
const userContextId = ("userContextId" in options) ? options.userContextId : 0;
|
||||
const active = ("nofocus" in options) ? options.nofocus : true;
|
||||
const discarded = ("noload" in options) ? options.noload : false;
|
||||
|
||||
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
|
||||
// Autofocus url bar will happen in 54: https://bugzilla.mozilla.org/show_bug.cgi?id=1295072
|
||||
@@ -70,6 +69,7 @@ const backgroundLogic = {
|
||||
return browser.tabs.create({
|
||||
url,
|
||||
active,
|
||||
discarded,
|
||||
pinned: options.pinned || false,
|
||||
cookieStoreId
|
||||
});
|
||||
@@ -112,6 +112,18 @@ const backgroundLogic = {
|
||||
return list.concat(containerState.hiddenTabs);
|
||||
},
|
||||
|
||||
async unhideContainer(cookieStoreId, alreadyShowingUrl) {
|
||||
if (!this.unhideQueue.includes(cookieStoreId)) {
|
||||
this.unhideQueue.push(cookieStoreId);
|
||||
await this.showTabs({
|
||||
cookieStoreId,
|
||||
alreadyShowingUrl
|
||||
});
|
||||
this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async moveTabsToWindow(options) {
|
||||
const requiredArguments = ["cookieStoreId", "windowId"];
|
||||
this.checkArgs(requiredArguments, options, "moveTabsToWindow");
|
||||
@@ -123,6 +135,7 @@ const backgroundLogic = {
|
||||
});
|
||||
|
||||
const containerState = await identityState.storageArea.get(cookieStoreId);
|
||||
|
||||
// Nothing to do
|
||||
if (list.length === 0 &&
|
||||
containerState.hiddenTabs.length === 0) {
|
||||
@@ -131,9 +144,13 @@ const backgroundLogic = {
|
||||
let newWindowObj;
|
||||
let hiddenDefaultTabToClose;
|
||||
if (list.length) {
|
||||
newWindowObj = await browser.windows.create({
|
||||
tabId: list.shift().id
|
||||
});
|
||||
newWindowObj = await browser.windows.create();
|
||||
|
||||
// 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
|
||||
@@ -148,12 +165,15 @@ const backgroundLogic = {
|
||||
const showHiddenPromises = [];
|
||||
|
||||
// Let's show the hidden tabs.
|
||||
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
||||
showHiddenPromises.push(browser.tabs.create({
|
||||
url: object.url || DEFAULT_TAB,
|
||||
windowId: newWindowObj.id,
|
||||
cookieStoreId
|
||||
}));
|
||||
if (!this.unhideQueue.includes(cookieStoreId)) {
|
||||
this.unhideQueue.push(cookieStoreId);
|
||||
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
||||
showHiddenPromises.push(browser.tabs.create({
|
||||
url: object.url || DEFAULT_TAB,
|
||||
windowId: newWindowObj.id,
|
||||
cookieStoreId
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (hiddenDefaultTabToClose) {
|
||||
@@ -172,7 +192,9 @@ const backgroundLogic = {
|
||||
browser.tabs.remove(tab.id);
|
||||
}
|
||||
}
|
||||
return await identityState.storageArea.set(cookieStoreId, containerState);
|
||||
const rv = await identityState.storageArea.set(cookieStoreId, containerState);
|
||||
this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1);
|
||||
return rv;
|
||||
},
|
||||
|
||||
async _closeTabs(userContextId, windowId = false) {
|
||||
@@ -196,7 +218,7 @@ const backgroundLogic = {
|
||||
async queryIdentitiesState(windowId) {
|
||||
const identities = await browser.contextualIdentities.query({});
|
||||
const identitiesOutput = {};
|
||||
const identitiesPromise = identities.map(async function (identity) {
|
||||
const identitiesPromise = identities.map(async (identity) => {
|
||||
const { cookieStoreId } = identity;
|
||||
const containerState = await identityState.storageArea.get(cookieStoreId);
|
||||
const openTabs = await browser.tabs.query({
|
||||
@@ -205,7 +227,9 @@ const backgroundLogic = {
|
||||
});
|
||||
identitiesOutput[cookieStoreId] = {
|
||||
hasHiddenTabs: !!containerState.hiddenTabs.length,
|
||||
hasOpenTabs: !!openTabs.length
|
||||
hasOpenTabs: !!openTabs.length,
|
||||
numberOfHiddenTabs: containerState.hiddenTabs.length,
|
||||
numberOfOpenTabs: openTabs.length
|
||||
};
|
||||
return;
|
||||
});
|
||||
@@ -285,22 +309,26 @@ const backgroundLogic = {
|
||||
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
||||
|
||||
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
||||
promises.push(this.openNewTab({
|
||||
userContextId: userContextId,
|
||||
url: object.url,
|
||||
nofocus: options.nofocus || false,
|
||||
pinned: object.pinned,
|
||||
}));
|
||||
// do not show already opened url
|
||||
const noload = !object.pinned;
|
||||
if (object.url !== options.alreadyShowingUrl) {
|
||||
promises.push(this.openNewTab({
|
||||
userContextId: userContextId,
|
||||
url: object.url,
|
||||
nofocus: options.nofocus || false,
|
||||
noload: noload,
|
||||
pinned: object.pinned,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
containerState.hiddenTabs = [];
|
||||
|
||||
await Promise.all(promises);
|
||||
return await identityState.storageArea.set(options.cookieStoreId, containerState);
|
||||
return identityState.storageArea.set(options.cookieStoreId, containerState);
|
||||
},
|
||||
|
||||
cookieStoreId(userContextId) {
|
||||
return `firefox-container-${userContextId}`;
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
@@ -1,23 +1,18 @@
|
||||
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
|
||||
const MAJOR_VERSIONS = ["2.3.0", "2.4.0", "6.2.0"];
|
||||
const badge = {
|
||||
async init() {
|
||||
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" });
|
||||
this.displayBrowserActionBadge(currentWindow);
|
||||
},
|
||||
|
||||
async displayBrowserActionBadge() {
|
||||
const extensionInfo = await backgroundLogic.getExtensionInfo();
|
||||
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
||||
const storage = await browser.storage.local.get({ browserActionBadgesClicked: [] });
|
||||
|
||||
if (MAJOR_VERSIONS.indexOf(extensionInfo.version) > -1 &&
|
||||
storage.browserActionBadgesClicked.indexOf(extensionInfo.version) < 0) {
|
||||
browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"});
|
||||
browser.browserAction.setBadgeText({text: "NEW"});
|
||||
storage.browserActionBadgesClicked.indexOf(extensionInfo.version) < 0) {
|
||||
browser.browserAction.setBadgeBackgroundColor({ color: "rgba(0,217,0,255)" });
|
||||
browser.browserAction.setBadgeText({ text: "NEW" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const identityState = {
|
||||
window.identityState = {
|
||||
storageArea: {
|
||||
area: browser.storage.local,
|
||||
|
||||
@@ -11,12 +11,23 @@ const identityState = {
|
||||
const storeKey = this.getContainerStoreKey(cookieStoreId);
|
||||
const storageResponse = await this.area.get([storeKey]);
|
||||
if (storageResponse && storeKey in storageResponse) {
|
||||
if (!storageResponse[storeKey].macAddonUUID){
|
||||
storageResponse[storeKey].macAddonUUID = uuidv4();
|
||||
await this.set(cookieStoreId, storageResponse[storeKey]);
|
||||
}
|
||||
return storageResponse[storeKey];
|
||||
}
|
||||
const defaultContainerState = identityState._createIdentityState();
|
||||
await this.set(cookieStoreId, defaultContainerState);
|
||||
|
||||
return defaultContainerState;
|
||||
// If local storage doesn't have an entry, look it up to make sure it's
|
||||
// an in-use identity.
|
||||
const identities = await browser.contextualIdentities.query({});
|
||||
const match = identities.find(
|
||||
(identity) => identity.cookieStoreId === cookieStoreId);
|
||||
if (match) {
|
||||
const defaultContainerState = identityState._createIdentityState();
|
||||
await this.set(cookieStoreId, defaultContainerState);
|
||||
return defaultContainerState;
|
||||
}
|
||||
throw new Error (`${cookieStoreId} not found`);
|
||||
},
|
||||
|
||||
set(cookieStoreId, data) {
|
||||
@@ -26,9 +37,41 @@ const identityState = {
|
||||
});
|
||||
},
|
||||
|
||||
remove(cookieStoreId) {
|
||||
async remove(cookieStoreId) {
|
||||
const storeKey = this.getContainerStoreKey(cookieStoreId);
|
||||
return this.area.remove([storeKey]);
|
||||
},
|
||||
|
||||
/*
|
||||
* Looks for abandoned identity keys in local storage, and makes sure all
|
||||
* identities registered in the browser are also in local storage. (this
|
||||
* appears to not always be the case based on how this.get() is written)
|
||||
*/
|
||||
async upgradeData() {
|
||||
const identitiesList = await browser.contextualIdentities.query({});
|
||||
|
||||
for (const identity of identitiesList) {
|
||||
// ensure all identities have an entry in local storage
|
||||
await identityState.addUUID(identity.cookieStoreId);
|
||||
}
|
||||
|
||||
const macConfigs = await this.area.get();
|
||||
for(const configKey of Object.keys(macConfigs)) {
|
||||
if (configKey.includes("identitiesState@@_")) {
|
||||
const cookieStoreId = String(configKey).replace(/^identitiesState@@_/, "");
|
||||
const match = identitiesList.find(
|
||||
localIdentity => localIdentity.cookieStoreId === cookieStoreId
|
||||
);
|
||||
if (cookieStoreId === "firefox-default") continue;
|
||||
if (!match) {
|
||||
await this.remove(cookieStoreId);
|
||||
continue;
|
||||
}
|
||||
if (!macConfigs[configKey].macAddonUUID) {
|
||||
await identityState.storageArea.get(cookieStoreId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -36,6 +79,16 @@ const identityState = {
|
||||
return Object.assign({}, tab);
|
||||
},
|
||||
|
||||
async getCookieStoreIDuuidMap() {
|
||||
const containers = {};
|
||||
const identities = await browser.contextualIdentities.query({});
|
||||
for(const identity of identities) {
|
||||
const containerInfo = await this.storageArea.get(identity.cookieStoreId);
|
||||
containers[identity.cookieStoreId] = containerInfo.macAddonUUID;
|
||||
}
|
||||
return containers;
|
||||
},
|
||||
|
||||
async storeHidden(cookieStoreId, windowId) {
|
||||
const containerState = await this.storageArea.get(cookieStoreId);
|
||||
const tabsByContainer = await browser.tabs.query({cookieStoreId, windowId});
|
||||
@@ -54,9 +107,57 @@ const identityState = {
|
||||
return this.storageArea.set(cookieStoreId, containerState);
|
||||
},
|
||||
|
||||
async updateUUID(cookieStoreId, uuid) {
|
||||
if (!cookieStoreId || !uuid) {
|
||||
throw new Error ("cookieStoreId or uuid missing");
|
||||
}
|
||||
const containerState = await this.storageArea.get(cookieStoreId);
|
||||
containerState.macAddonUUID = uuid;
|
||||
await this.storageArea.set(cookieStoreId, containerState);
|
||||
return uuid;
|
||||
},
|
||||
|
||||
async addUUID(cookieStoreId) {
|
||||
await this.storageArea.get(cookieStoreId);
|
||||
},
|
||||
|
||||
async lookupMACaddonUUID(cookieStoreId) {
|
||||
// This stays a lookup, because if the cookieStoreId doesn't
|
||||
// exist, this.get() will create it, which is not what we want.
|
||||
const cookieStoreIdKey = cookieStoreId.includes("firefox-container-") ?
|
||||
cookieStoreId : "firefox-container-" + cookieStoreId;
|
||||
const macConfigs = await this.storageArea.area.get();
|
||||
for(const configKey of Object.keys(macConfigs)) {
|
||||
if (configKey === this.storageArea.getContainerStoreKey(cookieStoreIdKey)) {
|
||||
return macConfigs[configKey].macAddonUUID;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
async lookupCookieStoreId(macAddonUUID) {
|
||||
const macConfigs = await this.storageArea.area.get();
|
||||
for(const configKey of Object.keys(macConfigs)) {
|
||||
if (configKey.includes("identitiesState@@_")) {
|
||||
if(macConfigs[configKey].macAddonUUID === macAddonUUID) {
|
||||
return String(configKey).replace(/^identitiesState@@_/, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_createIdentityState() {
|
||||
return {
|
||||
hiddenTabs: []
|
||||
hiddenTabs: [],
|
||||
macAddonUUID: uuidv4()
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function uuidv4() {
|
||||
// https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,5 +18,6 @@
|
||||
<script type="text/javascript" src="badge.js"></script>
|
||||
<script type="text/javascript" src="identityState.js"></script>
|
||||
<script type="text/javascript" src="messageHandler.js"></script>
|
||||
<script type="text/javascript" src="sync.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,6 @@ const messageHandler = {
|
||||
// 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
|
||||
@@ -11,6 +10,12 @@ const messageHandler = {
|
||||
let response;
|
||||
|
||||
switch (m.method) {
|
||||
case "resetSync":
|
||||
response = sync.resetSync();
|
||||
break;
|
||||
case "resetBookmarksContext":
|
||||
response = assignManager.resetBookmarksMenuItem();
|
||||
break;
|
||||
case "deleteContainer":
|
||||
response = backgroundLogic.deleteContainer(m.message.userContextId);
|
||||
break;
|
||||
@@ -39,7 +44,7 @@ const messageHandler = {
|
||||
backgroundLogic.sortTabs();
|
||||
break;
|
||||
case "showTabs":
|
||||
this.unhideContainer(m.cookieStoreId);
|
||||
backgroundLogic.unhideContainer(m.cookieStoreId);
|
||||
break;
|
||||
case "hideTabs":
|
||||
backgroundLogic.hideTabs({
|
||||
@@ -72,6 +77,43 @@ const messageHandler = {
|
||||
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");
|
||||
}
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
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);
|
||||
@@ -106,9 +148,6 @@ const messageHandler = {
|
||||
}, {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) {
|
||||
@@ -118,9 +157,26 @@ const messageHandler = {
|
||||
!tab.url.startsWith("moz-extension")) {
|
||||
// increment the counter of container tabs opened
|
||||
this.incrementCountOfContainerTabsOpened();
|
||||
}
|
||||
|
||||
this.unhideContainer(tab.cookieStoreId);
|
||||
this.tabUpdateHandler = (tabId, changeInfo) => {
|
||||
if (tabId === tab.id && changeInfo.status === "complete") {
|
||||
// get current tab's url to not open the same one from hidden tabs
|
||||
browser.tabs.get(tabId).then(loadedTab => {
|
||||
backgroundLogic.unhideContainer(tab.cookieStoreId, loadedTab.url);
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
|
||||
browser.tabs.onUpdated.removeListener(this.tabUpdateHandler);
|
||||
}
|
||||
};
|
||||
|
||||
// if it's a container tab wait for it to complete and
|
||||
// unhide other tabs from this container
|
||||
if (tab.cookieStoreId.startsWith("firefox-container")) {
|
||||
browser.tabs.onUpdated.addListener(this.tabUpdateHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.lastCreatedTab = null;
|
||||
@@ -146,17 +202,6 @@ const messageHandler = {
|
||||
}
|
||||
},
|
||||
|
||||
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 ...
|
||||
|
||||
@@ -0,0 +1,580 @@
|
||||
const SYNC_DEBUG = false;
|
||||
|
||||
const sync = {
|
||||
storageArea: {
|
||||
area: browser.storage.sync,
|
||||
|
||||
async get(){
|
||||
return this.area.get();
|
||||
},
|
||||
|
||||
async set(options) {
|
||||
return this.area.set(options);
|
||||
},
|
||||
|
||||
async deleteIdentity(deletedIdentityUUID) {
|
||||
const deletedIdentityList =
|
||||
await sync.storageArea.getDeletedIdentityList();
|
||||
if (
|
||||
! deletedIdentityList.find(element => element === deletedIdentityUUID)
|
||||
) {
|
||||
deletedIdentityList.push(deletedIdentityUUID);
|
||||
await sync.storageArea.set({ deletedIdentityList });
|
||||
}
|
||||
await this.removeIdentityKeyFromSync(deletedIdentityUUID);
|
||||
},
|
||||
|
||||
async removeIdentityKeyFromSync(deletedIdentityUUID) {
|
||||
await sync.storageArea.area.remove( "identity@@_" + deletedIdentityUUID);
|
||||
},
|
||||
|
||||
async deleteSite(siteStoreKey) {
|
||||
const deletedSiteList =
|
||||
await sync.storageArea.getDeletedSiteList();
|
||||
if (deletedSiteList.find(element => element === siteStoreKey)) return;
|
||||
deletedSiteList.push(siteStoreKey);
|
||||
await sync.storageArea.set({ deletedSiteList });
|
||||
await sync.storageArea.area.remove(siteStoreKey);
|
||||
},
|
||||
|
||||
async getDeletedIdentityList() {
|
||||
const storedArray = await this.getStoredItem("deletedIdentityList");
|
||||
return storedArray || [];
|
||||
},
|
||||
|
||||
async getIdentities() {
|
||||
const allSyncStorage = await this.get();
|
||||
const identities = [];
|
||||
for (const storageKey of Object.keys(allSyncStorage)) {
|
||||
if (storageKey.includes("identity@@_")) {
|
||||
identities.push(allSyncStorage[storageKey]);
|
||||
}
|
||||
}
|
||||
return identities;
|
||||
},
|
||||
|
||||
async getDeletedSiteList() {
|
||||
const storedArray = await this.getStoredItem("deletedSiteList");
|
||||
return (storedArray) ? storedArray : [];
|
||||
},
|
||||
|
||||
async getAssignedSites() {
|
||||
const allSyncStorage = await this.get();
|
||||
const sites = {};
|
||||
for (const storageKey of Object.keys(allSyncStorage)) {
|
||||
if (storageKey.includes("siteContainerMap@@_")) {
|
||||
sites[storageKey] = allSyncStorage[storageKey];
|
||||
}
|
||||
}
|
||||
return sites;
|
||||
},
|
||||
|
||||
async getStoredItem(objectKey) {
|
||||
const outputObject = await this.get(objectKey);
|
||||
if (outputObject && outputObject[objectKey])
|
||||
return outputObject[objectKey];
|
||||
return false;
|
||||
},
|
||||
|
||||
async getAllInstanceInfo() {
|
||||
const instanceList = {};
|
||||
const allSyncInfo = await this.get();
|
||||
for (const objectKey of Object.keys(allSyncInfo)) {
|
||||
if (objectKey.includes("MACinstance")) {
|
||||
instanceList[objectKey] = allSyncInfo[objectKey]; }
|
||||
}
|
||||
return instanceList;
|
||||
},
|
||||
|
||||
getInstanceKey() {
|
||||
return browser.runtime.getURL("")
|
||||
.replace(/moz-extension:\/\//, "MACinstance:")
|
||||
.replace(/\//, "");
|
||||
},
|
||||
async removeInstance(installUUID) {
|
||||
if (SYNC_DEBUG) console.log("removing", installUUID);
|
||||
await this.area.remove(installUUID);
|
||||
return;
|
||||
},
|
||||
|
||||
async removeThisInstanceFromSync() {
|
||||
const installUUID = this.getInstanceKey();
|
||||
await this.removeInstance(installUUID);
|
||||
return;
|
||||
},
|
||||
|
||||
async hasSyncStorage(){
|
||||
const inSync = await this.get();
|
||||
return !(Object.entries(inSync).length === 0);
|
||||
},
|
||||
|
||||
async backup(options) {
|
||||
// remove listeners to avoid an infinite loop!
|
||||
await sync.checkForListenersMaybeRemove();
|
||||
|
||||
const identities = await updateSyncIdentities();
|
||||
const siteAssignments = await updateSyncSiteAssignments();
|
||||
await updateInstanceInfo(identities, siteAssignments);
|
||||
if (options && options.uuid)
|
||||
await this.deleteIdentity(options.uuid);
|
||||
if (options && options.undeleteUUID)
|
||||
await removeFromDeletedIdentityList(options.undeleteUUID);
|
||||
if (options && options.siteStoreKey)
|
||||
await this.deleteSite(options.siteStoreKey);
|
||||
if (options && options.undeleteSiteStoreKey)
|
||||
await removeFromDeletedSitesList(options.undeleteSiteStoreKey);
|
||||
|
||||
if (SYNC_DEBUG) console.log("Backed up!");
|
||||
await sync.checkForListenersMaybeAdd();
|
||||
|
||||
async function updateSyncIdentities() {
|
||||
const identities = await browser.contextualIdentities.query({});
|
||||
|
||||
for (const identity of identities) {
|
||||
delete identity.colorCode;
|
||||
delete identity.iconUrl;
|
||||
identity.macAddonUUID = await identityState.lookupMACaddonUUID(identity.cookieStoreId);
|
||||
if(identity.macAddonUUID) {
|
||||
const storageKey = "identity@@_" + identity.macAddonUUID;
|
||||
await sync.storageArea.set({ [storageKey]: identity });
|
||||
}
|
||||
}
|
||||
//await sync.storageArea.set({ identities });
|
||||
return identities;
|
||||
}
|
||||
|
||||
async function updateSyncSiteAssignments() {
|
||||
const assignedSites =
|
||||
await assignManager.storageArea.getAssignedSites();
|
||||
for (const siteKey of Object.keys(assignedSites)) {
|
||||
await sync.storageArea.set({ [siteKey]: assignedSites[siteKey] });
|
||||
}
|
||||
return assignedSites;
|
||||
}
|
||||
|
||||
async function updateInstanceInfo(identitiesInput, siteAssignmentsInput) {
|
||||
const date = new Date();
|
||||
const timestamp = date.getTime();
|
||||
const installUUID = sync.storageArea.getInstanceKey();
|
||||
if (SYNC_DEBUG) console.log("adding", installUUID);
|
||||
const identities = [];
|
||||
const siteAssignments = [];
|
||||
for (const identity of identitiesInput) {
|
||||
identities.push(identity.macAddonUUID);
|
||||
}
|
||||
for (const siteAssignmentKey of Object.keys(siteAssignmentsInput)) {
|
||||
siteAssignments.push(siteAssignmentKey);
|
||||
}
|
||||
await sync.storageArea.set({ [installUUID]: { timestamp, identities, siteAssignments } });
|
||||
}
|
||||
|
||||
async function removeFromDeletedIdentityList(identityUUID) {
|
||||
const deletedIdentityList =
|
||||
await sync.storageArea.getDeletedIdentityList();
|
||||
const newDeletedIdentityList = deletedIdentityList
|
||||
.filter(element => element !== identityUUID);
|
||||
await sync.storageArea.set({ deletedIdentityList: newDeletedIdentityList });
|
||||
}
|
||||
|
||||
async function removeFromDeletedSitesList(siteStoreKey) {
|
||||
const deletedSiteList =
|
||||
await sync.storageArea.getDeletedSiteList();
|
||||
const newDeletedSiteList = deletedSiteList
|
||||
.filter(element => element !== siteStoreKey);
|
||||
await sync.storageArea.set({ deletedSiteList: newDeletedSiteList });
|
||||
}
|
||||
},
|
||||
|
||||
onChangedListener(changes, areaName) {
|
||||
if (areaName === "sync") sync.errorHandledRunSync();
|
||||
},
|
||||
|
||||
async addToDeletedList(changeInfo) {
|
||||
const identity = changeInfo.contextualIdentity;
|
||||
const deletedUUID =
|
||||
await identityState.lookupMACaddonUUID(identity.cookieStoreId);
|
||||
await identityState.storageArea.remove(identity.cookieStoreId);
|
||||
sync.storageArea.backup({uuid: deletedUUID});
|
||||
}
|
||||
},
|
||||
|
||||
async init() {
|
||||
const syncEnabled = await assignManager.storageArea.getSyncEnabled();
|
||||
if (syncEnabled) {
|
||||
// Add listener to sync storage and containers.
|
||||
// Works for all installs that have any sync storage.
|
||||
// Waits for sync storage change before kicking off the restore/backup
|
||||
// initial sync must be kicked off by user.
|
||||
this.checkForListenersMaybeAdd();
|
||||
return;
|
||||
}
|
||||
this.checkForListenersMaybeRemove();
|
||||
|
||||
},
|
||||
|
||||
async errorHandledRunSync () {
|
||||
await sync.runSync().catch( async (error)=> {
|
||||
if (SYNC_DEBUG) console.error("Error from runSync", error);
|
||||
await sync.checkForListenersMaybeAdd();
|
||||
});
|
||||
},
|
||||
|
||||
async checkForListenersMaybeAdd() {
|
||||
const hasStorageListener =
|
||||
await browser.storage.onChanged.hasListener(
|
||||
sync.storageArea.onChangedListener
|
||||
);
|
||||
|
||||
const hasCIListener = await sync.hasContextualIdentityListeners();
|
||||
|
||||
if (!hasCIListener) {
|
||||
await sync.addContextualIdentityListeners();
|
||||
}
|
||||
|
||||
if (!hasStorageListener) {
|
||||
await browser.storage.onChanged.addListener(
|
||||
sync.storageArea.onChangedListener);
|
||||
}
|
||||
},
|
||||
|
||||
async checkForListenersMaybeRemove() {
|
||||
const hasStorageListener =
|
||||
await browser.storage.onChanged.hasListener(
|
||||
sync.storageArea.onChangedListener
|
||||
);
|
||||
|
||||
const hasCIListener = await sync.hasContextualIdentityListeners();
|
||||
|
||||
if (hasCIListener) {
|
||||
await sync.removeContextualIdentityListeners();
|
||||
}
|
||||
|
||||
if (hasStorageListener) {
|
||||
await browser.storage.onChanged.removeListener(
|
||||
sync.storageArea.onChangedListener);
|
||||
}
|
||||
},
|
||||
|
||||
async runSync() {
|
||||
if (SYNC_DEBUG) {
|
||||
const syncInfo = await sync.storageArea.get();
|
||||
const localInfo = await browser.storage.local.get();
|
||||
const idents = await browser.contextualIdentities.query({});
|
||||
console.log("Initial State:", {syncInfo, localInfo, idents});
|
||||
}
|
||||
await sync.checkForListenersMaybeRemove();
|
||||
if (SYNC_DEBUG) console.log("runSync");
|
||||
|
||||
await identityState.storageArea.upgradeData();
|
||||
await assignManager.storageArea.upgradeData();
|
||||
|
||||
const hasSyncStorage = await sync.storageArea.hasSyncStorage();
|
||||
if (hasSyncStorage) await restore();
|
||||
|
||||
await sync.storageArea.backup();
|
||||
await removeOldDeletedItems();
|
||||
return;
|
||||
},
|
||||
|
||||
async addContextualIdentityListeners() {
|
||||
await browser.contextualIdentities.onCreated.addListener(sync.storageArea.backup);
|
||||
await browser.contextualIdentities.onRemoved.addListener(sync.storageArea.addToDeletedList);
|
||||
await browser.contextualIdentities.onUpdated.addListener(sync.storageArea.backup);
|
||||
},
|
||||
|
||||
async removeContextualIdentityListeners() {
|
||||
await browser.contextualIdentities.onCreated.removeListener(sync.storageArea.backup);
|
||||
await browser.contextualIdentities.onRemoved.removeListener(sync.storageArea.addToDeletedList);
|
||||
await browser.contextualIdentities.onUpdated.removeListener(sync.storageArea.backup);
|
||||
},
|
||||
|
||||
async hasContextualIdentityListeners() {
|
||||
return (
|
||||
await browser.contextualIdentities.onCreated.hasListener(sync.storageArea.backup) &&
|
||||
await browser.contextualIdentities.onRemoved.hasListener(sync.storageArea.addToDeletedList) &&
|
||||
await browser.contextualIdentities.onUpdated.hasListener(sync.storageArea.backup)
|
||||
);
|
||||
},
|
||||
|
||||
async resetSync() {
|
||||
const syncEnabled = await assignManager.storageArea.getSyncEnabled();
|
||||
if (syncEnabled) {
|
||||
this.errorHandledRunSync();
|
||||
return;
|
||||
}
|
||||
await this.checkForListenersMaybeRemove();
|
||||
await this.storageArea.removeThisInstanceFromSync();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// attaching to window for use in mocha tests
|
||||
window.sync = sync;
|
||||
|
||||
sync.init();
|
||||
|
||||
async function restore() {
|
||||
if (SYNC_DEBUG) console.log("restore");
|
||||
await reconcileIdentities();
|
||||
await reconcileSiteAssignments();
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks for the container name. If it exists, they are assumed to be the
|
||||
* same container, and the color and icon are overwritten from sync, if
|
||||
* different.
|
||||
*/
|
||||
async function reconcileIdentities(){
|
||||
if (SYNC_DEBUG) console.log("reconcileIdentities");
|
||||
|
||||
// first delete any from the deleted list
|
||||
const deletedIdentityList =
|
||||
await sync.storageArea.getDeletedIdentityList();
|
||||
// first remove any deleted identities
|
||||
for (const deletedUUID of deletedIdentityList) {
|
||||
const deletedCookieStoreId =
|
||||
await identityState.lookupCookieStoreId(deletedUUID);
|
||||
if (deletedCookieStoreId){
|
||||
try{
|
||||
await browser.contextualIdentities.remove(deletedCookieStoreId);
|
||||
} catch (error) {
|
||||
// if the identity we are deleting is not there, that's fine.
|
||||
console.error("Error deleting contextualIdentity", deletedCookieStoreId);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
const localIdentities = await browser.contextualIdentities.query({});
|
||||
const syncIdentitiesRemoveDupes =
|
||||
await sync.storageArea.getIdentities();
|
||||
// find any local dupes created on sync storage and delete from sync storage
|
||||
for (const localIdentity of localIdentities) {
|
||||
const syncIdentitiesOfName = syncIdentitiesRemoveDupes
|
||||
.filter(identity => identity.name === localIdentity.name);
|
||||
if (syncIdentitiesOfName.length > 1) {
|
||||
const identityMatchingContextId = syncIdentitiesOfName
|
||||
.find(identity => identity.cookieStoreId === localIdentity.cookieStoreId);
|
||||
if (identityMatchingContextId)
|
||||
await sync.storageArea.removeIdentityKeyFromSync(identityMatchingContextId.macAddonUUID);
|
||||
}
|
||||
}
|
||||
const syncIdentities =
|
||||
await sync.storageArea.getIdentities();
|
||||
// now compare all containers for matching names.
|
||||
for (const syncIdentity of syncIdentities) {
|
||||
if (syncIdentity.macAddonUUID){
|
||||
const localMatch = localIdentities.find(
|
||||
localIdentity => localIdentity.name === syncIdentity.name
|
||||
);
|
||||
if (!localMatch) {
|
||||
// if there's no name match found, check on uuid,
|
||||
const localCookieStoreID =
|
||||
await identityState.lookupCookieStoreId(syncIdentity.macAddonUUID);
|
||||
if (localCookieStoreID) {
|
||||
await ifUUIDMatch(syncIdentity, localCookieStoreID);
|
||||
continue;
|
||||
}
|
||||
await ifNoMatch(syncIdentity);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Names match, so use the info from Sync
|
||||
await updateIdentityWithSyncInfo(syncIdentity, localMatch);
|
||||
continue;
|
||||
}
|
||||
// if no macAddonUUID, there is a problem with the sync info and it needs to be ignored.
|
||||
}
|
||||
|
||||
await updateSiteAssignmentUUIDs();
|
||||
|
||||
async function updateSiteAssignmentUUIDs(){
|
||||
const sites = assignManager.storageArea.getAssignedSites();
|
||||
for (const siteKey of Object.keys(sites)) {
|
||||
await assignManager.storageArea.set(siteKey, sites[siteKey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateIdentityWithSyncInfo(syncIdentity, localMatch) {
|
||||
// Sync is truth. if there is a match, compare data and update as needed
|
||||
if (syncIdentity.color !== localMatch.color
|
||||
|| syncIdentity.icon !== localMatch.icon) {
|
||||
await browser.contextualIdentities.update(
|
||||
localMatch.cookieStoreId, {
|
||||
name: syncIdentity.name,
|
||||
color: syncIdentity.color,
|
||||
icon: syncIdentity.icon
|
||||
});
|
||||
|
||||
if (SYNC_DEBUG) {
|
||||
if (localMatch.color !== syncIdentity.color) {
|
||||
console.log(localMatch.name, "Change color: ", syncIdentity.color);
|
||||
}
|
||||
if (localMatch.icon !== syncIdentity.icon) {
|
||||
console.log(localMatch.name, "Change icon: ", syncIdentity.icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sync is truth. If all is the same, update the local uuid to match sync
|
||||
if (localMatch.macAddonUUID !== syncIdentity.macAddonUUID) {
|
||||
await identityState.updateUUID(
|
||||
localMatch.cookieStoreId,
|
||||
syncIdentity.macAddonUUID
|
||||
);
|
||||
}
|
||||
// TODOkmw: update any site assignment UUIDs
|
||||
}
|
||||
|
||||
async function ifUUIDMatch(syncIdentity, localCookieStoreID) {
|
||||
// if there's an identical local uuid, it's the same container. Sync is truth
|
||||
const identityInfo = {
|
||||
name: syncIdentity.name,
|
||||
color: syncIdentity.color,
|
||||
icon: syncIdentity.icon
|
||||
};
|
||||
if (SYNC_DEBUG) {
|
||||
try {
|
||||
const getIdent =
|
||||
await browser.contextualIdentities.get(localCookieStoreID);
|
||||
if (getIdent.name !== identityInfo.name) {
|
||||
console.log(getIdent.name, "Change name: ", identityInfo.name);
|
||||
}
|
||||
if (getIdent.color !== identityInfo.color) {
|
||||
console.log(getIdent.name, "Change color: ", identityInfo.color);
|
||||
}
|
||||
if (getIdent.icon !== identityInfo.icon) {
|
||||
console.log(getIdent.name, "Change icon: ", identityInfo.icon);
|
||||
}
|
||||
} catch (error) {
|
||||
//if this fails, there is probably differing sync info.
|
||||
console.error("Error getting info on CI", error);
|
||||
}
|
||||
}
|
||||
try {
|
||||
// update the local container with the sync data
|
||||
await browser.contextualIdentities
|
||||
.update(localCookieStoreID, identityInfo);
|
||||
return;
|
||||
} catch (error) {
|
||||
// If this fails, sync info is off.
|
||||
console.error("Error udpating CI", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function ifNoMatch(syncIdentity){
|
||||
// if no uuid match either, make new identity
|
||||
if (SYNC_DEBUG) console.log("create new ident: ", syncIdentity.name);
|
||||
const newIdentity =
|
||||
await browser.contextualIdentities.create({
|
||||
name: syncIdentity.name,
|
||||
color: syncIdentity.color,
|
||||
icon: syncIdentity.icon
|
||||
});
|
||||
await identityState.updateUUID(
|
||||
newIdentity.cookieStoreId,
|
||||
syncIdentity.macAddonUUID
|
||||
);
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* Checks for site previously assigned. If it exists, and has the same
|
||||
* container assignment, the assignment is kept. If it exists, but has
|
||||
* a different assignment, the user is prompted (not yet implemented).
|
||||
* If it does not exist, it is created.
|
||||
*/
|
||||
async function reconcileSiteAssignments() {
|
||||
if (SYNC_DEBUG) console.log("reconcileSiteAssignments");
|
||||
const assignedSitesLocal =
|
||||
await assignManager.storageArea.getAssignedSites();
|
||||
const assignedSitesFromSync =
|
||||
await sync.storageArea.getAssignedSites();
|
||||
const deletedSiteList =
|
||||
await sync.storageArea.getDeletedSiteList();
|
||||
for(const siteStoreKey of deletedSiteList) {
|
||||
if (Object.prototype.hasOwnProperty.call(assignedSitesLocal,siteStoreKey)) {
|
||||
assignManager
|
||||
.storageArea
|
||||
.remove(siteStoreKey);
|
||||
}
|
||||
}
|
||||
|
||||
for(const urlKey of Object.keys(assignedSitesFromSync)) {
|
||||
const assignedSite = assignedSitesFromSync[urlKey];
|
||||
try{
|
||||
if (assignedSite.identityMacAddonUUID) {
|
||||
// Sync is truth.
|
||||
// Not even looking it up. Just overwrite
|
||||
if (SYNC_DEBUG){
|
||||
const isInStorage = await assignManager.storageArea.getByUrlKey(urlKey);
|
||||
if (!isInStorage)
|
||||
console.log("new assignment ", assignedSite);
|
||||
}
|
||||
|
||||
await setAssignmentWithUUID(assignedSite, urlKey);
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
// this is probably old or incorrect site info in Sync
|
||||
// skip and move on.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MILISECONDS_IN_THIRTY_DAYS = 2592000000;
|
||||
|
||||
async function removeOldDeletedItems() {
|
||||
const instanceList = await sync.storageArea.getAllInstanceInfo();
|
||||
const deletedSiteList = await sync.storageArea.getDeletedSiteList();
|
||||
const deletedIdentityList = await sync.storageArea.getDeletedIdentityList();
|
||||
|
||||
for (const instanceKey of Object.keys(instanceList)) {
|
||||
const date = new Date();
|
||||
const currentTimestamp = date.getTime();
|
||||
if (instanceList[instanceKey].timestamp < currentTimestamp - MILISECONDS_IN_THIRTY_DAYS) {
|
||||
delete instanceList[instanceKey];
|
||||
sync.storageArea.removeInstance(instanceKey);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (const siteStoreKey of deletedSiteList) {
|
||||
let hasMatch = false;
|
||||
for (const instance of Object.values(instanceList)) {
|
||||
const match = instance.siteAssignments.find(element => element === siteStoreKey);
|
||||
if (!match) continue;
|
||||
hasMatch = true;
|
||||
}
|
||||
if (!hasMatch) {
|
||||
await sync.storageArea.backup({undeleteSiteStoreKey: siteStoreKey});
|
||||
}
|
||||
}
|
||||
for (const identityUUID of deletedIdentityList) {
|
||||
let hasMatch = false;
|
||||
for (const instance of Object.values(instanceList)) {
|
||||
const match = instance.identities.find(element => element === identityUUID);
|
||||
if (!match) continue;
|
||||
hasMatch = true;
|
||||
}
|
||||
if (!hasMatch) {
|
||||
await sync.storageArea.backup({undeleteUUID: identityUUID});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setAssignmentWithUUID(assignedSite, urlKey) {
|
||||
const uuid = assignedSite.identityMacAddonUUID;
|
||||
const cookieStoreId = await identityState.lookupCookieStoreId(uuid);
|
||||
if (cookieStoreId) {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
assignedSite.userContextId = cookieStoreId
|
||||
.replace(/^firefox-container-/, "");
|
||||
await assignManager.storageArea.set(
|
||||
urlKey,
|
||||
assignedSite,
|
||||
false,
|
||||
false
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw new Error (`No cookieStoreId found for: ${uuid}, ${urlKey}`);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
async function load() {
|
||||
const searchParams = new URL(window.location).searchParams;
|
||||
const redirectUrl = decodeURIComponent(searchParams.get("url"));
|
||||
const redirectUrl = searchParams.get("url");
|
||||
const cookieStoreId = searchParams.get("cookieStoreId");
|
||||
const currentCookieStoreId = searchParams.get("currentCookieStoreId");
|
||||
const redirectUrlElement = document.getElementById("redirect-url");
|
||||
@@ -17,18 +17,14 @@ async function load() {
|
||||
const currentContainer = await browser.contextualIdentities.get(currentCookieStoreId);
|
||||
document.getElementById("current-container-name").textContent = currentContainer.name;
|
||||
}
|
||||
|
||||
document.getElementById("redirect-form").addEventListener("submit", (e) => {
|
||||
document.getElementById("deny").addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const buttonTarget = e.explicitOriginalTarget;
|
||||
switch (buttonTarget.id) {
|
||||
case "confirm":
|
||||
confirmSubmit(redirectUrl, cookieStoreId);
|
||||
break;
|
||||
case "deny":
|
||||
denySubmit(redirectUrl);
|
||||
break;
|
||||
}
|
||||
denySubmit(redirectUrl);
|
||||
});
|
||||
|
||||
document.getElementById("confirm").addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
confirmSubmit(redirectUrl, cookieStoreId);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
async function requestPermissions() {
|
||||
const checkbox = document.querySelector("#bookmarksPermissions");
|
||||
if (checkbox.checked) {
|
||||
const granted = await browser.permissions.request({permissions: ["bookmarks"]});
|
||||
if (!granted) {
|
||||
checkbox.checked = false;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await browser.permissions.remove({permissions: ["bookmarks"]});
|
||||
}
|
||||
browser.runtime.sendMessage({ method: "resetBookmarksContext" });
|
||||
}
|
||||
|
||||
async function enableDisableSync() {
|
||||
const checkbox = document.querySelector("#syncCheck");
|
||||
if (checkbox.checked) {
|
||||
await browser.storage.local.set({syncEnabled: true});
|
||||
} else {
|
||||
await browser.storage.local.set({syncEnabled: false});
|
||||
}
|
||||
browser.runtime.sendMessage({ method: "resetSync" });
|
||||
}
|
||||
|
||||
async function restoreOptions() {
|
||||
const hasPermission = await browser.permissions.contains({permissions: ["bookmarks"]});
|
||||
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
|
||||
if (hasPermission) {
|
||||
document.querySelector("#bookmarksPermissions").checked = true;
|
||||
}
|
||||
if (syncEnabled) {
|
||||
document.querySelector("#syncCheck").checked = true;
|
||||
} else {
|
||||
document.querySelector("#syncCheck").checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", restoreOptions);
|
||||
document.querySelector("#bookmarksPermissions").addEventListener( "change", requestPermissions);
|
||||
document.querySelector("#syncCheck").addEventListener( "change", enableDisableSync);
|
||||
@@ -12,15 +12,17 @@ const NEW_CONTAINER_ID = "new";
|
||||
const ONBOARDING_STORAGE_KEY = "onboarding-stage";
|
||||
|
||||
// List of panels
|
||||
const P_ONBOARDING_1 = "onboarding1";
|
||||
const P_ONBOARDING_2 = "onboarding2";
|
||||
const P_ONBOARDING_3 = "onboarding3";
|
||||
const P_ONBOARDING_4 = "onboarding4";
|
||||
const P_ONBOARDING_5 = "onboarding5";
|
||||
const P_CONTAINERS_LIST = "containersList";
|
||||
const P_CONTAINERS_EDIT = "containersEdit";
|
||||
const P_CONTAINER_INFO = "containerInfo";
|
||||
const P_CONTAINER_EDIT = "containerEdit";
|
||||
const P_ONBOARDING_1 = "onboarding1";
|
||||
const P_ONBOARDING_2 = "onboarding2";
|
||||
const P_ONBOARDING_3 = "onboarding3";
|
||||
const P_ONBOARDING_4 = "onboarding4";
|
||||
const P_ONBOARDING_5 = "onboarding5";
|
||||
const P_ONBOARDING_6 = "onboarding6";
|
||||
const P_ONBOARDING_7 = "onboarding7";
|
||||
const P_CONTAINERS_LIST = "containersList";
|
||||
const P_CONTAINERS_EDIT = "containersEdit";
|
||||
const P_CONTAINER_INFO = "containerInfo";
|
||||
const P_CONTAINER_EDIT = "containerEdit";
|
||||
const P_CONTAINER_DELETE = "containerDelete";
|
||||
const P_CONTAINERS_ACHIEVEMENT = "containersAchievement";
|
||||
|
||||
@@ -32,7 +34,7 @@ const P_CONTAINERS_ACHIEVEMENT = "containersAchievement";
|
||||
* @return {string} The escaped string.
|
||||
*/
|
||||
function escapeXML(str) {
|
||||
const replacements = {"&": "&", "\"": """, "'": "'", "<": "<", ">": ">", "/": "/"};
|
||||
const replacements = { "&": "&", "\"": """, "'": "'", "<": "<", ">": ">", "/": "/" };
|
||||
return String(str).replace(/[&"'<>/]/g, m => replacements[m]);
|
||||
}
|
||||
|
||||
@@ -85,7 +87,7 @@ const Logic = {
|
||||
|
||||
try {
|
||||
await identitiesPromise;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
throw new Error("Failed to retrieve the identities or variation. We cannot continue. ", e.message);
|
||||
}
|
||||
|
||||
@@ -99,9 +101,15 @@ const Logic = {
|
||||
}
|
||||
|
||||
switch (onboarded) {
|
||||
case 5:
|
||||
case 7:
|
||||
this.showAchievementOrContainersListPanel();
|
||||
break;
|
||||
case 6:
|
||||
this.showPanel(P_ONBOARDING_7);
|
||||
break;
|
||||
case 5:
|
||||
this.showPanel(P_ONBOARDING_6);
|
||||
break;
|
||||
case 4:
|
||||
this.showPanel(P_ONBOARDING_5);
|
||||
break;
|
||||
@@ -125,7 +133,7 @@ const Logic = {
|
||||
async showAchievementOrContainersListPanel() {
|
||||
// Do we need to show an achievement panel?
|
||||
let showAchievements = false;
|
||||
const achievementsStorage = await browser.storage.local.get({achievements: []});
|
||||
const achievementsStorage = await browser.storage.local.get({ achievements: [] });
|
||||
for (const achievement of achievementsStorage.achievements) {
|
||||
if (!achievement.done) {
|
||||
showAchievements = true;
|
||||
@@ -142,7 +150,7 @@ const Logic = {
|
||||
// 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 achievementsStorage = await browser.storage.local.get({ achievements: [] });
|
||||
const achievements = achievementsStorage.achievements;
|
||||
achievements.forEach((achievement, index, achievementsArray) => {
|
||||
if (achievement.name === achievementName) {
|
||||
@@ -150,7 +158,7 @@ const Logic = {
|
||||
achievementsArray[index] = achievement;
|
||||
}
|
||||
});
|
||||
browser.storage.local.set({achievements});
|
||||
browser.storage.local.set({ achievements });
|
||||
},
|
||||
|
||||
setOnboardingStage(stage) {
|
||||
@@ -161,9 +169,9 @@ const Logic = {
|
||||
|
||||
async clearBrowserActionBadge() {
|
||||
const extensionInfo = await getExtensionInfo();
|
||||
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
||||
browser.browserAction.setBadgeBackgroundColor({color: ""});
|
||||
browser.browserAction.setBadgeText({text: ""});
|
||||
const storage = await browser.storage.local.get({ browserActionBadgesClicked: [] });
|
||||
browser.browserAction.setBadgeBackgroundColor({ color: null });
|
||||
browser.browserAction.setBadgeText({ text: "" });
|
||||
storage.browserActionBadgesClicked.push(extensionInfo.version);
|
||||
// use set and spread to create a unique array
|
||||
const browserActionBadgesClicked = [...new Set(storage.browserActionBadgesClicked)];
|
||||
@@ -177,12 +185,14 @@ const Logic = {
|
||||
name: "Default",
|
||||
cookieStoreId,
|
||||
icon: "default-tab",
|
||||
color: "default-tab"
|
||||
color: "default-tab",
|
||||
numberOfHiddenTabs: 0,
|
||||
numberOfOpenTabs: 0
|
||||
};
|
||||
// Handle old style rejection with null and also Promise.reject new style
|
||||
try {
|
||||
return await browser.contextualIdentities.get(cookieStoreId) || defaultContainer;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
return defaultContainer;
|
||||
}
|
||||
},
|
||||
@@ -205,13 +215,34 @@ const Logic = {
|
||||
},
|
||||
|
||||
async currentTab() {
|
||||
const activeTabs = await browser.tabs.query({active: true, windowId: browser.windows.WINDOW_ID_CURRENT});
|
||||
const activeTabs = await browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT });
|
||||
if (activeTabs.length > 0) {
|
||||
return activeTabs[0];
|
||||
}
|
||||
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() {
|
||||
const [identities, state] = await Promise.all([
|
||||
browser.contextualIdentities.query({}),
|
||||
@@ -227,6 +258,8 @@ const Logic = {
|
||||
if (stateObject) {
|
||||
identity.hasOpenTabs = stateObject.hasOpenTabs;
|
||||
identity.hasHiddenTabs = stateObject.hasHiddenTabs;
|
||||
identity.numberOfHiddenTabs = stateObject.numberOfHiddenTabs;
|
||||
identity.numberOfOpenTabs = stateObject.numberOfOpenTabs;
|
||||
}
|
||||
return identity;
|
||||
});
|
||||
@@ -234,7 +267,8 @@ const Logic = {
|
||||
|
||||
getPanelSelector(panel) {
|
||||
if (this._onboardingVariation === "securityOnboarding" &&
|
||||
panel.hasOwnProperty("securityPanelSelector")) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
panel.hasOwnProperty("securityPanelSelector")) {
|
||||
return panel.securityPanelSelector;
|
||||
} else {
|
||||
return panel.panelSelector;
|
||||
@@ -264,7 +298,13 @@ const Logic = {
|
||||
}
|
||||
}
|
||||
});
|
||||
document.querySelector(this.getPanelSelector(this._panels[panel])).classList.remove("hide");
|
||||
const panelEl = document.querySelector(this.getPanelSelector(this._panels[panel]));
|
||||
panelEl.classList.remove("hide");
|
||||
|
||||
const focusEl = panelEl.querySelector(".firstTabindex");
|
||||
if(focusEl) {
|
||||
focusEl.focus();
|
||||
}
|
||||
},
|
||||
|
||||
showPreviousPanel() {
|
||||
@@ -308,7 +348,7 @@ const Logic = {
|
||||
|
||||
return browser.runtime.sendMessage({
|
||||
method: "deleteContainer",
|
||||
message: {userContextId}
|
||||
message: { userContextId }
|
||||
});
|
||||
},
|
||||
|
||||
@@ -320,9 +360,12 @@ const Logic = {
|
||||
},
|
||||
|
||||
getAssignmentObjectByContainer(userContextId) {
|
||||
if (!userContextId) {
|
||||
return {};
|
||||
}
|
||||
return browser.runtime.sendMessage({
|
||||
method: "getAssignmentObjectByContainer",
|
||||
message: {userContextId}
|
||||
message: { userContextId }
|
||||
});
|
||||
},
|
||||
|
||||
@@ -351,12 +394,17 @@ const Logic = {
|
||||
});
|
||||
|
||||
// Here we find the first valid id.
|
||||
for (let id = 1;; ++id) {
|
||||
for (let id = 1; ; ++id) {
|
||||
if (ids.indexOf(id) === -1) {
|
||||
return defaultName + (id < 10 ? "0" : "") + id;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getCurrentPanelElement() {
|
||||
const panelItem = this._panels[this._currentPanel];
|
||||
return document.querySelector(this.getPanelSelector(panelItem));
|
||||
},
|
||||
};
|
||||
|
||||
// P_ONBOARDING_1: First page for Onboarding.
|
||||
@@ -370,7 +418,7 @@ Logic.registerPanel(P_ONBOARDING_1, {
|
||||
initialize() {
|
||||
// Let's move to the next panel.
|
||||
[...document.querySelectorAll(".onboarding-start-button")].forEach(startElement => {
|
||||
Logic.addEnterHandler(startElement, async function () {
|
||||
Logic.addEnterHandler(startElement, async () => {
|
||||
await Logic.setOnboardingStage(1);
|
||||
Logic.showPanel(P_ONBOARDING_2);
|
||||
});
|
||||
@@ -394,7 +442,7 @@ Logic.registerPanel(P_ONBOARDING_2, {
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
[...document.querySelectorAll(".onboarding-next-button")].forEach(nextElement => {
|
||||
Logic.addEnterHandler(nextElement, async function () {
|
||||
Logic.addEnterHandler(nextElement, async () => {
|
||||
await Logic.setOnboardingStage(2);
|
||||
Logic.showPanel(P_ONBOARDING_3);
|
||||
});
|
||||
@@ -418,7 +466,7 @@ Logic.registerPanel(P_ONBOARDING_3, {
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
[...document.querySelectorAll(".onboarding-almost-done-button")].forEach(almostElement => {
|
||||
Logic.addEnterHandler(almostElement, async function () {
|
||||
Logic.addEnterHandler(almostElement, async () => {
|
||||
await Logic.setOnboardingStage(3);
|
||||
Logic.showPanel(P_ONBOARDING_4);
|
||||
});
|
||||
@@ -440,7 +488,7 @@ Logic.registerPanel(P_ONBOARDING_4, {
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
Logic.addEnterHandler(document.querySelector("#onboarding-done-button"), async function () {
|
||||
Logic.addEnterHandler(document.querySelector("#onboarding-done-button"), async () => {
|
||||
await Logic.setOnboardingStage(4);
|
||||
Logic.showPanel(P_ONBOARDING_5);
|
||||
});
|
||||
@@ -461,8 +509,41 @@ Logic.registerPanel(P_ONBOARDING_5, {
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
Logic.addEnterHandler(document.querySelector("#onboarding-longpress-button"), async function () {
|
||||
Logic.addEnterHandler(document.querySelector("#onboarding-longpress-button"), async () => {
|
||||
await Logic.setOnboardingStage(5);
|
||||
Logic.showPanel(P_ONBOARDING_6);
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
});
|
||||
|
||||
// P_ONBOARDING_6: Sixth page for Onboarding: new tab long-press behavior
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Logic.registerPanel(P_ONBOARDING_6, {
|
||||
panelSelector: ".onboarding-panel-6",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
Logic.addEnterHandler(document.querySelector("#start-sync-button"), async () => {
|
||||
await Logic.setOnboardingStage(6);
|
||||
await browser.storage.local.set({syncEnabled: true});
|
||||
await browser.runtime.sendMessage({
|
||||
method: "resetSync"
|
||||
});
|
||||
Logic.showPanel(P_ONBOARDING_7);
|
||||
});
|
||||
Logic.addEnterHandler(document.querySelector("#no-sync"), async () => {
|
||||
await Logic.setOnboardingStage(7);
|
||||
await browser.storage.local.set({syncEnabled: false});
|
||||
await browser.runtime.sendMessage({
|
||||
method: "resetSync"
|
||||
});
|
||||
Logic.showPanel(P_CONTAINERS_LIST);
|
||||
});
|
||||
},
|
||||
@@ -473,6 +554,33 @@ Logic.registerPanel(P_ONBOARDING_5, {
|
||||
},
|
||||
});
|
||||
|
||||
// P_ONBOARDING_6: Sixth page for Onboarding: new tab long-press behavior
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Logic.registerPanel(P_ONBOARDING_7, {
|
||||
panelSelector: ".onboarding-panel-7",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
Logic.addEnterHandler(document.querySelector("#sign-in"), async () => {
|
||||
browser.tabs.create({
|
||||
url: "https://accounts.firefox.com/?service=sync&action=email&context=fx_desktop_v3&entrypoint=multi-account-containers&utm_source=addon&utm_medium=panel&utm_campaign=container-sync",
|
||||
});
|
||||
await Logic.setOnboardingStage(7);
|
||||
Logic.showPanel(P_CONTAINERS_LIST);
|
||||
});
|
||||
Logic.addEnterHandler(document.querySelector("#no-sign-in"), async () => {
|
||||
await Logic.setOnboardingStage(7);
|
||||
Logic.showPanel(P_CONTAINERS_LIST);
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
});
|
||||
// P_CONTAINERS_LIST: The list of containers. The main page.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -485,11 +593,13 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
|
||||
});
|
||||
|
||||
Logic.addEnterHandler(document.querySelector("#edit-containers-link"), () => {
|
||||
Logic.showPanel(P_CONTAINERS_EDIT);
|
||||
Logic.addEnterHandler(document.querySelector("#edit-containers-link"), (e) => {
|
||||
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 () => {
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
method: "sortTabs"
|
||||
@@ -501,7 +611,7 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const selectables = [...document.querySelectorAll("[tabindex='0'], [tabindex='-1']")];
|
||||
const selectables = [...document.querySelectorAll(".open-newtab[tabindex='0']")];
|
||||
const element = document.activeElement;
|
||||
const index = selectables.indexOf(element) || 0;
|
||||
function next() {
|
||||
@@ -523,9 +633,26 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||
case 38:
|
||||
previous();
|
||||
break;
|
||||
case 39:
|
||||
{
|
||||
const showTabs = element.parentNode.querySelector(".show-tabs");
|
||||
if(showTabs) {
|
||||
showTabs.click();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 37:
|
||||
{
|
||||
const hideTabs = document.querySelector(".panel-back-arrow");
|
||||
if(hideTabs) {
|
||||
hideTabs.click();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (e.keyCode >= 49 && e.keyCode <= 57) {
|
||||
const element = selectables[e.keyCode - 48];
|
||||
if ((e.keyCode >= 49 && e.keyCode <= 57) &&
|
||||
Logic._currentPanel === "containersList") {
|
||||
const element = selectables[e.keyCode - 49];
|
||||
if (element) {
|
||||
element.click();
|
||||
}
|
||||
@@ -607,11 +734,11 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||
|
||||
tr.classList.add("container-panel-row");
|
||||
|
||||
context.classList.add("userContext-wrapper", "open-newtab", "clickable");
|
||||
context.classList.add("userContext-wrapper", "open-newtab", "clickable", "firstTabindex");
|
||||
manage.classList.add("show-tabs", "pop-button");
|
||||
manage.title = escaped`View ${identity.name} container`;
|
||||
manage.setAttribute("title", `View ${identity.name} container`);
|
||||
context.setAttribute("tabindex", "0");
|
||||
context.title = escaped`Create ${identity.name} tab`;
|
||||
context.setAttribute("title", `Create ${identity.name} tab`);
|
||||
context.innerHTML = escaped`
|
||||
<div class="userContext-icon-wrapper open-newtab">
|
||||
<div class="usercontext-icon"
|
||||
@@ -631,10 +758,10 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||
tr.appendChild(manage);
|
||||
}
|
||||
|
||||
Logic.addEnterHandler(tr, async function (e) {
|
||||
Logic.addEnterHandler(tr, async (e) => {
|
||||
if (e.target.matches(".open-newtab")
|
||||
|| e.target.parentNode.matches(".open-newtab")
|
||||
|| e.type === "keydown") {
|
||||
|| e.target.parentNode.matches(".open-newtab")
|
||||
|| e.type === "keydown") {
|
||||
try {
|
||||
browser.tabs.create({
|
||||
cookieStoreId: identity.cookieStoreId
|
||||
@@ -657,14 +784,25 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||
however it allows us to have a tabindex before the first selected item
|
||||
*/
|
||||
const focusHandler = () => {
|
||||
list.querySelector("tr .clickable").focus();
|
||||
document.removeEventListener("focus", focusHandler);
|
||||
const identityList = list.querySelector("tr .clickable");
|
||||
if (identityList) {
|
||||
// otherwise this throws an error when there are no containers present.
|
||||
identityList.focus();
|
||||
document.removeEventListener("focus", focusHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener("focus", focusHandler);
|
||||
/* If the user mousedown's first then remove the focus handler */
|
||||
document.addEventListener("mousedown", () => {
|
||||
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();
|
||||
},
|
||||
@@ -678,11 +816,15 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||
|
||||
// This method is called when the object is registered.
|
||||
async initialize() {
|
||||
Logic.addEnterHandler(document.querySelector("#close-container-info-panel"), () => {
|
||||
const closeContEl = document.querySelector("#close-container-info-panel");
|
||||
closeContEl.setAttribute("tabindex", "0");
|
||||
closeContEl.classList.add("firstTabindex");
|
||||
Logic.addEnterHandler(closeContEl, () => {
|
||||
Logic.showPreviousPanel();
|
||||
});
|
||||
|
||||
Logic.addEnterHandler(document.querySelector("#container-info-hideorshow"), async function () {
|
||||
const hideContEl = document.querySelector("#container-info-hideorshow");
|
||||
hideContEl.setAttribute("tabindex", "0");
|
||||
Logic.addEnterHandler(hideContEl, async () => {
|
||||
const identity = Logic.currentIdentity();
|
||||
try {
|
||||
browser.runtime.sendMessage({
|
||||
@@ -697,37 +839,32 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||
});
|
||||
|
||||
// Check if the user has incompatible add-ons installed
|
||||
let incompatible = false;
|
||||
try {
|
||||
const incompatible = await browser.runtime.sendMessage({
|
||||
incompatible = await browser.runtime.sendMessage({
|
||||
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",
|
||||
windowId: browser.windows.WINDOW_ID_CURRENT,
|
||||
cookieStoreId: Logic.currentIdentity().cookieStoreId,
|
||||
});
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error("Could not check for incompatible add-ons.");
|
||||
}
|
||||
const moveTabsEl = document.querySelector("#container-info-movetabs");
|
||||
moveTabsEl.setAttribute("tabindex","0");
|
||||
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 () => {
|
||||
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.
|
||||
@@ -776,20 +913,45 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||
tr.classList.add("container-info-tab-row");
|
||||
tr.innerHTML = escaped`
|
||||
<td></td>
|
||||
<td class="container-info-tab-title truncate-text" title="${tab.url}" >${tab.title}</td>`;
|
||||
<td class="container-info-tab-title truncate-text" title="${tab.url}" ><div class="container-tab-title">${tab.title}</div></td>`;
|
||||
tr.querySelector("td").appendChild(Utils.createFavIconElement(tab.favIconUrl));
|
||||
tr.setAttribute("tabindex", "0");
|
||||
document.getElementById("container-info-table").appendChild(fragment);
|
||||
|
||||
// On click, we activate this tab. But only if this tab is active.
|
||||
if (!tab.hiddenState) {
|
||||
const closeImage = document.createElement("img");
|
||||
closeImage.src = "/img/container-close-tab.svg";
|
||||
closeImage.className = "container-close-tab";
|
||||
closeImage.title = "Close tab";
|
||||
closeImage.id = tab.id;
|
||||
const tabTitle = tr.querySelector(".container-info-tab-title");
|
||||
tabTitle.appendChild(closeImage);
|
||||
|
||||
// On hover, we add truncate-text class to add close-tab-image after tab title truncates
|
||||
const tabTitleHoverEvent = () => {
|
||||
tabTitle.classList.toggle("truncate-text");
|
||||
tr.querySelector(".container-tab-title").classList.toggle("truncate-text");
|
||||
};
|
||||
|
||||
tr.addEventListener("mouseover", tabTitleHoverEvent);
|
||||
tr.addEventListener("mouseout", tabTitleHoverEvent);
|
||||
|
||||
tr.classList.add("clickable");
|
||||
Logic.addEnterHandler(tr, async function () {
|
||||
await browser.tabs.update(tab.id, {active: true});
|
||||
Logic.addEnterHandler(tr, async () => {
|
||||
await browser.tabs.update(tab.id, { active: true });
|
||||
window.close();
|
||||
});
|
||||
|
||||
const closeTab = document.getElementById(tab.id);
|
||||
if (closeTab) {
|
||||
Logic.addEnterHandler(closeTab, async (e) => {
|
||||
await browser.tabs.remove(Number(e.target.id));
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("container-info-table").appendChild(fragment);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -927,14 +1089,15 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||
while (tableElement.firstChild) {
|
||||
tableElement.firstChild.remove();
|
||||
}
|
||||
|
||||
assignmentKeys.forEach((siteKey) => {
|
||||
const site = assignments[siteKey];
|
||||
const trElement = document.createElement("div");
|
||||
/* As we don't have the full or correct path the best we can assume is the path is HTTPS and then replace with a broken icon later if it doesn't load.
|
||||
This is pending a better solution for favicons from web extensions */
|
||||
const assumedUrl = `https://${site.hostname}`;
|
||||
const assumedUrl = `https://${site.hostname}/favicon.ico`;
|
||||
trElement.innerHTML = escaped`
|
||||
<img class="icon" src="${assumedUrl}/favicon.ico">
|
||||
<div class="favicon"></div>
|
||||
<div title="${site.hostname}" class="truncate-text hostname">
|
||||
${site.hostname}
|
||||
</div>
|
||||
@@ -942,9 +1105,10 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||
class="pop-button-image delete-assignment"
|
||||
src="/img/container-delete.svg"
|
||||
/>`;
|
||||
trElement.getElementsByClassName("favicon")[0].appendChild(Utils.createFavIconElement(assumedUrl));
|
||||
const deleteButton = trElement.querySelector(".delete-assignment");
|
||||
const that = this;
|
||||
Logic.addEnterHandler(deleteButton, async function () {
|
||||
Logic.addEnterHandler(deleteButton, async () => {
|
||||
const userContextId = Logic.currentUserContextId();
|
||||
// Lets show the message to the current tab
|
||||
// TODO remove then when firefox supports arrow fn async
|
||||
@@ -964,7 +1128,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||
return escaped`<input type="radio" value="${containerColor}" name="container-color" id="edit-container-panel-choose-color-${containerColor}" />
|
||||
<label for="edit-container-panel-choose-color-${containerColor}" class="usercontext-icon choose-color-icon" data-identity-icon="circle" data-identity-color="${containerColor}">`;
|
||||
};
|
||||
const colors = ["blue", "turquoise", "green", "yellow", "orange", "red", "pink", "purple" ];
|
||||
const colors = ["blue", "turquoise", "green", "yellow", "orange", "red", "pink", "purple"];
|
||||
const colorRadioFieldset = document.getElementById("edit-container-panel-choose-color");
|
||||
colors.forEach((containerColor) => {
|
||||
const templateInstance = document.createElement("div");
|
||||
@@ -1000,6 +1164,11 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||
|
||||
document.querySelector("#edit-container-panel-name-input").value = identity.name || "";
|
||||
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 => {
|
||||
colorInput.checked = colorInput.value === identity.color;
|
||||
});
|
||||
@@ -1024,7 +1193,7 @@ Logic.registerPanel(P_CONTAINER_DELETE, {
|
||||
Logic.showPreviousPanel();
|
||||
});
|
||||
|
||||
Logic.addEnterHandler(document.querySelector("#delete-container-ok-link"), async function () {
|
||||
Logic.addEnterHandler(document.querySelector("#delete-container-ok-link"), async () => {
|
||||
/* This promise wont resolve if the last tab was removed from the window.
|
||||
as the message async callback stops listening, this isn't an issue for us however it might be in future
|
||||
if you want to do anything post delete do it in the background script.
|
||||
@@ -1034,7 +1203,7 @@ Logic.registerPanel(P_CONTAINER_DELETE, {
|
||||
await Logic.removeIdentity(Logic.userContextId(Logic.currentIdentity().cookieStoreId));
|
||||
await Logic.refreshIdentities();
|
||||
Logic.showPreviousPanel();
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
Logic.showPanel(P_CONTAINERS_LIST);
|
||||
}
|
||||
});
|
||||
@@ -1044,9 +1213,17 @@ Logic.registerPanel(P_CONTAINER_DELETE, {
|
||||
prepare() {
|
||||
const identity = Logic.currentIdentity();
|
||||
|
||||
// Populating the panel: name and icon
|
||||
// Populating the panel: name, icon, and warning message
|
||||
document.getElementById("delete-container-name").textContent = identity.name;
|
||||
|
||||
const totalNumberOfTabs = identity.numberOfHiddenTabs + identity.numberOfOpenTabs;
|
||||
let warningMessage = "";
|
||||
if (totalNumberOfTabs > 0) {
|
||||
const grammaticalNumTabs = totalNumberOfTabs > 1 ? "tabs" : "tab";
|
||||
warningMessage = `If you remove this container now, ${totalNumberOfTabs} container ${grammaticalNumTabs} will be closed.`;
|
||||
}
|
||||
document.getElementById("delete-container-tab-warning").textContent = warningMessage;
|
||||
|
||||
const icon = document.getElementById("delete-container-icon");
|
||||
icon.setAttribute("data-identity-icon", identity.icon);
|
||||
icon.setAttribute("data-identity-color", identity.color);
|
||||
@@ -1064,7 +1241,7 @@ Logic.registerPanel(P_CONTAINERS_ACHIEVEMENT, {
|
||||
// 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 () {
|
||||
Logic.addEnterHandler(document.querySelector("#achievement-done-button"), async () => {
|
||||
await Logic.setAchievementDone("manyContainersOpened");
|
||||
Logic.showPanel(P_CONTAINERS_LIST);
|
||||
});
|
||||
@@ -1077,3 +1254,14 @@ Logic.registerPanel(P_CONTAINERS_ACHIEVEMENT, {
|
||||
});
|
||||
|
||||
Logic.init();
|
||||
|
||||
window.addEventListener("resize", function () {
|
||||
//for overflow menu
|
||||
const difference = window.innerWidth - document.body.offsetWidth;
|
||||
if (difference > 2) {
|
||||
//if popup is in the overflow menu, window will be larger than 300px
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty("--overflow-size", difference + "px");
|
||||
root.style.setProperty("--icon-fit", "12");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
window.Utils = {
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Firefox Multi-Account Containers",
|
||||
"version": "5.0.0",
|
||||
|
||||
"version": "6.2.5",
|
||||
"incognito": "not_allowed",
|
||||
"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": {
|
||||
"48": "img/container-site-d-48.png",
|
||||
"96": "img/container-site-d-96.png"
|
||||
},
|
||||
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"strict_min_version": "57.0"
|
||||
"id": "@testpilot-containers",
|
||||
"strict_min_version": "67.0"
|
||||
}
|
||||
},
|
||||
|
||||
"homepage_url": "https://testpilot.firefox.com/",
|
||||
|
||||
"homepage_url": "https://github.com/mozilla/multi-account-containers#readme",
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"activeTab",
|
||||
@@ -25,12 +23,15 @@
|
||||
"contextualIdentities",
|
||||
"history",
|
||||
"idle",
|
||||
"management",
|
||||
"storage",
|
||||
"tabs",
|
||||
"webRequestBlocking",
|
||||
"webRequest"
|
||||
],
|
||||
|
||||
"optional_permissions": [
|
||||
"bookmarks"
|
||||
],
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
"suggested_key": {
|
||||
@@ -40,28 +41,40 @@
|
||||
"description": "Open containers panel"
|
||||
}
|
||||
},
|
||||
|
||||
"browser_action": {
|
||||
"browser_style": true,
|
||||
"default_icon": "img/container-site.svg",
|
||||
"default_title": "Multi-Account Containers",
|
||||
"default_popup": "popup.html"
|
||||
"default_popup": "popup.html",
|
||||
"theme_icons": [
|
||||
{
|
||||
"light": "img/container-site-light.svg",
|
||||
"dark": "img/container-site.svg",
|
||||
"size": 32
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"background": {
|
||||
"page": "js/background/index.html"
|
||||
},
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["js/content-script.js"],
|
||||
"css": ["css/content.css"],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"js/content-script.js"
|
||||
],
|
||||
"css": [
|
||||
"css/content.css"
|
||||
],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
|
||||
"web_accessible_resources": [
|
||||
"/img/container-site-d-24.png"
|
||||
]
|
||||
],
|
||||
"options_ui": {
|
||||
"page": "options.html"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<form>
|
||||
<label>
|
||||
<input type="checkbox" id="bookmarksPermissions">
|
||||
Enable Bookmark Menus
|
||||
</label>
|
||||
<p>This setting allows you to open a bookmark or folder of bookmarks in a container.</p>
|
||||
<label>
|
||||
<input type="checkbox" id="syncCheck">
|
||||
Enable Sync
|
||||
</label>
|
||||
<p>This setting allows you to sync your containers and site assignments across devices.</p>
|
||||
</form>
|
||||
<script src="js/options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,7 +2,7 @@
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Multi-Account Containers</title>
|
||||
<link rel="stylesheet" href="/css/popup.css">
|
||||
<link rel="stylesheet" href="css/popup.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
@@ -13,7 +13,7 @@
|
||||
<p>
|
||||
Use containers to organize tasks, manage accounts, and keep your focus where you want it.
|
||||
</p>
|
||||
<a href="#" class="onboarding-button onboarding-start-button">Get Started</a>
|
||||
<a href="#" class="onboarding-button onboarding-start-button" tabindex="0">Get Started</a>
|
||||
</div>
|
||||
|
||||
<div class="hide panel onboarding security-onboarding-panel-1">
|
||||
@@ -22,49 +22,69 @@
|
||||
<p>
|
||||
Use containers to organize tasks, manage accounts, and store sensitive data.
|
||||
</p>
|
||||
<a href="#" class="onboarding-button onboarding-start-button">Get Started</a>
|
||||
<a href="#" class="onboarding-button onboarding-start-button" tabindex="0">Get Started</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-2 hide">
|
||||
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-2.png" />
|
||||
<h3 class="onboarding-title">Put containers to work for you.</h3>
|
||||
<p>Features like color-coding and separate container tabs help you find things easily, focus your attention, and minimize distractions.</p>
|
||||
<a href="#" class="onboarding-button onboarding-next-button">Next</a>
|
||||
<a href="#" class="onboarding-button onboarding-next-button" tabindex="0">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding security-onboarding-panel-2 hide">
|
||||
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-2.png" />
|
||||
<h3 class="onboarding-title">Put containers to work for you.</h3>
|
||||
<p>Color-coding helps you categorize your online life, find things easily, and minimize distractions.</p>
|
||||
<a href="#" class="onboarding-button onboarding-next-button">Next</a>
|
||||
<a href="#" class="onboarding-button onboarding-next-button" tabindex="0">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-3 hide">
|
||||
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3.png" />
|
||||
<h3 class="onboarding-title">A place for everything, and everything in its place.</h3>
|
||||
<p>Start with the containers we've created, or create your own.</p>
|
||||
<a href="#" class="onboarding-button onboarding-almost-done-button">Next</a>
|
||||
<a href="#" class="onboarding-button onboarding-almost-done-button" tabindex="0">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding security-onboarding-panel-3 hide">
|
||||
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3-security.png" />
|
||||
<h3 class="onboarding-title">Set boundaries for your browsing.</h3>
|
||||
<p>Cookies are stored within a container, so you can segment sensitive data and browsing history to stay organized and to limit the impact of online trackers.</p>
|
||||
<a href="#" class="onboarding-button onboarding-almost-done-button">Next</a>
|
||||
<a href="#" class="onboarding-button onboarding-almost-done-button" tabindex="0">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-4 hide" id="onboarding-panel-4">
|
||||
<img class="onboarding-img" alt="How to assign sites to containers" src="/img/onboarding-4.png" />
|
||||
<h3 class="onboarding-title">Always open sites in the containers you want.</h3>
|
||||
<p>Right-click inside a container tab to assign the site to always open in the container.</p>
|
||||
<a href="#" id="onboarding-done-button" class="onboarding-button">Next</a>
|
||||
<a href="#" id="onboarding-done-button" class="onboarding-button" tabindex="0">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-5 hide" id="onboarding-panel-5">
|
||||
<img class="onboarding-img" alt="Long-press the New Tab button to create a new container tab." src="/img/onboarding-3.png" />
|
||||
<h3 class="onboarding-title">Container tabs when you need them.</h3>
|
||||
<p>Long-press the New Tab button to create a new container tab.</p>
|
||||
<a href="#" id="onboarding-longpress-button" class="onboarding-button">Done</a>
|
||||
<a href="#" id="onboarding-longpress-button" class="onboarding-button" tabindex="0">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-6 hide" id="onboarding-panel-6">
|
||||
<img class="onboarding-img" alt="Syncing Containers is now Available!" src="/img/Sync.svg" />
|
||||
<h3 class="onboarding-title">Syncing Containers is now Available!</h3>
|
||||
<p>Turn on Sync to share container and site assignments with any computer connected to your Firefox Account.</p>
|
||||
<div class="half-button-wrapper">
|
||||
<a herf="#" id="no-sync" class="half-onboarding-button grey-button" tabindex="0">Not Now</a>
|
||||
<a href="#" id="start-sync-button" class="half-onboarding-button" tabindex="0">Start Syncing</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-7 hide" id="onboarding-panel-7">
|
||||
<img class="onboarding-img" alt="Firefox Account is required to sync" src="/img/Account.svg" />
|
||||
<h3 class="onboarding-title">Firefox Account is required to sync.</h3>
|
||||
<p>Click Sign In to confirm that your Firefox Account is active.</p>
|
||||
<div class="half-button-wrapper">
|
||||
<a herf="#" id="no-sign-in" class="half-onboarding-button grey-button" tabindex="0">Not Now</a>
|
||||
<a href="#" id="sign-in" class="half-onboarding-button" tabindex="0">Sign In</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel achievement-panel hide" id="achievement-panel">
|
||||
@@ -160,8 +180,9 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer edit-containers-panel-footer">
|
||||
<a href="#" id="exit-edit-mode-link" class="exit-edit-mode-link edit-containers-exit-text">Exit Edit Mode</a>
|
||||
</div>
|
||||
<a href="#" id="exit-edit-mode-link" class="exit-edit-mode-link edit-containers-exit-text">
|
||||
<img src="/img/container-arrow.svg"/>Exit Edit Mode</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -204,7 +225,7 @@
|
||||
</div>
|
||||
<div class="panel-content delete-container-confirm">
|
||||
<h4 class="delete-container-confirm-title">Remove This Container</h4>
|
||||
<p>If you remove this container now, <span id="delete-container-tab-count"></span> container tabs will be closed. Are you sure you want to remove this Container?</p>
|
||||
<p><span id="delete-container-tab-warning"></span> Are you sure you want to remove this Container?</p>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<a href="#" class="button expanded secondary footer-button cancel-button" id="delete-container-cancel-link">Cancel</a>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
"node": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"rules": {
|
||||
"no-restricted-globals": ["error", "browser"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
if (!process.listenerCount("unhandledRejection")) {
|
||||
// eslint-disable-next-line no-console
|
||||
process.on("unhandledRejection", r => console.log(r));
|
||||
}
|
||||
|
||||
const path = require("path");
|
||||
const chai = require("chai");
|
||||
const sinonChai = require("sinon-chai");
|
||||
const crypto = require("crypto");
|
||||
const sinon = require("sinon");
|
||||
const expect = chai.expect;
|
||||
chai.should();
|
||||
chai.use(sinonChai);
|
||||
const nextTick = () => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
process.nextTick(resolve);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const webExtensionsJSDOM = require("webextensions-jsdom");
|
||||
const manifestPath = path.resolve(path.join(__dirname, "../src/manifest.json"));
|
||||
|
||||
const buildDom = async ({background = {}, popup = {}}) => {
|
||||
background = {
|
||||
...background,
|
||||
jsdom: {
|
||||
...background.jsom,
|
||||
beforeParse(window) {
|
||||
window.browser.permissions.getAll.resolves({permissions: ["bookmarks"]});
|
||||
window.crypto = {
|
||||
getRandomValues: arr => crypto.randomBytes(arr.length),
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
popup = {
|
||||
...popup,
|
||||
jsdom: {
|
||||
...popup.jsdom,
|
||||
pretendToBeVisual: true
|
||||
}
|
||||
};
|
||||
|
||||
const webExtension = await webExtensionsJSDOM.fromManifest(manifestPath, {
|
||||
apiFake: true,
|
||||
wiring: true,
|
||||
sinon: global.sinon,
|
||||
background,
|
||||
popup
|
||||
});
|
||||
|
||||
webExtension.browser = webExtension.background.browser;
|
||||
return webExtension;
|
||||
};
|
||||
|
||||
const buildBackgroundDom = background => {
|
||||
return buildDom({
|
||||
background,
|
||||
popup: false
|
||||
});
|
||||
};
|
||||
|
||||
const buildPopupDom = popup => {
|
||||
return buildDom({
|
||||
popup,
|
||||
background: false
|
||||
});
|
||||
};
|
||||
|
||||
const initializeWithTab = async (details = {
|
||||
cookieStoreId: "firefox-default"
|
||||
}) => {
|
||||
let tab;
|
||||
const webExtension = await buildDom({
|
||||
background: {
|
||||
async afterBuild(background) {
|
||||
tab = await background.browser.tabs._create(details);
|
||||
}
|
||||
},
|
||||
popup: {
|
||||
jsdom: {
|
||||
beforeParse(window) {
|
||||
window.browser.storage.local.set({
|
||||
"browserActionBadgesClicked": [],
|
||||
"onboarding-stage": 7,
|
||||
"achievements": [],
|
||||
"syncEnabled": true
|
||||
});
|
||||
window.browser.storage.local.set.resetHistory();
|
||||
window.browser.storage.sync.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
webExtension.tab = tab;
|
||||
|
||||
return webExtension;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
buildDom,
|
||||
buildBackgroundDom,
|
||||
buildPopupDom,
|
||||
initializeWithTab,
|
||||
sinon,
|
||||
expect,
|
||||
nextTick,
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
const {initializeWithTab} = require("../common");
|
||||
|
||||
describe("Assignment Feature", function () {
|
||||
const url = "http://example.com";
|
||||
|
||||
beforeEach(async function () {
|
||||
this.webExt = await initializeWithTab({
|
||||
cookieStoreId: "firefox-container-1",
|
||||
url
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.webExt.destroy();
|
||||
});
|
||||
|
||||
describe("click the 'Always open in' checkbox in the popup", function () {
|
||||
beforeEach(async function () {
|
||||
// popup click to set assignment for activeTab.url
|
||||
await this.webExt.popup.helper.clickElementById("container-page-assigned");
|
||||
});
|
||||
|
||||
describe("open new Tab with the assigned URL in the default container", function () {
|
||||
let newTab;
|
||||
beforeEach(async function () {
|
||||
// new Tab opening activeTab.url in default container
|
||||
newTab = await this.webExt.background.browser.tabs._create({
|
||||
cookieStoreId: "firefox-default",
|
||||
url
|
||||
}, {
|
||||
options: {
|
||||
webRequestError: true // because request is canceled due to reopening
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should open the confirm page", async function () {
|
||||
// should have created a new tab with the confirm page
|
||||
this.webExt.background.browser.tabs.create.should.have.been.calledWithMatch({
|
||||
url: "moz-extension://fake/confirm-page.html?" +
|
||||
`url=${encodeURIComponent(url)}` +
|
||||
`&cookieStoreId=${this.webExt.tab.cookieStoreId}`,
|
||||
cookieStoreId: undefined,
|
||||
openerTabId: null,
|
||||
index: 2,
|
||||
active: true
|
||||
});
|
||||
});
|
||||
|
||||
it("should remove the new Tab that got opened in the default container", function () {
|
||||
this.webExt.background.browser.tabs.remove.should.have.been.calledWith(newTab.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("click the 'Always open in' checkbox in the popup again", function () {
|
||||
beforeEach(async function () {
|
||||
// popup click to remove assignment for activeTab.url
|
||||
await this.webExt.popup.helper.clickElementById("container-page-assigned");
|
||||
});
|
||||
|
||||
describe("open new Tab with the no longer assigned URL in the default container", function () {
|
||||
beforeEach(async function () {
|
||||
// new Tab opening activeTab.url in default container
|
||||
await this.webExt.background.browser.tabs._create({
|
||||
cookieStoreId: "firefox-default",
|
||||
url
|
||||
});
|
||||
});
|
||||
|
||||
it("should not open the confirm page", async function () {
|
||||
// should not have created a new tab
|
||||
this.webExt.background.browser.tabs.create.should.not.have.been.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
const {initializeWithTab} = require("../common");
|
||||
|
||||
describe("Containers Management", function () {
|
||||
beforeEach(async function () {
|
||||
this.webExt = await initializeWithTab();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.webExt.destroy();
|
||||
});
|
||||
|
||||
describe("creating a new container", function () {
|
||||
beforeEach(async function () {
|
||||
await this.webExt.popup.helper.clickElementById("container-add-link");
|
||||
await this.webExt.popup.helper.clickElementById("edit-container-ok-link");
|
||||
});
|
||||
|
||||
it("should create it in the browser as well", function () {
|
||||
this.webExt.background.browser.contextualIdentities.create.should.have.been.calledOnce;
|
||||
});
|
||||
|
||||
describe("removing it afterwards", function () {
|
||||
beforeEach(async function () {
|
||||
await this.webExt.popup.helper.clickElementById("edit-containers-link");
|
||||
await this.webExt.popup.helper.clickElementByQuerySelectorAll(".delete-container-icon", "last");
|
||||
await this.webExt.popup.helper.clickElementById("delete-container-ok-link");
|
||||
});
|
||||
|
||||
it("should remove it in the browser as well", function () {
|
||||
this.webExt.background.browser.contextualIdentities.remove.should.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
const {expect, initializeWithTab} = require("../common");
|
||||
|
||||
describe("External Webextensions", function () {
|
||||
const url = "http://example.com";
|
||||
|
||||
beforeEach(async function () {
|
||||
this.webExt = await initializeWithTab({
|
||||
cookieStoreId: "firefox-container-1",
|
||||
url
|
||||
});
|
||||
|
||||
await this.webExt.popup.helper.clickElementById("container-page-assigned");
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.webExt.destroy();
|
||||
});
|
||||
|
||||
describe("with contextualIdentities permissions", function () {
|
||||
it("should be able to get assignments", async function () {
|
||||
this.webExt.background.browser.management.get.resolves({
|
||||
permissions: ["contextualIdentities"]
|
||||
});
|
||||
|
||||
const message = {
|
||||
method: "getAssignment",
|
||||
url
|
||||
};
|
||||
const sender = {
|
||||
id: "external-webextension"
|
||||
};
|
||||
|
||||
const [promise] = this.webExt.background.browser.runtime.onMessageExternal.addListener.yield(message, sender);
|
||||
const answer = await promise;
|
||||
expect(answer.userContextId === "1").to.be.true;
|
||||
expect(answer.neverAsk === false).to.be.true;
|
||||
expect(
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
answer, "identityMacAddonUUID")).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("without contextualIdentities permissions", function () {
|
||||
it("should throw an error", async function () {
|
||||
this.webExt.background.browser.management.get.resolves({
|
||||
permissions: []
|
||||
});
|
||||
|
||||
const message = {
|
||||
method: "getAssignment",
|
||||
url
|
||||
};
|
||||
const sender = {
|
||||
id: "external-webextension"
|
||||
};
|
||||
|
||||
const [promise] = this.webExt.background.browser.runtime.onMessageExternal.addListener.yield(message, sender);
|
||||
return promise.catch(error => {
|
||||
expect(error.message).to.equal("Missing contextualIdentities permission");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,465 @@
|
||||
const {initializeWithTab} = require("../common");
|
||||
|
||||
describe("Sync", function() {
|
||||
beforeEach(async function() {
|
||||
this.webExt = await initializeWithTab();
|
||||
this.syncHelper = new SyncTestHelper(this.webExt);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
this.webExt.destroy();
|
||||
delete this.syncHelper;
|
||||
});
|
||||
|
||||
it("testIdentityStateCleanup", async function() {
|
||||
await this.syncHelper.stopSyncListeners();
|
||||
|
||||
await this.syncHelper.setState({}, LOCAL_DATA, TEST_CONTAINERS, []);
|
||||
|
||||
await this.webExt.browser.storage.local.set({
|
||||
"identitiesState@@_firefox-container-5": {
|
||||
"hiddenTabs": []
|
||||
}
|
||||
});
|
||||
|
||||
await this.webExt.background.window.identityState.storageArea.upgradeData();
|
||||
|
||||
const macConfigs = await this.webExt.browser.storage.local.get();
|
||||
const identities = [];
|
||||
for(const configKey of Object.keys(macConfigs)) {
|
||||
if (configKey.includes("identitiesState@@_") && !configKey.includes("default")) {
|
||||
identities.push(macConfigs[configKey]);
|
||||
}
|
||||
}
|
||||
|
||||
identities.should.have.lengthOf(5, "There should be 5 identity entries");
|
||||
for (const identity of identities) {
|
||||
(!!identity.macAddonUUID).should.be.true;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
it("testAssignManagerCleanup", async function() {
|
||||
await this.syncHelper.stopSyncListeners();
|
||||
|
||||
await this.syncHelper.setState({}, LOCAL_DATA, TEST_CONTAINERS, TEST_ASSIGNMENTS);
|
||||
|
||||
await this.webExt.browser.storage.local.set({
|
||||
"siteContainerMap@@_www.goop.com": {
|
||||
"userContextId": "999",
|
||||
"neverAsk": true
|
||||
}
|
||||
});
|
||||
|
||||
await this.webExt.background.window.identityState.storageArea.upgradeData();
|
||||
await this.webExt.background.window.assignManager.storageArea.upgradeData();
|
||||
const macConfigs = await this.webExt.browser.storage.local.get();
|
||||
const assignments = [];
|
||||
for(const configKey of Object.keys(macConfigs)) {
|
||||
if (configKey.includes("siteContainerMap@@_")) {
|
||||
macConfigs[configKey].configKey = configKey;
|
||||
assignments.push(macConfigs[configKey]);
|
||||
}
|
||||
}
|
||||
assignments.should.have.lengthOf(5, "There should be 5 site assignments");
|
||||
for (const assignment of assignments) {
|
||||
(!!assignment.identityMacAddonUUID).should.be.true;
|
||||
}
|
||||
});
|
||||
|
||||
it("testReconcileSiteAssignments", async function() {
|
||||
await this.syncHelper.stopSyncListeners();
|
||||
|
||||
await this.syncHelper.setState(
|
||||
DUPE_TEST_SYNC,
|
||||
LOCAL_DATA,
|
||||
TEST_CONTAINERS,
|
||||
SITE_ASSIGNMENT_TEST
|
||||
);
|
||||
|
||||
// add 200ok (bad data).
|
||||
const testSites = {
|
||||
"siteContainerMap@@_developer.mozilla.org": {
|
||||
"userContextId": "588",
|
||||
"neverAsk": true,
|
||||
"identityMacAddonUUID": "d20d7af2-9866-468e-bb43-541efe8c2c2e",
|
||||
"hostname": "developer.mozilla.org"
|
||||
},
|
||||
"siteContainerMap@@_reddit.com": {
|
||||
"userContextId": "592",
|
||||
"neverAsk": true,
|
||||
"identityMacAddonUUID": "3dc916fb-8c0a-4538-9758-73ef819a45f7",
|
||||
"hostname": "reddit.com"
|
||||
},
|
||||
"siteContainerMap@@_twitter.com": {
|
||||
"userContextId": "589",
|
||||
"neverAsk": true,
|
||||
"identityMacAddonUUID": "cdd73c20-c26a-4c06-9b17-735c1f5e9187",
|
||||
"hostname": "twitter.com"
|
||||
},
|
||||
"siteContainerMap@@_www.facebook.com": {
|
||||
"userContextId": "590",
|
||||
"neverAsk": true,
|
||||
"identityMacAddonUUID": "32cc4a9b-05ed-4e54-8e11-732468de62f4",
|
||||
"hostname": "www.facebook.com"
|
||||
},
|
||||
"siteContainerMap@@_www.linkedin.com": {
|
||||
"userContextId": "591",
|
||||
"neverAsk": true,
|
||||
"identityMacAddonUUID": "9ff381e3-4c11-420d-8e12-e352a3318be1",
|
||||
"hostname": "www.linkedin.com"
|
||||
},
|
||||
"siteContainerMap@@_200ok.us": {
|
||||
"userContextId": "1",
|
||||
"neverAsk": true,
|
||||
"identityMacAddonUUID": "b5f5f794-b37e-4cec-9f4e-6490df620336",
|
||||
"hostname": "www.linkedin.com"
|
||||
}
|
||||
};
|
||||
|
||||
for (const site of Object.keys(testSites)) {
|
||||
await this.webExt.browser.storage.sync.set({[site]:testSites[site]});
|
||||
}
|
||||
|
||||
await this.webExt.browser.storage.sync.set({
|
||||
deletedSiteList: ["siteContainerMap@@_www.google.com"]
|
||||
});
|
||||
await this.webExt.background.window.sync.runSync();
|
||||
|
||||
const assignedSites = await this.webExt.background.window.assignManager.storageArea.getAssignedSites();
|
||||
Object.keys(assignedSites).should.have.lengthOf(6);
|
||||
});
|
||||
|
||||
it("testInitialSync", async function() {
|
||||
await this.syncHelper.stopSyncListeners();
|
||||
await this.syncHelper.setState({}, LOCAL_DATA, TEST_CONTAINERS, []);
|
||||
await this.webExt.background.window.sync.runSync();
|
||||
|
||||
const getAssignedSites =
|
||||
await this.webExt.background.window.assignManager.storageArea.getAssignedSites();
|
||||
const identities = await this.webExt.browser.contextualIdentities.query({});
|
||||
|
||||
identities.should.have.lengthOf(5, "There should be 5 identity entries");
|
||||
Object.keys(getAssignedSites).should.have.lengthOf(0, "There should be no site assignments");
|
||||
});
|
||||
|
||||
it("test2", async function() {
|
||||
await this.syncHelper.stopSyncListeners();
|
||||
|
||||
await this.syncHelper.setState(SYNC_DATA, LOCAL_DATA, TEST_CONTAINERS, TEST_ASSIGNMENTS);
|
||||
|
||||
await this.webExt.background.window.sync.runSync();
|
||||
|
||||
const getAssignedSites =
|
||||
await this.webExt.background.window.assignManager.storageArea.getAssignedSites();
|
||||
|
||||
const identities = await this.webExt.browser.contextualIdentities.query({});
|
||||
|
||||
identities.should.have.lengthOf(6, "There should be 6 identity entries");
|
||||
Object.keys(getAssignedSites).should.have.lengthOf(5, "There should be 5 site assignments");
|
||||
});
|
||||
|
||||
it("dupeTest", async function() {
|
||||
await this.syncHelper.stopSyncListeners();
|
||||
await this.syncHelper.setState(
|
||||
DUPE_TEST_SYNC,
|
||||
DUPE_TEST_LOCAL,
|
||||
DUPE_TEST_IDENTS,
|
||||
DUPE_TEST_ASSIGNMENTS
|
||||
);
|
||||
|
||||
await this.webExt.background.window.sync.runSync();
|
||||
|
||||
const getAssignedSites =
|
||||
await this.webExt.background.window.assignManager.storageArea.getAssignedSites();
|
||||
|
||||
const identities = await this.webExt.browser.contextualIdentities.query({});
|
||||
|
||||
identities.should.have.lengthOf(7, "There should be 7 identity entries");
|
||||
|
||||
Object.keys(getAssignedSites).should.have.lengthOf(5, "There should be 5 identity entries");
|
||||
|
||||
const personalContainer =
|
||||
this.syncHelper.lookupIdentityBy(identities, {name: "Personal"});
|
||||
(personalContainer.color === "red").should.be.true;
|
||||
|
||||
const mozillaContainer =
|
||||
this.syncHelper.lookupIdentityBy(identities, {name: "Mozilla"});
|
||||
(mozillaContainer.icon === "pet").should.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
class SyncTestHelper {
|
||||
constructor(webExt) {
|
||||
this.webExt = webExt;
|
||||
}
|
||||
|
||||
async stopSyncListeners() {
|
||||
await this.webExt.browser.storage.onChanged.removeListener(this.webExt.background.window.sync.storageArea.onChangedListener);
|
||||
await this.webExt.background.window.sync.removeContextualIdentityListeners();
|
||||
}
|
||||
|
||||
async setState(syncData, localData, identityData, assignmentData){
|
||||
await this.removeAllContainers();
|
||||
await this.webExt.browser.storage.sync.clear();
|
||||
await this.webExt.browser.storage.sync.set(syncData);
|
||||
await this.webExt.browser.storage.local.clear();
|
||||
await this.webExt.browser.storage.local.set(localData);
|
||||
for (let i=0; i < identityData.length; i++) {
|
||||
//build identities
|
||||
const newIdentity =
|
||||
await this.webExt.browser.contextualIdentities.create(identityData[i]);
|
||||
// fill identies with site assignments
|
||||
if (assignmentData && assignmentData[i]) {
|
||||
const data = {
|
||||
"userContextId":
|
||||
String(
|
||||
newIdentity.cookieStoreId.replace(/^firefox-container-/, "")
|
||||
),
|
||||
"neverAsk": true
|
||||
};
|
||||
|
||||
await this.webExt.browser.storage.local.set({[assignmentData[i]]: data});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async removeAllContainers() {
|
||||
const identities = await this.webExt.browser.contextualIdentities.query({});
|
||||
for (const identity of identities) {
|
||||
await this.webExt.browser.contextualIdentities.remove(identity.cookieStoreId);
|
||||
}
|
||||
}
|
||||
|
||||
lookupIdentityBy(identities, options) {
|
||||
for (const identity of identities) {
|
||||
if (options && options.name) {
|
||||
if (identity.name === options.name) return identity;
|
||||
}
|
||||
if (options && options.color) {
|
||||
if (identity.color === options.color) return identity;
|
||||
}
|
||||
if (options && options.color) {
|
||||
if (identity.color === options.color) return identity;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const TEST_CONTAINERS = [
|
||||
{
|
||||
name: "Personal",
|
||||
color: "blue",
|
||||
icon: "fingerprint"
|
||||
},
|
||||
{
|
||||
name: "Banking",
|
||||
color: "green",
|
||||
icon: "dollar"
|
||||
},
|
||||
{
|
||||
name: "Mozilla",
|
||||
color: "red",
|
||||
icon: "briefcase"
|
||||
},
|
||||
{
|
||||
name: "Groceries, obviously",
|
||||
color: "yellow",
|
||||
icon: "cart"
|
||||
},
|
||||
{
|
||||
name: "Facebook",
|
||||
color: "toolbar",
|
||||
icon: "fence"
|
||||
},
|
||||
];
|
||||
|
||||
const TEST_ASSIGNMENTS = [
|
||||
"siteContainerMap@@_developer.mozilla.org",
|
||||
"siteContainerMap@@_twitter.com",
|
||||
"siteContainerMap@@_www.facebook.com",
|
||||
"siteContainerMap@@_www.linkedin.com",
|
||||
"siteContainerMap@@_reddit.com"
|
||||
];
|
||||
|
||||
const LOCAL_DATA = {
|
||||
"browserActionBadgesClicked": [ "6.2.0" ],
|
||||
"containerTabsOpened": 7,
|
||||
"identitiesState@@_firefox-default": { "hiddenTabs": [] },
|
||||
"onboarding-stage": 5
|
||||
};
|
||||
|
||||
const SYNC_DATA = {
|
||||
"identity@@_22ded543-5173-44a5-a47a-8813535945ca": {
|
||||
"name": "Personal",
|
||||
"icon": "fingerprint",
|
||||
"color": "red",
|
||||
"cookieStoreId": "firefox-container-146",
|
||||
"macAddonUUID": "22ded543-5173-44a5-a47a-8813535945ca"
|
||||
},
|
||||
"identity@@_63e5212f-0858-418e-b5a3-09c2dea61fcd": {
|
||||
"name": "Oscar",
|
||||
"icon": "dollar",
|
||||
"color": "green",
|
||||
"cookieStoreId": "firefox-container-147",
|
||||
"macAddonUUID": "3e5212f-0858-418e-b5a3-09c2dea61fcd"
|
||||
},
|
||||
"identity@@_71335417-158e-4d74-a55b-e9e9081601ec": {
|
||||
"name": "Mozilla",
|
||||
"icon": "pet",
|
||||
"color": "red",
|
||||
"cookieStoreId": "firefox-container-148",
|
||||
"macAddonUUID": "71335417-158e-4d74-a55b-e9e9081601ec"
|
||||
},
|
||||
"identity@@_59c4e5f7-fe3b-435a-ae60-1340db31a91b": {
|
||||
"name": "Groceries, obviously",
|
||||
"icon": "cart",
|
||||
"color": "pink",
|
||||
"cookieStoreId": "firefox-container-149",
|
||||
"macAddonUUID": "59c4e5f7-fe3b-435a-ae60-1340db31a91b"
|
||||
},
|
||||
"identity@@_3dc916fb-8c0a-4538-9758-73ef819a45f7": {
|
||||
"name": "Facebook",
|
||||
"icon": "fence",
|
||||
"color": "toolbar",
|
||||
"cookieStoreId": "firefox-container-150",
|
||||
"macAddonUUID": "3dc916fb-8c0a-4538-9758-73ef819a45f7"
|
||||
}
|
||||
};
|
||||
|
||||
const DUPE_TEST_SYNC = {
|
||||
"identity@@_d20d7af2-9866-468e-bb43-541efe8c2c2e": {
|
||||
"name": "Personal",
|
||||
"icon": "fingerprint",
|
||||
"color": "red",
|
||||
"cookieStoreId": "firefox-container-588",
|
||||
"macAddonUUID": "d20d7af2-9866-468e-bb43-541efe8c2c2e"
|
||||
},
|
||||
"identity@@_cdd73c20-c26a-4c06-9b17-735c1f5e9187": {
|
||||
"name": "Big Bird",
|
||||
"icon": "pet",
|
||||
"color": "yellow",
|
||||
"cookieStoreId": "firefox-container-589",
|
||||
"macAddonUUID": "cdd73c20-c26a-4c06-9b17-735c1f5e9187"
|
||||
},
|
||||
"identity@@_32cc4a9b-05ed-4e54-8e11-732468de62f4": {
|
||||
"name": "Mozilla",
|
||||
"icon": "pet",
|
||||
"color": "red",
|
||||
"cookieStoreId": "firefox-container-590",
|
||||
"macAddonUUID": "32cc4a9b-05ed-4e54-8e11-732468de62f4"
|
||||
},
|
||||
"identity@@_9ff381e3-4c11-420d-8e12-e352a3318be1": {
|
||||
"name": "Groceries, obviously",
|
||||
"icon": "cart",
|
||||
"color": "pink",
|
||||
"cookieStoreId": "firefox-container-591",
|
||||
"macAddonUUID": "9ff381e3-4c11-420d-8e12-e352a3318be1"
|
||||
},
|
||||
"identity@@_3dc916fb-8c0a-4538-9758-73ef819a45f7": {
|
||||
"name": "Facebook",
|
||||
"icon": "fence",
|
||||
"color": "toolbar",
|
||||
"cookieStoreId": "firefox-container-592",
|
||||
"macAddonUUID": "3dc916fb-8c0a-4538-9758-73ef819a45f7"
|
||||
},
|
||||
"identity@@_63e5212f-0858-418e-b5a3-09c2dea61fcd": {
|
||||
"name": "Oscar",
|
||||
"icon": "dollar",
|
||||
"color": "green",
|
||||
"cookieStoreId": "firefox-container-593",
|
||||
"macAddonUUID": "63e5212f-0858-418e-b5a3-09c2dea61fcd"
|
||||
},
|
||||
"siteContainerMap@@_developer.mozilla.org": {
|
||||
"userContextId": "588",
|
||||
"neverAsk": true,
|
||||
"identityMacAddonUUID": "d20d7af2-9866-468e-bb43-541efe8c2c2e",
|
||||
"hostname": "developer.mozilla.org"
|
||||
},
|
||||
"siteContainerMap@@_reddit.com": {
|
||||
"userContextId": "592",
|
||||
"neverAsk": true,
|
||||
"identityMacAddonUUID": "3dc916fb-8c0a-4538-9758-73ef819a45f7",
|
||||
"hostname": "reddit.com"
|
||||
},
|
||||
"siteContainerMap@@_twitter.com": {
|
||||
"userContextId": "589",
|
||||
"neverAsk": true,
|
||||
"identityMacAddonUUID": "cdd73c20-c26a-4c06-9b17-735c1f5e9187",
|
||||
"hostname": "twitter.com"
|
||||
},
|
||||
"siteContainerMap@@_www.facebook.com": {
|
||||
"userContextId": "590",
|
||||
"neverAsk": true,
|
||||
"identityMacAddonUUID": "32cc4a9b-05ed-4e54-8e11-732468de62f4",
|
||||
"hostname": "www.facebook.com"
|
||||
},
|
||||
"siteContainerMap@@_www.linkedin.com": {
|
||||
"userContextId": "591",
|
||||
"neverAsk": true,
|
||||
"identityMacAddonUUID": "9ff381e3-4c11-420d-8e12-e352a3318be1",
|
||||
"hostname": "www.linkedin.com"
|
||||
}
|
||||
};
|
||||
|
||||
const DUPE_TEST_LOCAL = {
|
||||
"beenSynced": true,
|
||||
"browserActionBadgesClicked": [
|
||||
"6.2.0"
|
||||
],
|
||||
"containerTabsOpened": 7,
|
||||
"identitiesState@@_firefox-default": {
|
||||
"hiddenTabs": []
|
||||
},
|
||||
"onboarding-stage": 5,
|
||||
};
|
||||
|
||||
const DUPE_TEST_ASSIGNMENTS = [
|
||||
"siteContainerMap@@_developer.mozilla.org",
|
||||
"siteContainerMap@@_reddit.com",
|
||||
"siteContainerMap@@_twitter.com",
|
||||
"siteContainerMap@@_www.facebook.com",
|
||||
"siteContainerMap@@_www.linkedin.com"
|
||||
];
|
||||
|
||||
const SITE_ASSIGNMENT_TEST = [
|
||||
"siteContainerMap@@_developer.mozilla.org",
|
||||
"siteContainerMap@@_www.facebook.com",
|
||||
"siteContainerMap@@_www.google.com",
|
||||
"siteContainerMap@@_bugzilla.mozilla.org"
|
||||
];
|
||||
|
||||
const DUPE_TEST_IDENTS = [
|
||||
{
|
||||
"name": "Personal",
|
||||
"icon": "fingerprint",
|
||||
"color": "blue",
|
||||
},
|
||||
{
|
||||
"name": "Banking",
|
||||
"icon": "pet",
|
||||
"color": "green",
|
||||
},
|
||||
{
|
||||
"name": "Mozilla",
|
||||
"icon": "briefcase",
|
||||
"color": "red",
|
||||
},
|
||||
{
|
||||
"name": "Groceries, obviously",
|
||||
"icon": "cart",
|
||||
"color": "orange",
|
||||
},
|
||||
{
|
||||
"name": "Facebook",
|
||||
"icon": "fence",
|
||||
"color": "toolbar",
|
||||
},
|
||||
{
|
||||
"name": "Big Bird",
|
||||
"icon": "dollar",
|
||||
"color": "yellow",
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,38 @@
|
||||
const {expect, sinon, initializeWithTab} = require("../common");
|
||||
|
||||
describe("#1168", function () {
|
||||
describe("when navigation happens too slow after opening new tab to a page which then redirects", function () {
|
||||
let clock, tab, background;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.webExt = await initializeWithTab({
|
||||
cookieStoreId: "firefox-container-1",
|
||||
url: "https://bugzilla.mozilla.org"
|
||||
});
|
||||
|
||||
await this.webExt.popup.helper.clickElementById("container-page-assigned");
|
||||
|
||||
clock = sinon.useFakeTimers();
|
||||
tab = await this.webExt.browser.tabs._create({});
|
||||
|
||||
clock.tick(2000);
|
||||
|
||||
await background.browser.tabs._navigate(tab.id, "https://duckduckgo.com/?q=%21bugzilla+thing&t=ffab");
|
||||
await background.browser.tabs._redirect(tab.id, [
|
||||
"https://bugzilla.mozilla.org"
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.webExt.destroy();
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
// Not solved yet
|
||||
// See: https://github.com/mozilla/multi-account-containers/issues/1168#issuecomment-378394091
|
||||
it.skip("should remove the old tab", async function () {
|
||||
expect(background.browser.tabs.create).to.have.been.calledOnce;
|
||||
expect(background.browser.tabs.remove).to.have.been.calledWith(tab.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
const {expect, sinon, initializeWithTab} = require("../common");
|
||||
|
||||
describe("#940", function () {
|
||||
describe("when other onBeforeRequestHandlers are faster and redirect with the same requestId", function () {
|
||||
it("should not open two confirm pages", async function () {
|
||||
const webExtension = await initializeWithTab({
|
||||
cookieStoreId: "firefox-container-1",
|
||||
url: "http://example.com"
|
||||
});
|
||||
|
||||
await webExtension.popup.helper.clickElementById("container-page-assigned");
|
||||
|
||||
const responses = {};
|
||||
await webExtension.background.browser.tabs._create({
|
||||
url: "https://example.com"
|
||||
}, {
|
||||
options: {
|
||||
webRequestRedirects: ["https://example.com"],
|
||||
webRequestError: true,
|
||||
instantRedirects: true
|
||||
},
|
||||
responses
|
||||
});
|
||||
|
||||
const result = await responses.webRequest.onBeforeRequest[1];
|
||||
expect(result).to.deep.equal({
|
||||
cancel: true
|
||||
});
|
||||
webExtension.browser.tabs.create.should.have.been.calledOnce;
|
||||
|
||||
webExtension.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when redirects change requestId midflight", function () {
|
||||
beforeEach(async function () {
|
||||
|
||||
this.webExt = await initializeWithTab({
|
||||
cookieStoreId: "firefox-container-1",
|
||||
url: "https://www.youtube.com"
|
||||
});
|
||||
await this.webExt.popup.helper.clickElementById("container-page-assigned");
|
||||
|
||||
global.clock = sinon.useFakeTimers();
|
||||
this.redirectedRequest = async (options = {}) => {
|
||||
const newTabResponses = {};
|
||||
const newTab = await this.webExt.browser.tabs._create({
|
||||
url: "http://youtube.com"
|
||||
}, {
|
||||
options: Object.assign({
|
||||
webRequestRedirects: [
|
||||
"https://youtube.com",
|
||||
"https://www.youtube.com",
|
||||
{
|
||||
url: "https://www.youtube.com",
|
||||
webRequest: {
|
||||
requestId: 2
|
||||
}
|
||||
}
|
||||
],
|
||||
webRequestError: true,
|
||||
instantRedirects: true
|
||||
}, options),
|
||||
responses: newTabResponses
|
||||
});
|
||||
|
||||
return [newTabResponses, newTab];
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
this.webExt.destroy();
|
||||
global.clock.restore();
|
||||
});
|
||||
|
||||
it("should not open two confirm pages", async function () {
|
||||
const [newTabResponses] = await this.redirectedRequest();
|
||||
|
||||
// http://youtube.com is not assigned, no cancel, no reopening
|
||||
expect(await newTabResponses.webRequest.onBeforeRequest[0]).to.deep.equal({});
|
||||
|
||||
// https://youtube.com is not assigned, no cancel, no reopening
|
||||
expect(await newTabResponses.webRequest.onBeforeRequest[1]).to.deep.equal({});
|
||||
|
||||
// https://www.youtube.com is assigned, this triggers reopening, cancel
|
||||
expect(await newTabResponses.webRequest.onBeforeRequest[2]).to.deep.equal({
|
||||
cancel: true
|
||||
});
|
||||
|
||||
// https://www.youtube.com is assigned, this was a redirect, cancel early, no reopening
|
||||
expect(await newTabResponses.webRequest.onBeforeRequest[3]).to.deep.equal({
|
||||
cancel: true
|
||||
});
|
||||
|
||||
this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it("should uncancel after webRequest.onCompleted", async function () {
|
||||
const [newTabResponses, newTab] = await this.redirectedRequest();
|
||||
// remove onCompleted listeners because in the real world this request would never complete
|
||||
// and thus might trigger unexpected behavior because the tab gets removed when reopening
|
||||
this.webExt.background.browser.webRequest.onCompleted.addListener = sinon.stub();
|
||||
this.webExt.background.browser.tabs.create.resetHistory();
|
||||
// we create a tab with the same id and use the same request id to see if uncanceled
|
||||
await this.webExt.browser.tabs._create({
|
||||
id: newTab.id,
|
||||
url: "https://www.youtube.com"
|
||||
}, {
|
||||
options: {
|
||||
webRequest: {
|
||||
requestId: newTabResponses.webRequest.request.requestId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it("should uncancel after webRequest.onErrorOccurred", async function () {
|
||||
const [newTabResponses, newTab] = await this.redirectedRequest();
|
||||
this.webExt.background.browser.tabs.create.resetHistory();
|
||||
// we create a tab with the same id and use the same request id to see if uncanceled
|
||||
await this.webExt.browser.tabs._create({
|
||||
id: newTab.id,
|
||||
url: "https://www.youtube.com"
|
||||
}, {
|
||||
options: {
|
||||
webRequest: {
|
||||
requestId: newTabResponses.webRequest.request.requestId
|
||||
},
|
||||
webRequestError: true
|
||||
}
|
||||
});
|
||||
|
||||
this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it("should uncancel after 2 seconds", async function () {
|
||||
const [newTabResponses, newTab] = await this.redirectedRequest({
|
||||
webRequestDontYield: ["onCompleted", "onErrorOccurred"]
|
||||
});
|
||||
global.clock.tick(2000);
|
||||
|
||||
this.webExt.background.browser.tabs.create.resetHistory();
|
||||
// we create a tab with the same id and use the same request id to see if uncanceled
|
||||
await this.webExt.browser.tabs._create({
|
||||
id: newTab.id,
|
||||
url: "https://www.youtube.com"
|
||||
}, {
|
||||
options: {
|
||||
webRequest: {
|
||||
requestId: newTabResponses.webRequest.request.requestId
|
||||
},
|
||||
webRequestError: true
|
||||
}
|
||||
});
|
||||
|
||||
this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it("should not influence the canceled url in other tabs", async function () {
|
||||
await this.redirectedRequest();
|
||||
this.webExt.background.browser.tabs.create.resetHistory();
|
||||
await this.webExt.browser.tabs._create({
|
||||
cookieStoreId: "firefox-default",
|
||||
url: "https://www.youtube.com"
|
||||
}, {
|
||||
options: {
|
||||
webRequestError: true
|
||||
}
|
||||
});
|
||||
|
||||
this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
});
|
||||