Compare commits
450 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| bea201a389 | |||
| d944116e3e | |||
| 4a1597c87f | |||
| f87bf2a861 | |||
| ef45cde290 | |||
| 752b1c3b27 | |||
| cf26d8547a | |||
| 1d78febafc | |||
| f483119a40 | |||
| abd2b73fca | |||
| 31ac365e6d | |||
| df8471a4dd | |||
| 18539f2540 | |||
| 7c1105a2b7 | |||
| 31298146f3 | |||
| 4e6eee220c | |||
| a7be3c9935 | |||
| f512473986 | |||
| 8166a37722 | |||
| adadb98482 | |||
| 25e760cd64 | |||
| 0ff8e17005 | |||
| c433c6b39e | |||
| 1c09c29104 | |||
| c1e9cc3c56 | |||
| 27296d24c5 | |||
| 030e635417 | |||
| 07711aaecc | |||
| 16ed8992e2 | |||
| 88e6dc7a05 | |||
| 28e8d46743 | |||
| 77ba1b723f | |||
| b0cc6e7c2f | |||
| e84e482130 | |||
| b5ae20b874 | |||
| 3ec81e3d1f | |||
| fb5436c287 | |||
| 01a628822b | |||
| 66e2c8e297 | |||
| 80661d68f2 | |||
| ef8aa3be75 | |||
| 6bc056e019 | |||
| 75deab139b | |||
| ae79f0a303 | |||
| 9b83068234 | |||
| fec2be9429 | |||
| 9f1b06ddd3 | |||
| ad2198e8b5 | |||
| 1791fdf0ef | |||
| 4ab705081e | |||
| 15b9dce1a9 | |||
| da3fc2ede2 | |||
| 385c585888 | |||
| 734b97beb0 | |||
| 3aa311a3c1 | |||
| df7d7f9c38 | |||
| 65be77665a | |||
| 44548659db | |||
| 10c4395efd | |||
| 27b2a4b5f2 | |||
| 6f54e7ff7f | |||
| cb6726b667 | |||
| 0964311fa1 | |||
| 2831d019f5 | |||
| 1cc58cad9b | |||
| bc9660f76e | |||
| f526caca50 | |||
| b6a98fb83e | |||
| af2b4b79a9 | |||
| a762b5eca2 | |||
| 3cc40344af | |||
| 278cdb7f69 | |||
| 0be03ebeb7 | |||
| c69f37a2de | |||
| b20ac8169a | |||
| 9e98d35b45 | |||
| 17f2781e07 | |||
| 0acf9cc0e6 | |||
| 70573d0559 | |||
| da239237f7 | |||
| 29f078d2c9 | |||
| 5704a21c97 | |||
| 57a31f7f97 | |||
| d685a58d74 | |||
| a44bf21582 | |||
| 8f8fc322eb | |||
| f03404ad9e | |||
| 78b5de3b44 | |||
| e78f49bec5 | |||
| ee8c69b73e | |||
| 4dfffb8c34 | |||
| fe77c891cd | |||
| e3ed4582d2 | |||
| 38c098edb6 | |||
| 12a6bb3b9b | |||
| 175cdc1a6b | |||
| 0ec7e4aee3 | |||
| ee63f72f3e | |||
| 4cbcfac0f4 | |||
| 641c95e64e | |||
| b646a9183c | |||
| 6e2ed6393e | |||
| af98174a19 | |||
| 1c8530ef02 | |||
| 366f9ec047 | |||
| 63343f18eb | |||
| 080e9dd22d | |||
| 268da1350a | |||
| e0abaa67e2 | |||
| 69f06f96cc | |||
| 62d479a3f3 | |||
| 315c75f2ac | |||
| 9fcf822140 | |||
| e7ac72a6a2 | |||
| da39d18ce0 | |||
| 63025ab3d6 | |||
| 1c38e09dcc | |||
| 2178e26220 | |||
| 310e2fa503 | |||
| 30c55e093c | |||
| d0037d1377 | |||
| 2a3aa296c0 | |||
| 10e83d3795 | |||
| 29b9590878 | |||
| a86bcf7983 | |||
| e28b25e04c | |||
| 1cc3ab83b9 | |||
| 0566c9f962 | |||
| 8c92d8ef5d | |||
| a8cac47125 | |||
| e191255c47 | |||
| 7eb752c2f7 | |||
| 40f2f1af5e | |||
| 214a83deda | |||
| 51b804f96d | |||
| 83e8340a70 | |||
| 6292d9b25d | |||
| 2f5e195c91 | |||
| af966d6d29 | |||
| 4ed136299b | |||
| 5237e67fa6 | |||
| 13cd601212 | |||
| bc847b53f5 | |||
| 4e0180d521 | |||
| 13e4b4e7f7 | |||
| 2278498b06 | |||
| 6533c74d0a | |||
| 9b0fe826de | |||
| 5d75d4525d | |||
| 59f2b8a764 | |||
| 68c21624e2 | |||
| bfc6f68978 | |||
| 78ef2e8304 | |||
| 5b85fc1690 | |||
| be8f6bbe7c | |||
| dfd420d1a5 | |||
| 4030b6eeec | |||
| d2b4d972e1 | |||
| 06d381b931 | |||
| c2ed5420a4 | |||
| fc789a49ac | |||
| bf75f52a52 | |||
| 090ae1f139 | |||
| cd2e110c17 | |||
| d63e887ef7 | |||
| ea0c9d4306 | |||
| e5a87ab535 | |||
| 3b9da05e67 | |||
| 5c5cf02249 | |||
| 8503e9c9c5 | |||
| 7f37ed906f | |||
| 15477dc384 | |||
| 06d35e65ce | |||
| 49e8afaf9a | |||
| 9903e811c2 | |||
| e467988a71 | |||
| 094a0e2391 | |||
| df8bf4e5e4 | |||
| 45f34a586a | |||
| ab2b9a48c7 | |||
| 82c9cac34c | |||
| 5cd2ac0187 | |||
| 0f9dd77687 | |||
| fb845cce12 | |||
| a29fae0893 | |||
| bd72b4e759 | |||
| 4f6e91336f | |||
| 69d497bacd | |||
| d3413c7afc | |||
| bb96ab9c37 | |||
| 6a10c1c970 | |||
| 1d385d2aaa | |||
| fd918408f4 | |||
| 08ba094748 | |||
| 5916bd2871 | |||
| 3700e6f461 | |||
| a99cd9c8ef | |||
| dad3214986 | |||
| 099d07bf1f | |||
| 93b6378b22 | |||
| 84dd73bff5 | |||
| b0c53063d2 | |||
| 1819e6cde9 | |||
| 54c598e22e | |||
| e499ff5711 | |||
| cd03ea7a59 | |||
| 46b155c90f | |||
| 31f3a76a1c | |||
| 4ffb587d9e | |||
| 4d61fa190c | |||
| 4a97e07d43 | |||
| 41df7a10dc | |||
| 5089091617 | |||
| 54ccf5b9ec | |||
| ee98034572 | |||
| d8fd47a353 | |||
| 02300630f6 | |||
| 3805f12e17 | |||
| 4a48a7debb | |||
| 26e6efd611 | |||
| 92ab56448c | |||
| ac02c45e81 | |||
| 1ec86c7fd2 | |||
| 8f80b527f5 | |||
| 337dee2061 | |||
| f2ddc7fd84 | |||
| f4597eae84 | |||
| 5887f991b3 | |||
| 1de3f42385 | |||
| 91ec0c4a6d | |||
| 0b1d6e9087 | |||
| 2ee9682950 |
@@ -1 +1,2 @@
|
||||
testpilot-metrics.js
|
||||
lib/testpilot/*.js
|
||||
coverage
|
||||
@@ -1,22 +1,31 @@
|
||||
module.exports = {
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"webextensions": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"globals": {
|
||||
"Utils": true,
|
||||
"CustomizableUI": true,
|
||||
"CustomizableWidgets": true,
|
||||
"SessionStore": true,
|
||||
"Services": true
|
||||
"Services": true,
|
||||
"Components": true,
|
||||
"XPCOMUtils": true,
|
||||
"OS": true,
|
||||
"ADDON_UNINSTALL": true,
|
||||
"ADDON_DISABLE": true
|
||||
},
|
||||
"plugins": [
|
||||
"promise",
|
||||
"no-unescaped"
|
||||
"no-unsanitized"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"root": true,
|
||||
"rules": {
|
||||
@@ -29,8 +38,18 @@ module.exports = {
|
||||
"promise/no-promise-in-callback": "warn",
|
||||
"promise/no-return-wrap": "error",
|
||||
"promise/param-names": "error",
|
||||
"no-unescaped/no-key-assignment": "error",
|
||||
"no-unescaped/enforce": "error",
|
||||
|
||||
"no-unsanitized/method": [
|
||||
"error"
|
||||
],
|
||||
"no-unsanitized/property": [
|
||||
"error",
|
||||
{
|
||||
"escape": {
|
||||
"taggedTemplates": ["escaped"]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"eqeqeq": "error",
|
||||
"indent": ["error", 2],
|
||||
|
||||
@@ -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
|
||||
@@ -1,8 +1,18 @@
|
||||
.DS_Store
|
||||
package-lock.json
|
||||
node_modules
|
||||
README.html
|
||||
*.xpi
|
||||
*.swp
|
||||
*.sw*
|
||||
.vimrc
|
||||
.env
|
||||
addon.env
|
||||
|
||||
src/web-ext-artifacts/*
|
||||
|
||||
# JetBrains IDE files
|
||||
.idea
|
||||
|
||||
# IstanbulJS
|
||||
.nyc_output
|
||||
coverage
|
||||
@@ -3,6 +3,7 @@ docs/
|
||||
test/
|
||||
.npm/
|
||||
node_modules/
|
||||
bin/
|
||||
|
||||
.env
|
||||
.eslintrc.js
|
||||
@@ -14,6 +15,8 @@ node_modules/
|
||||
.stylelintrc
|
||||
.travis.yml
|
||||
*.xpi
|
||||
*.md
|
||||
.vimrc
|
||||
.DS_Store
|
||||
.gdb_history
|
||||
*.sw*
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
|
||||
"extends": "stylelint-config-standard",
|
||||
|
||||
"ignoreFiles": ["webextension/css/*.min.css"],
|
||||
"ignoreFiles": ["src/css/*.min.css"],
|
||||
|
||||
"rules": {
|
||||
"declaration-block-no-duplicate-properties": true,
|
||||
"order/declaration-block-properties-alphabetical-order": true,
|
||||
"property-blacklist": [
|
||||
"/height/",
|
||||
"/(min[-]|max[-])height/",
|
||||
"/width/",
|
||||
"/top/",
|
||||
"/bottom/",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6.1"
|
||||
- "lts/*"
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Community Participation Guidelines
|
||||
|
||||
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.
|
||||
-->
|
||||
@@ -0,0 +1,35 @@
|
||||
# Contributing
|
||||
|
||||
Everyone is welcome to contribute to containers. Reach out to team members if you have questions:
|
||||
|
||||
- IRC: #containers on irc.mozilla.org
|
||||
- Email: containers@mozilla.com
|
||||
|
||||
## Filing bugs
|
||||
|
||||
If you find a bug with containers, please file a issue.
|
||||
|
||||
Check first if the bug might already exist: https://github.com/mozilla/multi-account-containers/issues
|
||||
|
||||
[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:
|
||||
- Application Basics
|
||||
- Nightly Features (if you are in nightly)
|
||||
- Extensions
|
||||
- Experimental Features
|
||||
3. Include clear steps to reproduce the issue you have experienced.
|
||||
4. Include screenshots if possible.
|
||||
|
||||
## Sending Pull Requests
|
||||
|
||||
Patches should be submitted as pull requests. When submitting patches as PRs:
|
||||
|
||||
- You agree to license your code under the project's open source license (MPL 2.0).
|
||||
- Base your branch off the current master (see below for an example workflow).
|
||||
- Add both your code and new tests if relevant.
|
||||
- Run npm test to make sure all tests still pass.
|
||||
- Please do not include merge commits in pull requests; include only commits with the new relevant code.
|
||||
|
||||
See the main [README](./README.md) for information on prerequisites, installing, running and testing.
|
||||
@@ -1,80 +1,64 @@
|
||||
# Containers: Test Pilot Experiment
|
||||
# Multi-Account Containers
|
||||
|
||||
[](https://testpilot.firefox.com/experiments/containers)
|
||||
The Firefox Multi-Account Containers extension lets you carve out a separate box for each of your online lives – no more opening a different browser just to check your work email! [Learn More Here](https://blog.mozilla.org/firefox/introducing-firefox-multi-account-containers/)
|
||||
|
||||
[Embedded Web Extension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Embedded_WebExtensions) to experiment with [Containers](https://blog.mozilla.org/tanvi/2016/06/16/contextual-identities-on-the-web/) in [Firefox Test Pilot](https://testpilot.firefox.com/) to learn:
|
||||
[Available on addons.mozilla.org](https://addons.mozilla.org/en-GB/firefox/addon/multi-account-containers/)
|
||||
|
||||
* Will a general Firefox audience understand the Containers feature?
|
||||
* Is the UI as currently implemented in Nightly clear or discoverable?
|
||||
For more info, see:
|
||||
|
||||
See [the Product Hypothesis Document for more
|
||||
details](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit?ts=5824ba12#).
|
||||
* [Test Pilot Product Hypothesis Document](https://docs.google.com/document/d/1WQdHTVXROk7dYkSFluc6_hS44tqZjIrG9I-uPyzevE8/edit#)
|
||||
* [Shield Product Hypothesis Document](https://docs.google.com/document/d/1vMD-fH_5hGDDqNvpRZk12_RhCN2WAe4_yaBamaNdtik/edit#)
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
* node 7+ (for jpm)
|
||||
* Firefox 51+
|
||||
|
||||
|
||||
## Run it
|
||||
|
||||
See Development
|
||||
* Firefox 57+
|
||||
|
||||
|
||||
## Development
|
||||
### Development Environment
|
||||
|
||||
Add-on development is better with [a particular environment](https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment). One simple way to get that environment set up is to install the [DevPrefs add-on](https://addons.mozilla.org/en-US/firefox/addon/devprefs/). You can make a custom Firefox profile that includes the DevPrefs add-on, and use that profile when you run the code in this repository.
|
||||
|
||||
|
||||
1. Make a new profile by running `/path/to/firefox -P`, which launches the profile editor. "Create Profile" -- name it whatever you wish (e.g. 'addon_dev') and store it in the default location. It's probably best to deselect the option to "Use without asking," since you probably don't want to use this as your default profile.
|
||||
|
||||
2. Once you've created your profile, click "Start Firefox". A new instance of Firefox should launch. Go to Tools->Add-ons and search for "DevPrefs". Install it. Quit Firefox.
|
||||
|
||||
3. Now you have a new, vanilla Firefox profile with the DevPrefs add-on installed. You can use your new profile with the code in _this_ repository like so:
|
||||
|
||||
**Beta building**
|
||||
|
||||
To build this for 51 beta just using the downloaded version of beta will not work as XPI signature checking is disabled fully.
|
||||
|
||||
The only way to run the experiment is using an [unbranded version build](https://wiki.mozilla.org/Add-ons/Extension_Signing#Unbranded_Builds) or to build beta yourself:
|
||||
|
||||
1. [Download the mozilla-beta repo](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Source_Code/Mercurial#mozilla-beta_(prerelease_development_tree))
|
||||
2. [Create a mozconfig file](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Configuring_Build_Options) - probably optional
|
||||
3. `cd <reponame>`
|
||||
3. `./mach bootstrap`
|
||||
4. `./mach build`
|
||||
5. Follow the above instructions by creating the new profile via: `~/<reponame>/obj-x86_64-pc-linux-gnu/dist/bin/firefox -P` (Where "obj-x86_64-pc-linux-gnu" may be different depending on platform obj-...)
|
||||
|
||||
|
||||
### Run with jpm
|
||||
|
||||
1. `git clone git@github.com:mozilla/testpilot-containers.git`
|
||||
2. `cd testpilot-containers`
|
||||
3. `npm install`
|
||||
4. `./node_modules/.bin/jpm run -p /Path/To/Firefox/Profiles/{junk}.addon_dev -b FirefoxBeta` (where FirefoxBeta might be: ~/<reponame>/obj-x86_64-pc-linux-gnu/dist/bin/firefox or ~/<downloadedFirefoxBeta>/firefox)
|
||||
|
||||
Check out the [Browser Toolbox](https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox) for more information about debugging add-on code.
|
||||
|
||||
|
||||
### Building .xpi
|
||||
|
||||
To build a local .xpi, use the plain [`jpm
|
||||
xpi`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_xpi) command.
|
||||
|
||||
### Signing an .xpi
|
||||
|
||||
To sign an .xpi, use [`jpm
|
||||
sign`](https://developer.mozilla.org/en-US/Add-ons/SDK/Tools/jpm#jpm_sign)
|
||||
command.
|
||||
|
||||
Note: You will need to be [an author on the AMO
|
||||
add-on](https://addons.mozilla.org/en-US/developers/addon/containers-experiment/ownership).
|
||||
1. `npm install`
|
||||
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
|
||||
TBD
|
||||
#### Make the new version
|
||||
|
||||
1. Bump the version number in `package.json` and `manifest.json`
|
||||
2. Commit the version number bump
|
||||
3. Create a git tag for the version: `git tag <version>`
|
||||
4. Push the tag up to GitHub: `git push --tags`
|
||||
|
||||
#### Publish to AMO
|
||||
|
||||
1. `npm run-script build`
|
||||
2. [Upload the `.zip` to AMO](https://addons.mozilla.org/en-US/developers/addon/multi-account-containers/versions/submit/)
|
||||
|
||||
#### Publish to GitHub
|
||||
Finally, we also publish the release to GitHub for those followers.
|
||||
|
||||
1. Download the signed `.xpi` from [the addon versions page](https://addons.mozilla.org/en-US/developers/addon/multi-account-containers/versions)
|
||||
2. [Make the new release on
|
||||
GitHub](https://github.com/mozilla/multi-account-containers/releases/new)
|
||||
* Use the version number for "Tag version" and "Release title"
|
||||
* Release notes: copy the output of `git log --no-merges --pretty=format:"%h %s" <previous-version>..<new-version>`
|
||||
* Attach binaries: select the signed `.xpi` file
|
||||
|
||||
### Links
|
||||
|
||||
Facebook & Twitter icons CC-Attrib https://fairheadcreative.com.
|
||||
|
||||
- [License](./LICENSE.txt)
|
||||
- [Contributing](./CONTRIBUTING.md)
|
||||
- [Code Of Conduct](./CODE_OF_CONDUCT.md)
|
||||
|
||||
@@ -1,108 +1,3 @@
|
||||
/* HACK: Custom Container vars do not propigate correctly
|
||||
until the container tab is blurred and refocused,
|
||||
adding the data-identity-color with the default hex
|
||||
value, or chrome url path as an alternate selector mitiages this bug.*/
|
||||
[data-identity-color="blue"],
|
||||
[data-identity-color="#00a7e0"] {
|
||||
--identity-tab-color: #37adff;
|
||||
--identity-icon-color: #37adff;
|
||||
}
|
||||
|
||||
[data-identity-color="turquoise"],
|
||||
[data-identity-color="#01bdad"] {
|
||||
--identity-tab-color: #00c79a;
|
||||
--identity-icon-color: #00c79a;
|
||||
}
|
||||
|
||||
[data-identity-color="green"],
|
||||
[data-identity-color="#7dc14c"] {
|
||||
--identity-tab-color: #51cd00;
|
||||
--identity-icon-color: #51cd00;
|
||||
}
|
||||
|
||||
[data-identity-color="yellow"],
|
||||
[data-identity-color="#ffcb00"] {
|
||||
--identity-tab-color: #ffcb00;
|
||||
--identity-icon-color: #ffcb00;
|
||||
}
|
||||
|
||||
[data-identity-color="orange"],
|
||||
[data-identity-color="#f89c24"] {
|
||||
--identity-tab-color: #ff9f00;
|
||||
--identity-icon-color: #ff9f00;
|
||||
}
|
||||
|
||||
[data-identity-color="red"],
|
||||
[data-identity-color="#d92215"] {
|
||||
--identity-tab-color: #ff613d;
|
||||
--identity-icon-color: #ff613d;
|
||||
}
|
||||
|
||||
[data-identity-color="pink"],
|
||||
[data-identity-color="#ee5195"] {
|
||||
--identity-tab-color: #ff4bda;
|
||||
--identity-icon-color: #ff4bda;
|
||||
}
|
||||
|
||||
[data-identity-color="purple"],
|
||||
[data-identity-color="#7a2f7a"] {
|
||||
--identity-tab-color: #af51f5;
|
||||
--identity-icon-color: #af51f5;
|
||||
}
|
||||
|
||||
[data-identity-icon="fingerprint"],
|
||||
[data-identity-icon="chrome://browser/skin/usercontext/personal.svg"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#fingerprint");
|
||||
}
|
||||
|
||||
[data-identity-icon="briefcase"],
|
||||
[data-identity-icon="chrome://browser/skin/usercontext/work.svg"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#briefcase");
|
||||
}
|
||||
|
||||
[data-identity-icon="dollar"],
|
||||
[data-identity-icon="chrome://browser/skin/usercontext/banking.svg"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#dollar");
|
||||
}
|
||||
|
||||
[data-identity-icon="cart"],
|
||||
[data-identity-icon="chrome://browser/skin/usercontext/cart.svg"],
|
||||
[data-identity-icon="chrome://browser/skin/usercontext/shopping.svg"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#cart");
|
||||
}
|
||||
|
||||
[data-identity-icon="circle"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#circle");
|
||||
}
|
||||
|
||||
[data-identity-icon="gift"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#gift");
|
||||
}
|
||||
|
||||
[data-identity-icon="vacation"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#vacation");
|
||||
}
|
||||
|
||||
[data-identity-icon="food"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#food");
|
||||
}
|
||||
|
||||
[data-identity-icon="fruit"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#fruit");
|
||||
}
|
||||
|
||||
[data-identity-icon="pet"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#pet");
|
||||
}
|
||||
|
||||
[data-identity-icon="tree"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#tree");
|
||||
}
|
||||
|
||||
[data-identity-icon="chill"] {
|
||||
--identity-icon: url("resource://testpilot-containers/data/usercontext.svg#chill");
|
||||
}
|
||||
|
||||
#userContext-indicator {
|
||||
height: 16px;
|
||||
list-style-image: none !important;
|
||||
@@ -129,19 +24,6 @@ value, or chrome url path as an alternate selector mitiages this bug.*/
|
||||
display: none;
|
||||
}
|
||||
|
||||
.userContext-icon,
|
||||
.menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon,
|
||||
.subviewbutton[usercontextid] > .toolbarbutton-icon,
|
||||
#userContext-indicator {
|
||||
background-image: var(--identity-icon) !important;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
fill: var(--identity-icon-color) !important;
|
||||
filter: url(/img/filters.svg#fill);
|
||||
filter: url(resource://testpilot-containers/data/filters.svg#fill);
|
||||
}
|
||||
|
||||
/* containers experiment */
|
||||
|
||||
/* reset nightly containers */
|
||||
@@ -200,7 +82,7 @@ special cases are addressed below */
|
||||
}
|
||||
|
||||
#new-tab-overlay {
|
||||
--icon-size: 26px;
|
||||
--icon-size: 16px;
|
||||
-moz-appearance: none;
|
||||
background: transparent;
|
||||
font-style: -moz-use-system-font;
|
||||
@@ -252,8 +134,8 @@ special cases are addressed below */
|
||||
}
|
||||
|
||||
#new-tab-overlay .menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon {
|
||||
block-height: var(--icon-size);
|
||||
block-width: var(--icon-size);
|
||||
block-size: var(--icon-size);
|
||||
inline-size: var(--icon-size);
|
||||
}
|
||||
|
||||
.menuitem-iconic[data-usercontextid] > .menu-iconic-left {
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
# METRICS
|
||||
|
||||
## Data Analysis
|
||||
The collected data will primarily be used to answer the following questions.
|
||||
Images are used for visualization and are not composed of actual data.
|
||||
|
||||
### Do users install and run this?
|
||||
|
||||
What is the overall engagement of the Containers experiment?
|
||||
**This is the standard Daily Active User (DAU) and Monthly Active User (MAU) analysis.**
|
||||
|
||||
This captures data from the users who have the add-on installed, regardless of
|
||||
whether they are actively interacting with it.
|
||||
|
||||

|
||||
|
||||
### Immediate Questions
|
||||
|
||||
* Do people use the containers feature & how do people create new container tabs?
|
||||
* Click to create new container tab
|
||||
* \+ `entry-point` value: "tab-bar" or "pop-up"
|
||||
* Do people who use the containers feature continue to use it?
|
||||
* Retention: opening a second container tab (second tab in the same container, or a tab in a second container?)
|
||||
* What containers do people use?
|
||||
* userContextId
|
||||
* \+ Number of tabs in the container (when should we measure this? on every tab open?)
|
||||
* Do people edit their containers?
|
||||
* Click on "Edit Containers"
|
||||
* Click to edit a single container
|
||||
* Click "OK"
|
||||
* Click to delete a single container
|
||||
* Click "OK"
|
||||
* Click to add a container
|
||||
* Click "OK"
|
||||
* Do people sort the tabs?
|
||||
* Click sort
|
||||
* \+ Number of tabs when clicked
|
||||
* Average number of container tabs when sort was clicked
|
||||
* Do users show and hide container tabs?
|
||||
* Click hide
|
||||
* \+ Number of tabs when clicked
|
||||
* \+ Number of hidden containers when clicked
|
||||
* Click show
|
||||
* \+ Number of tabs when clicked
|
||||
* \+ Number of shown containers when clicked
|
||||
* Do users move container tabs to new windows?
|
||||
* Click move
|
||||
* \+ Number of tabs when clicked
|
||||
* Average number of container tabs when new window was clicked
|
||||
* How many containers do users have hidden at the same time? (when should we measure this? each time a container is hidden?)
|
||||
* Do users pin container tabs? (do we have existing Telemetry for pinning?)
|
||||
* Do users visit more pages in container tabs than non-container tabs?
|
||||
|
||||
### Follow-up Questions
|
||||
|
||||
What are some follow-up questions we anticipate we will ask based on any of the
|
||||
above answers/data?
|
||||
|
||||
* What is the average lifespan of a container tab? Is that longer or shorter than a regular tab? (if we don't have data on the latter, the former probably isn't worth gathering data on since we will have nothing to compare it to).
|
||||
|
||||
## Data Collection
|
||||
|
||||
### Server Side
|
||||
There is currently no server side component to Containers.
|
||||
|
||||
### Client Side
|
||||
Containers will use Test Pilot Telemetry with no batching of data. Details
|
||||
of when pings are sent are below, along with examples of the `payload` portion
|
||||
of a `testpilottest` telemetry ping for each scenario.
|
||||
|
||||
* The user clicks on a container name to open a tab in that container
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"userContextId": <userContextId>,
|
||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
||||
"event": "open-tab",
|
||||
"eventSource": ["tab-bar"|"pop-up"|"file-menu"]
|
||||
}
|
||||
```
|
||||
|
||||
* The user clicks "Edit Containers" in the pop-up
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"event": "edit-containers"
|
||||
}
|
||||
```
|
||||
|
||||
* The user clicks OK after clicking on a container edit icon in the pop-up
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"userContextId": <userContextId>,
|
||||
"event": "edit-container"
|
||||
}
|
||||
```
|
||||
|
||||
* The user clicks OK after clicking on a container delete icon in the pop-up
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"userContextId": <userContextId>,
|
||||
"event": "delete-container"
|
||||
}
|
||||
```
|
||||
|
||||
* The user clicks OK after clicking to add a container in the pop-up
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"event": "add-container"
|
||||
}
|
||||
```
|
||||
|
||||
* The user clicks the sort button/icon in the pop-up
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"event": "sort-tabs",
|
||||
"shownContainersCount": <number-of-containers-with-tabs-shown>,
|
||||
"totalContainerTabsCount": <number-of-all-container-tabs>,
|
||||
"totalNonContainerTabsCount": <number-of-all-non-container-tabs>
|
||||
}
|
||||
```
|
||||
|
||||
* The user clicks "Hide these container tabs" in the popup
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"userContextId": <userContextId>,
|
||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
||||
"event": "hide-tabs",
|
||||
"hiddenContainersCount": <number-of-containers-with-tabs-hidden>,
|
||||
"shownContainersCount": <number-of-containers-with-tabs-shown>,
|
||||
"totalContainersCount": <number-of-containers-with-tabs-hidden-or-shown>
|
||||
}
|
||||
```
|
||||
|
||||
* The user clicks "Show these container tabs" in the popup
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"userContextId": <userContextId>,
|
||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
||||
"event": "show-tabs",
|
||||
"hiddenContainersCount": <number-of-containers-with-tabs-hidden>,
|
||||
"shownContainersCount": <number-of-containers-with-tabs-shown>,
|
||||
"totalContainersCount": <number-of-containers-with-tabs-hidden-or-shown>
|
||||
}
|
||||
```
|
||||
|
||||
* The user clicks "Move tabs to a new window" in the popup
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"userContextId": <userContextId>,
|
||||
"clickedContainerTabCount": <number-of-tabs-in-the-container>,
|
||||
"event": "move-tabs-to-window"
|
||||
}
|
||||
```
|
||||
|
||||
* When a user encounters the disabled "move" feature because of incompatible add-ons
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"event": "incompatible-addons-detected"
|
||||
}
|
||||
```
|
||||
|
||||
* The user closes a tab
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"userContextId": <userContextId>,
|
||||
"event": "page-requests-completed-per-tab",
|
||||
"pageRequestCount": <pageRequestCount>
|
||||
}
|
||||
```
|
||||
|
||||
* The user goes idle
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"userContextId": <userContextId>,
|
||||
"event": "page-requests-completed-per-activity",
|
||||
"pageRequestCount": <pageRequestCount>
|
||||
}
|
||||
```
|
||||
|
||||
* The user chooses "Always Open in this Container" context menu option. (Note: We send two separate event names: one for assigning a site to a container, one for removing a site from a container.)
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"userContextId": <userContextId>,
|
||||
"event": "[added|removed]-container-assignment"
|
||||
}
|
||||
```
|
||||
|
||||
* Firefox prompts the user to reload a site into a container after the user picked "Always Open in this Container".
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"userContextId": <userContextId>,
|
||||
"event": "prompt-reload-page-in-container"
|
||||
}
|
||||
```
|
||||
|
||||
* The user clicks "Take me there" to reload a site into a container after the user picked "Always Open in this Container".
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"event": "click-to-reload-page-in-container"
|
||||
}
|
||||
```
|
||||
|
||||
* Firefox automatically reloads a site into a container after the user picked "Always Open in this Container".
|
||||
|
||||
```js
|
||||
{
|
||||
"uuid": <uuid>,
|
||||
"userContextId": <userContextId>,
|
||||
"event": "auto-reload-page-in-container"
|
||||
}
|
||||
```
|
||||
|
||||
### A Redshift schema for the payload:
|
||||
|
||||
```lua
|
||||
local schema = {
|
||||
-- column name field type length attributes field name
|
||||
{"uuid", "VARCHAR", 255, nil, "Fields[payload.uuid]"},
|
||||
{"userContextId", "INTEGER", 255, nil, "Fields[payload.userContextId]"},
|
||||
{"clickedContainerTabCount", "INTEGER", 255, nil, "Fields[payload.clickedContainerTabCount]"},
|
||||
{"eventSource", "VARCHAR", 255, nil, "Fields[payload.eventSource]"},
|
||||
{"event", "VARCHAR", 255, nil, "Fields[payload.event]"},
|
||||
{"pageRequestCount", "INTEGER", 255, nil, "Fields[payload.pageRequestCount]"}
|
||||
{"hiddenContainersCount", "INTEGER", 255, nil, "Fields[payload.hiddenContainersCount]"},
|
||||
{"shownContainersCount", "INTEGER", 255, nil, "Fields[payload.shownContainersCount]"},
|
||||
{"totalContainersCount", "INTEGER", 255, nil, "Fields[payload.totalContainersCount]"},
|
||||
{"totalContainerTabsCount", "INTEGER", 255, nil, "Fields[payload.totalContainerTabsCount]"},
|
||||
{"totalNonContainerTabsCount", "INTEGER", 255, nil, "Fields[payload.totalNonContainerTabsCount]"}
|
||||
}
|
||||
```
|
||||
|
||||
### Valid data should be enforced on the server side:
|
||||
|
||||
* `eventSource` should be one of `tab-bar`, `pop-up`, or `file-menu`.
|
||||
|
||||
All Mozilla data is kept by default for 180 days and in accordance with our
|
||||
privacy policies.
|
||||
@@ -1,54 +1,51 @@
|
||||
{
|
||||
"name": "testpilot-containers",
|
||||
"title": "Containers Experiment",
|
||||
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
||||
"version": "2.2.0",
|
||||
"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": "6.2.0",
|
||||
"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",
|
||||
"eslint-plugin-no-unescaped": "^1.1.0",
|
||||
"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",
|
||||
"jpm": "^1.2.2",
|
||||
"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",
|
||||
"testpilot-metrics": "^2.1.0"
|
||||
"web-ext": "^2.9.3",
|
||||
"webextensions-jsdom": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"firefox": ">=51.0"
|
||||
},
|
||||
"permissions": {
|
||||
"multiprocess": true
|
||||
},
|
||||
"hasEmbeddedWebExtension": true,
|
||||
"homepage": "https://github.com/mozilla/testpilot-containers#readme",
|
||||
"keywords": [
|
||||
"jetpack"
|
||||
],
|
||||
"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 && jpm xpi",
|
||||
"deploy": "deploy-txp",
|
||||
"build": "npm test && cd src && web-ext build --overwrite-dest",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:addon": "addons-linter webextension --self-hosted",
|
||||
"lint:css": "stylelint webextension/css/*.css",
|
||||
"lint:html": "htmllint webextension/*.html",
|
||||
"lint:addon": "addons-linter src --self-hosted",
|
||||
"lint:css": "stylelint src/css/*.css",
|
||||
"lint:html": "htmllint *.html",
|
||||
"lint:js": "eslint .",
|
||||
"package": "npm run build && mv testpilot-containers.xpi addon.xpi",
|
||||
"test": "npm run lint"
|
||||
},
|
||||
"updateURL": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"
|
||||
"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 && 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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Multi-Account Containers Confirm Navigation</title>
|
||||
<link xmlns="http://www.w3.org/1999/xhtml" rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
|
||||
<link rel="stylesheet" href="/css/confirm-page.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="title">
|
||||
<h1 class="title-text">Open this site in your assigned container?</h1>
|
||||
</div>
|
||||
<form id="redirect-form">
|
||||
<p>
|
||||
You asked <dfn id="browser-name" title="Thanks for trying out Containers. Sorry we may have got your browser name wrong. #FxNightly" >Firefox</dfn> to always open <dfn class="container-name"></dfn> for this site:<br />
|
||||
</p>
|
||||
<div id="redirect-url"></div>
|
||||
<p>Would you still like to open in this current container?</p>
|
||||
<br />
|
||||
<br />
|
||||
<label for="never-ask" class="check-label">
|
||||
<input id="never-ask" type="checkbox" />
|
||||
Remember my decision for this site
|
||||
</label>
|
||||
<br />
|
||||
<div class="button-container">
|
||||
<button id="deny" class="button">Open in <dfn id="current-container-name">Current</dfn> Container</button>
|
||||
<button id="confirm" class="button primary" autofocus>Open in <dfn class="container-name"></dfn> Container</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/confirm-page.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,87 @@
|
||||
/* General Rules and Resets */
|
||||
.title {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
main {
|
||||
background: url(/img/onboarding-4.png) no-repeat;
|
||||
background-position: -10px -15px;
|
||||
background-size: 300px;
|
||||
margin-inline-start: -350px;
|
||||
padding-inline-start: 350px;
|
||||
}
|
||||
|
||||
.container-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button .container-name,
|
||||
#current-container-name {
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1300px) {
|
||||
main {
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* for a mid sized window we have enough for this but not our image */
|
||||
.title {
|
||||
background-image: url("chrome://global/skin/icons/info.svg");
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font: message-box;
|
||||
}
|
||||
|
||||
#redirect-url,
|
||||
#redirect-origin {
|
||||
font-weight: bold;
|
||||
|
||||
/* max-inline-size is needed to force this text smaller than the layout at a mid-sized window */
|
||||
max-inline-size: 40rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#redirect-url {
|
||||
background: #efedf0; /* Grey 20 */
|
||||
border-radius: 2px;
|
||||
line-height: 1.5;
|
||||
padding-block-end: 0.5rem;
|
||||
padding-block-start: 0.5rem;
|
||||
padding-inline-end: 0.5rem;
|
||||
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;
|
||||
margin-inline-end: 6px;
|
||||
offset-block-start: 3px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
dfn {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.button-container > button {
|
||||
min-inline-size: 240px;
|
||||
}
|
||||
|
||||
.check-label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
.container-notification {
|
||||
align-items: center;
|
||||
background: #efefef;
|
||||
color: #003f07;
|
||||
display: flex;
|
||||
font: 12px sans-serif;
|
||||
inline-size: 100vw;
|
||||
justify-content: start;
|
||||
offset-block-start: 0;
|
||||
offset-inline-start: 0;
|
||||
padding-block-end: 8px;
|
||||
padding-block-start: 8px;
|
||||
padding-inline-end: 8px;
|
||||
padding-inline-start: 8px;
|
||||
position: fixed;
|
||||
text-align: start;
|
||||
transform: translateY(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.07, 0.95, 0, 1) 0.3s;
|
||||
z-index: 999999999999;
|
||||
}
|
||||
|
||||
.container-notification img {
|
||||
block-size: 16px;
|
||||
display: inline-block;
|
||||
inline-size: 16px;
|
||||
margin-inline-end: 3px;
|
||||
}
|
||||
@@ -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 |
@@ -0,0 +1 @@
|
||||
<svg width="32px" height="33px" viewBox="0 0 32 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch --> <desc>Created with Sketch.</desc> <defs> <linearGradient x1="74.0423237%" y1="18.5882821%" x2="0%" y2="100%" id="linearGradient-1"> <stop stop-color="#00FEFF" offset="0%"/> <stop stop-color="#3D85FF" offset="100%"/> </linearGradient> </defs> <g id="Specs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Header-Copy" transform="translate(-182.000000, -152.000000)" fill="url(#linearGradient-1)"> <path d="M205.58574,176.859518 L205.58574,169.287998 C205.58574,169.287998 205.800116,167.315137 207.086372,167.315137 C208.372629,167.315137 208.265441,169.394639 210.677171,169.394639 C211.909834,169.394639 214,168.754792 214,165.022352 C214,161.289912 211.909834,160.810027 210.677171,160.810027 C208.265441,160.810027 208.372629,162.782888 207.086372,162.782888 C205.800116,162.782888 205.58574,160.756707 205.58574,160.756707 L205.58574,157.664114 C205.58574,156.491061 204.621048,155.531291 203.44198,155.531291 L197.814608,155.531291 C197.814608,155.531291 195.992412,155.211368 195.992412,153.931674 C195.992412,152.65198 198.028985,152.545339 198.028985,150.145914 C198.028985,148.91954 197.332262,147 193.580682,147 C189.829101,147 189.293161,148.91954 189.293161,150.145914 C189.293161,152.545339 191.115357,152.65198 191.115357,153.931674 C191.115357,155.211368 189.293161,155.531291 189.293161,155.531291 L184.148135,155.531291 C182.969067,155.531291 182.004375,156.491061 182.004375,157.664114 L182.004375,161.823118 C182.004375,161.823118 181.789999,165.022352 184.362512,165.022352 C186.023926,165.022352 186.07752,162.836209 188.274874,162.836209 C189.346755,162.836209 190.418635,163.8493 190.418635,166.035443 C190.418635,168.274907 189.346755,169.394639 188.274874,169.394639 C186.131114,169.394639 186.023926,167.208496 184.362512,167.208496 C181.789999,167.208496 182.004375,170.301089 182.004375,170.301089 L182.004375,176.859518 C182.004375,178.032571 182.969067,178.992341 184.148135,178.992341 L191.115357,178.992341 C191.115357,178.992341 194.49178,179.205623 194.49178,176.646236 C194.49178,174.993299 192.348019,174.726696 192.348019,172.540552 C192.348019,171.474141 193.527088,170.141127 195.778036,170.141127 C198.028985,170.141127 199.315241,171.474141 199.315241,172.540552 C199.315241,174.673375 197.225074,174.993299 197.225074,176.646236 C197.225074,179.258944 200.601497,178.992341 200.601497,178.992341 L203.44198,178.992341 C204.621048,178.992341 205.58574,178.032571 205.58574,176.859518 Z" id="Shape-Copy-23" transform="translate(198.000000, 163.000000) rotate(-42.000000) translate(-198.000000, -163.000000) "/> </g> </g> </svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||
<path d="M17,12v2a1,1,0,0,1-1,1H2a1,1,0,0,1-1-1V12a1,1,0,0,1,1-1H1.142c2.3,0,2.536-1.773,2.874-4,0.351-2.316.083-4,3.13-4h3.707C13.917,3,13.647,4.684,14,7c0.34,2.228.582,4,2.89,4H16A1,1,0,0,1,17,12Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 307 B |
@@ -0,0 +1,6 @@
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="#3c3c3c" d="M6 14a1 1 0 0 1-.707-.293l-3-3a1 1 0 0 1 1.414-1.414l2.157 2.157 6.316-9.023a1 1 0 0 1 1.639 1.146l-7 10a1 1 0 0 1-.732.427A.863.863 0 0 1 6 14z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 477 B |
|
Before Width: | Height: | Size: 595 B After Width: | Height: | Size: 595 B |
|
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 520 B |
@@ -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 |
|
Before Width: | Height: | Size: 626 B After Width: | Height: | Size: 626 B |
|
Before Width: | Height: | Size: 603 B After Width: | Height: | Size: 603 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 883 B After Width: | Height: | Size: 883 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 342 B After Width: | Height: | Size: 342 B |
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 578 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
|
Before Width: | Height: | Size: 534 B After Width: | Height: | Size: 534 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 372 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 755 B After Width: | Height: | Size: 755 B |
|
Before Width: | Height: | Size: 399 B After Width: | Height: | Size: 399 B |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="-14 -14 48 48" enable-background="new -14 -14 48 48" xml:space="preserve">
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="90.0527" y1="-99.7603" x2="90.0527" y2="-106.3809" gradientTransform="matrix(7.2338 0 0 -7.2338 -641.4998 -735.5619)">
|
||||
<stop offset="0" style="stop-color:#4B71B8"/>
|
||||
<stop offset="1" style="stop-color:#293F7E"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_1_)" d="M33.931,27.993c0,3.304-2.689,5.983-6.002,5.983H-8.082c-3.315,0-6.001-2.683-6.001-5.983V-7.928
|
||||
c0-3.308,2.687-5.988,6.001-5.988h36.011c3.312,0,6.002,2.681,6.002,5.988V27.993z"/>
|
||||
<path fill="#FFFFFF" d="M25.613-4.557c0,0-3.707,0-6.166,0c-3.662,0-7.732,1.535-7.732,6.835c0.019,1.845,0,3.613,0,5.603H7.481
|
||||
v6.728h4.366v19.37h8.021V14.48h5.295l0.479-6.618h-5.913c0,0,0.016-2.946,0-3.8c0-2.093,2.184-1.974,2.312-1.974
|
||||
c1.042,0,3.059,0.003,3.578,0v-6.646H25.613z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="23.9995" y1="0" x2="23.9995" y2="48.0005">
|
||||
<stop offset="0" style="stop-color:#4BD0EF"/>
|
||||
<stop offset="1" style="stop-color:#29AAE1"/>
|
||||
</linearGradient>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" fill="url(#SVGID_1_)" d="M48,42c0,3.313-2.687,6-6,6H6c-3.313,0-6-2.687-6-6V6
|
||||
c0-3.313,2.687-6,6-6h36c3.313,0,6,2.687,6,6V42z"/>
|
||||
<path fill="#29AAE1" d="M40.231,13.413c-1.12,0.497-2.323,0.833-3.588,0.984c1.291-0.774,2.28-1.998,2.747-3.457
|
||||
c-1.206,0.716-2.543,1.236-3.968,1.516c-1.139-1.214-2.763-1.972-4.56-1.972c-3.449,0-6.246,2.796-6.246,6.247
|
||||
c0,0.49,0.055,0.966,0.161,1.424c-5.192-0.261-9.795-2.749-12.876-6.528c-0.538,0.923-0.846,1.996-0.846,3.141
|
||||
c0,2.167,1.103,4.08,2.779,5.199c-1.024-0.032-1.987-0.313-2.83-0.781c0,0.026,0,0.053,0,0.079c0,3.026,2.153,5.551,5.011,6.125
|
||||
c-0.525,0.143-1.076,0.219-1.646,0.219c-0.403,0-0.794-0.038-1.176-0.11c0.795,2.48,3.102,4.287,5.835,4.338
|
||||
c-2.138,1.675-4.832,2.675-7.758,2.675c-0.504,0-1.002-0.03-1.491-0.089c2.765,1.773,6.048,2.808,9.576,2.808
|
||||
c11.49,0,17.774-9.519,17.774-17.774c0-0.271-0.006-0.54-0.019-0.809C38.334,15.766,39.394,14.666,40.231,13.413z"/>
|
||||
<path fill="#FFFFFF" d="M40.231,14.739c-1.12,0.497-2.323,0.833-3.588,0.984c1.291-0.773,2.28-1.998,2.747-3.456
|
||||
c-1.206,0.716-2.543,1.236-3.968,1.516c-1.139-1.214-2.763-1.972-4.56-1.972c-3.449,0-6.246,2.796-6.246,6.247
|
||||
c0,0.489,0.055,0.966,0.161,1.424c-5.192-0.261-9.795-2.748-12.876-6.527c-0.538,0.923-0.846,1.996-0.846,3.141
|
||||
c0,2.167,1.103,4.079,2.779,5.199c-1.024-0.032-1.987-0.313-2.83-0.781c0,0.026,0,0.052,0,0.079c0,3.027,2.153,5.551,5.011,6.125
|
||||
c-0.525,0.144-1.076,0.219-1.646,0.219c-0.403,0-0.794-0.038-1.176-0.11c0.795,2.481,3.102,4.287,5.835,4.338
|
||||
c-2.138,1.676-4.832,2.675-7.758,2.675c-0.504,0-1.002-0.03-1.491-0.089c2.765,1.773,6.048,2.808,9.576,2.808
|
||||
c11.49,0,17.774-9.519,17.774-17.774c0-0.271-0.006-0.54-0.019-0.808C38.334,17.092,39.394,15.992,40.231,14.739z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
"extends": [
|
||||
"../../.eslintrc.js"
|
||||
],
|
||||
"globals": {
|
||||
"assignManager": true,
|
||||
"badge": true,
|
||||
"backgroundLogic": true,
|
||||
"identityState": true,
|
||||
"messageHandler": true,
|
||||
"sync": true
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,648 @@
|
||||
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(pageUrlorUrlKey) {
|
||||
if (pageUrlorUrlKey.includes("siteContainerMap@@_")) return pageUrlorUrlKey;
|
||||
const url = new window.URL(pageUrlorUrlKey);
|
||||
const storagePrefix = "siteContainerMap@@_";
|
||||
if (url.port === "80" || url.port === "443") {
|
||||
return `${storagePrefix}${url.hostname}`;
|
||||
} else {
|
||||
return `${storagePrefix}${url.hostname}${url.port}`;
|
||||
}
|
||||
},
|
||||
|
||||
setExempted(pageUrlorUrlKey, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
if (!(siteStoreKey in this.exemptedTabs)) {
|
||||
this.exemptedTabs[siteStoreKey] = [];
|
||||
}
|
||||
this.exemptedTabs[siteStoreKey].push(tabId);
|
||||
},
|
||||
|
||||
removeExempted(pageUrlorUrlKey) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
this.exemptedTabs[siteStoreKey] = [];
|
||||
},
|
||||
|
||||
isExempted(pageUrlorUrlKey, tabId) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
if (!(siteStoreKey in this.exemptedTabs)) {
|
||||
return false;
|
||||
}
|
||||
return this.exemptedTabs[siteStoreKey].includes(tabId);
|
||||
},
|
||||
|
||||
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) {
|
||||
resolve(storageResponse[siteStoreKey]);
|
||||
}
|
||||
resolve(null);
|
||||
}).catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async set(pageUrlorUrlKey, data, exemptedTabIds, backup = true) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
if (exemptedTabIds) {
|
||||
exemptedTabIds.forEach((tabId) => {
|
||||
this.setExempted(pageUrlorUrlKey, tabId);
|
||||
});
|
||||
}
|
||||
// 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;
|
||||
},
|
||||
|
||||
async remove(pageUrlorUrlKey) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
|
||||
// When we remove an assignment we should clear all the exemptions
|
||||
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.getAssignedSites(userContextId);
|
||||
this.area.remove(Object.keys(sitesByContainer));
|
||||
},
|
||||
|
||||
async getAssignedSites(userContextId = null) {
|
||||
const sites = {};
|
||||
const siteConfigs = await this.area.get();
|
||||
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 = 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
|
||||
this.storageArea.get(pageUrl).then((siteSettings) => {
|
||||
if (siteSettings) {
|
||||
siteSettings.neverAsk = true;
|
||||
this.storageArea.set(pageUrl, siteSettings);
|
||||
}
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// We return here so the confirm page can load the tab when exempted
|
||||
async _exemptTab(m) {
|
||||
const pageUrl = m.pageUrl;
|
||||
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
|
||||
async onBeforeRequest(options) {
|
||||
if (options.frameId !== 0 || options.tabId === -1) {
|
||||
return {};
|
||||
}
|
||||
this.removeContextMenu();
|
||||
const [tab, siteSettings] = await Promise.all([
|
||||
browser.tabs.get(options.tabId),
|
||||
this.storageArea.get(options.url)
|
||||
]);
|
||||
let container;
|
||||
try {
|
||||
container = await browser.contextualIdentities
|
||||
.get(backgroundLogic.cookieStoreId(siteSettings.userContextId));
|
||||
} catch (e) {
|
||||
container = false;
|
||||
}
|
||||
|
||||
// The container we have in the assignment map isn't present any
|
||||
// more so lets remove it then continue the existing load
|
||||
if (siteSettings && !container) {
|
||||
this.deleteContainer(siteSettings.userContextId);
|
||||
return {};
|
||||
}
|
||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
if (!siteSettings
|
||||
|| userContextId === siteSettings.userContextId
|
||||
|| this.storageArea.isExempted(options.url, tab.id)) {
|
||||
return {};
|
||||
}
|
||||
const removeTab = backgroundLogic.NEW_TAB_PAGES.has(tab.url)
|
||||
|| (messageHandler.lastCreatedTab
|
||||
&& messageHandler.lastCreatedTab.id === tab.id);
|
||||
const openTabId = removeTab ? tab.openerTabId : tab.id;
|
||||
|
||||
if (!this.canceledRequests[tab.id]) {
|
||||
// we decided to cancel the request at this point, register
|
||||
// canceled request
|
||||
this.canceledRequests[tab.id] = {
|
||||
requestIds: {
|
||||
[options.requestId]: true
|
||||
},
|
||||
urls: {
|
||||
[options.url]: true
|
||||
}
|
||||
};
|
||||
|
||||
// since webRequest onCompleted and onErrorOccurred are not 100%
|
||||
// reliable (see #1120)
|
||||
// we register a timer here to cleanup canceled requests, just to
|
||||
// make sure we don't
|
||||
// end up in a situation where certain urls in a tab.id stay canceled
|
||||
setTimeout(() => {
|
||||
if (this.canceledRequests[tab.id]) {
|
||||
delete this.canceledRequests[tab.id];
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
let cancelEarly = false;
|
||||
if (this.canceledRequests[tab.id].requestIds[options.requestId] ||
|
||||
this.canceledRequests[tab.id].urls[options.url]) {
|
||||
// same requestId or url from the same tab
|
||||
// this is a redirect that we have to cancel early to prevent
|
||||
// opening two tabs
|
||||
cancelEarly = true;
|
||||
}
|
||||
// we decided to cancel the request at this point, register canceled
|
||||
// request
|
||||
this.canceledRequests[tab.id].requestIds[options.requestId] = true;
|
||||
this.canceledRequests[tab.id].urls[options.url] = true;
|
||||
if (cancelEarly) {
|
||||
return {
|
||||
cancel: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.reloadPageInContainer(
|
||||
options.url,
|
||||
userContextId,
|
||||
siteSettings.userContextId,
|
||||
tab.index + 1,
|
||||
tab.active,
|
||||
siteSettings.neverAsk,
|
||||
openTabId
|
||||
);
|
||||
this.calculateContextMenu(tab);
|
||||
|
||||
/* Removal of existing tabs:
|
||||
We aim to open the new assigned container tab / warning prompt in
|
||||
it's own tab:
|
||||
- As the history won't span from one container to another it
|
||||
seems most sane to not try and reopen a tab on history.back()
|
||||
- When users open a new tab themselves we want to make sure we
|
||||
don't end up with three tabs as per:
|
||||
https://github.com/mozilla/testpilot-containers/issues/421
|
||||
If we are coming from an internal url that are used for the new
|
||||
tab page (NEW_TAB_PAGES), we can safely close as user is unlikely
|
||||
losing history
|
||||
Detecting redirects on "new tab" opening actions is pretty hard
|
||||
as we don't get tab history:
|
||||
- Redirects happen from Short URLs and tracking links that act as
|
||||
a gateway
|
||||
- Extensions don't provide a way to history crawl for tabs, we
|
||||
could inject content scripts to do this
|
||||
however they don't run on about:blank so this would likely be
|
||||
just as hacky.
|
||||
We capture the time the tab was created and close if it was within
|
||||
the timeout to try to capture pages which haven't had user
|
||||
interaction or history.
|
||||
*/
|
||||
if (removeTab) {
|
||||
browser.tabs.remove(tab.id);
|
||||
}
|
||||
return {
|
||||
cancel: true,
|
||||
};
|
||||
},
|
||||
|
||||
init() {
|
||||
browser.contextMenus.onClicked.addListener((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
|
||||
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) {
|
||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
||||
let remove;
|
||||
if (userContextId) {
|
||||
switch (info.menuItemId) {
|
||||
case this.MENU_ASSIGN_ID:
|
||||
case this.MENU_REMOVE_ID:
|
||||
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
||||
remove = false;
|
||||
} else {
|
||||
remove = true;
|
||||
}
|
||||
await this._setOrRemoveAssignment(
|
||||
tab.id, info.pageUrl, userContextId, remove
|
||||
);
|
||||
break;
|
||||
case this.MENU_MOVE_ID:
|
||||
backgroundLogic.moveTabsToWindow({
|
||||
cookieStoreId: tab.cookieStoreId,
|
||||
windowId: tab.windowId,
|
||||
});
|
||||
break;
|
||||
case this.MENU_HIDE_ID:
|
||||
backgroundLogic.hideTabs({
|
||||
cookieStoreId: tab.cookieStoreId,
|
||||
windowId: tab.windowId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
getUserContextIdFromCookieStore(tab) {
|
||||
if (!("cookieStoreId" in tab)) {
|
||||
return false;
|
||||
}
|
||||
return backgroundLogic.getUserContextIdFromCookieStoreId(
|
||||
tab.cookieStoreId
|
||||
);
|
||||
},
|
||||
|
||||
isTabPermittedAssign(tab) {
|
||||
// Ensure we are not an important about url
|
||||
const url = new URL(tab.url);
|
||||
if (url.protocol === "about:"
|
||||
|| url.protocol === "moz-extension:") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
async _setOrRemoveAssignment(tabId, pageUrl, userContextId, remove) {
|
||||
let actionName;
|
||||
|
||||
// https://github.com/mozilla/testpilot-containers/issues/626
|
||||
// Context menu has stored context IDs as strings, so we need to coerce
|
||||
// the value to a string for accurate checking
|
||||
userContextId = String(userContextId);
|
||||
|
||||
if (!remove) {
|
||||
const tabs = await browser.tabs.query({});
|
||||
const assignmentStoreKey = this.storageArea.getSiteStoreKey(pageUrl);
|
||||
const exemptedTabIds = tabs.filter((tab) => {
|
||||
const tabStoreKey = this.storageArea.getSiteStoreKey(tab.url);
|
||||
/* Auto exempt all tabs that exist for this hostname that are not in the same container */
|
||||
if (tabStoreKey === assignmentStoreKey &&
|
||||
this.getUserContextIdFromCookieStore(tab) !== userContextId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).map((tab) => {
|
||||
return tab.id;
|
||||
});
|
||||
|
||||
await this.storageArea.set(pageUrl, {
|
||||
userContextId,
|
||||
neverAsk: false
|
||||
}, exemptedTabIds);
|
||||
actionName = "added";
|
||||
} else {
|
||||
await this.storageArea.remove(pageUrl);
|
||||
actionName = "removed";
|
||||
}
|
||||
browser.tabs.sendMessage(tabId, {
|
||||
text: `Successfully ${actionName} site to always open in this container`
|
||||
});
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
this.calculateContextMenu(tab);
|
||||
},
|
||||
|
||||
async _getAssignment(tab) {
|
||||
const cookieStore = this.getUserContextIdFromCookieStore(tab);
|
||||
// Ensure we have a cookieStore to assign to
|
||||
if (cookieStore
|
||||
&& this.isTabPermittedAssign(tab)) {
|
||||
return this.storageArea.get(tab.url);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_getByContainer(userContextId) {
|
||||
return this.storageArea.getAssignedSites(userContextId);
|
||||
},
|
||||
|
||||
removeContextMenu() {
|
||||
// There is a focus issue in this menu where if you change window with a context menu click
|
||||
// you get the wrong menu display because of async
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16
|
||||
// We also can't change for always private mode
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
|
||||
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
|
||||
browser.contextMenus.remove(this.MENU_REMOVE_ID);
|
||||
browser.contextMenus.remove(this.MENU_SEPARATOR_ID);
|
||||
browser.contextMenus.remove(this.MENU_HIDE_ID);
|
||||
browser.contextMenus.remove(this.MENU_MOVE_ID);
|
||||
},
|
||||
|
||||
async calculateContextMenu(tab) {
|
||||
this.removeContextMenu();
|
||||
const siteSettings = await this._getAssignment(tab);
|
||||
// Return early and not add an item if we have false
|
||||
// False represents assignment is not permitted
|
||||
if (siteSettings === false) {
|
||||
return false;
|
||||
}
|
||||
let checked = false;
|
||||
let menuId = this.MENU_ASSIGN_ID;
|
||||
const tabUserContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
if (siteSettings &&
|
||||
Number(siteSettings.userContextId) === Number(tabUserContextId)) {
|
||||
checked = true;
|
||||
menuId = this.MENU_REMOVE_ID;
|
||||
}
|
||||
browser.contextMenus.create({
|
||||
id: menuId,
|
||||
title: "Always Open in This Container",
|
||||
checked,
|
||||
type: "checkbox",
|
||||
contexts: ["all"],
|
||||
});
|
||||
|
||||
browser.contextMenus.create({
|
||||
id: this.MENU_SEPARATOR_ID,
|
||||
type: "separator",
|
||||
contexts: ["all"],
|
||||
});
|
||||
|
||||
browser.contextMenus.create({
|
||||
id: this.MENU_HIDE_ID,
|
||||
title: "Hide This Container",
|
||||
contexts: ["all"],
|
||||
});
|
||||
|
||||
browser.contextMenus.create({
|
||||
id: this.MENU_MOVE_ID,
|
||||
title: "Move Tabs to a New Window",
|
||||
contexts: ["all"],
|
||||
});
|
||||
},
|
||||
|
||||
encodeURLProperty(url) {
|
||||
return encodeURIComponent(url).replace(/[!'()*]/g, (c) => {
|
||||
const charCode = c.charCodeAt(0).toString(16);
|
||||
return `%${charCode}`;
|
||||
});
|
||||
},
|
||||
|
||||
reloadPageInContainer(url, currentUserContextId, userContextId, index, active, neverAsk = false, openerTabId = null) {
|
||||
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
|
||||
const 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, openerTabId});
|
||||
} else {
|
||||
let confirmUrl = `${loadPage}?url=${this.encodeURLProperty(url)}&cookieStoreId=${cookieStoreId}`;
|
||||
let currentCookieStoreId;
|
||||
if (currentUserContextId) {
|
||||
currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId);
|
||||
confirmUrl += `¤tCookieStoreId=${currentCookieStoreId}`;
|
||||
}
|
||||
browser.tabs.create({
|
||||
url: confirmUrl,
|
||||
cookieStoreId: currentCookieStoreId,
|
||||
openerTabId,
|
||||
index,
|
||||
active
|
||||
}).then(() => {
|
||||
// We don't want to sync this URL ever nor clutter the users history
|
||||
browser.history.deleteUrl({url: confirmUrl});
|
||||
}).catch((e) => {
|
||||
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();
|
||||
@@ -0,0 +1,333 @@
|
||||
const DEFAULT_TAB = "about:newtab";
|
||||
const backgroundLogic = {
|
||||
NEW_TAB_PAGES: new Set([
|
||||
"about:startpage",
|
||||
"about:newtab",
|
||||
"about:home",
|
||||
"about:blank"
|
||||
]),
|
||||
unhideQueue: [],
|
||||
|
||||
async getExtensionInfo() {
|
||||
const manifestPath = browser.extension.getURL("manifest.json");
|
||||
const response = await fetch(manifestPath);
|
||||
const extensionInfo = await response.json();
|
||||
return extensionInfo;
|
||||
},
|
||||
|
||||
getUserContextIdFromCookieStoreId(cookieStoreId) {
|
||||
if (!cookieStoreId) {
|
||||
return false;
|
||||
}
|
||||
const container = cookieStoreId.replace("firefox-container-", "");
|
||||
if (container !== cookieStoreId) {
|
||||
return container;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
async deleteContainer(userContextId, removed = false) {
|
||||
await this._closeTabs(userContextId);
|
||||
if (!removed) {
|
||||
await browser.contextualIdentities.remove(this.cookieStoreId(userContextId));
|
||||
}
|
||||
assignManager.deleteContainer(userContextId);
|
||||
return {done: true, userContextId};
|
||||
},
|
||||
|
||||
async createOrUpdateContainer(options) {
|
||||
let donePromise;
|
||||
if (options.userContextId !== "new") {
|
||||
donePromise = browser.contextualIdentities.update(
|
||||
this.cookieStoreId(options.userContextId),
|
||||
options.params
|
||||
);
|
||||
} else {
|
||||
donePromise = browser.contextualIdentities.create(options.params);
|
||||
}
|
||||
await donePromise;
|
||||
},
|
||||
|
||||
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
|
||||
|
||||
// We can't open new tab pages, so open a blank tab. Used in tab un-hide
|
||||
if (this.NEW_TAB_PAGES.has(url)) {
|
||||
url = undefined;
|
||||
}
|
||||
|
||||
if (!this.isPermissibleURL(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return browser.tabs.create({
|
||||
url,
|
||||
active,
|
||||
discarded,
|
||||
pinned: options.pinned || false,
|
||||
cookieStoreId
|
||||
});
|
||||
},
|
||||
|
||||
isPermissibleURL(url) {
|
||||
const protocol = new URL(url).protocol;
|
||||
// We can't open these we just have to throw them away
|
||||
if (protocol === "about:"
|
||||
|| protocol === "chrome:"
|
||||
|| protocol === "moz-extension:") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
checkArgs(requiredArguments, options, methodName) {
|
||||
requiredArguments.forEach((argument) => {
|
||||
if (!(argument in options)) {
|
||||
return new Error(`${methodName} must be called with ${argument} argument.`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async getTabs(options) {
|
||||
const requiredArguments = ["cookieStoreId", "windowId"];
|
||||
this.checkArgs(requiredArguments, options, "getTabs");
|
||||
const { cookieStoreId, windowId } = options;
|
||||
|
||||
const list = [];
|
||||
const tabs = await browser.tabs.query({
|
||||
cookieStoreId,
|
||||
windowId
|
||||
});
|
||||
tabs.forEach((tab) => {
|
||||
list.push(identityState._createTabObject(tab));
|
||||
});
|
||||
|
||||
const containerState = await identityState.storageArea.get(cookieStoreId);
|
||||
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");
|
||||
const { cookieStoreId, windowId } = options;
|
||||
|
||||
const list = await browser.tabs.query({
|
||||
cookieStoreId,
|
||||
windowId
|
||||
});
|
||||
|
||||
const containerState = await identityState.storageArea.get(cookieStoreId);
|
||||
|
||||
// Nothing to do
|
||||
if (list.length === 0 &&
|
||||
containerState.hiddenTabs.length === 0) {
|
||||
return;
|
||||
}
|
||||
let newWindowObj;
|
||||
let hiddenDefaultTabToClose;
|
||||
if (list.length) {
|
||||
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
|
||||
});
|
||||
} else {
|
||||
//As we get a blank tab here we will need to await the tabs creation
|
||||
newWindowObj = await browser.windows.create({
|
||||
});
|
||||
hiddenDefaultTabToClose = true;
|
||||
}
|
||||
|
||||
const showHiddenPromises = [];
|
||||
|
||||
// Let's show the hidden tabs.
|
||||
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) {
|
||||
// Lets wait for hidden tabs to show before closing the others
|
||||
await showHiddenPromises;
|
||||
}
|
||||
|
||||
containerState.hiddenTabs = [];
|
||||
|
||||
// Let's close all the normal tab in the new window. In theory it
|
||||
// should be only the first tab, but maybe there are addons doing
|
||||
// crazy stuff.
|
||||
const tabs = await browser.tabs.query({windowId: newWindowObj.id});
|
||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
||||
if (tab.cookieStoreId !== cookieStoreId) {
|
||||
browser.tabs.remove(tab.id);
|
||||
}
|
||||
}
|
||||
const rv = await identityState.storageArea.set(cookieStoreId, containerState);
|
||||
this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1);
|
||||
return rv;
|
||||
},
|
||||
|
||||
async _closeTabs(userContextId, windowId = false) {
|
||||
const cookieStoreId = this.cookieStoreId(userContextId);
|
||||
let tabs;
|
||||
/* if we have no windowId we are going to close all this container (used for deleting) */
|
||||
if (windowId !== false) {
|
||||
tabs = await browser.tabs.query({
|
||||
cookieStoreId,
|
||||
windowId
|
||||
});
|
||||
} else {
|
||||
tabs = await browser.tabs.query({
|
||||
cookieStoreId
|
||||
});
|
||||
}
|
||||
const tabIds = tabs.map((tab) => tab.id);
|
||||
return browser.tabs.remove(tabIds);
|
||||
},
|
||||
|
||||
async queryIdentitiesState(windowId) {
|
||||
const identities = await browser.contextualIdentities.query({});
|
||||
const identitiesOutput = {};
|
||||
const identitiesPromise = identities.map(async (identity) => {
|
||||
const { cookieStoreId } = identity;
|
||||
const containerState = await identityState.storageArea.get(cookieStoreId);
|
||||
const openTabs = await browser.tabs.query({
|
||||
cookieStoreId,
|
||||
windowId
|
||||
});
|
||||
identitiesOutput[cookieStoreId] = {
|
||||
hasHiddenTabs: !!containerState.hiddenTabs.length,
|
||||
hasOpenTabs: !!openTabs.length,
|
||||
numberOfHiddenTabs: containerState.hiddenTabs.length,
|
||||
numberOfOpenTabs: openTabs.length
|
||||
};
|
||||
return;
|
||||
});
|
||||
await Promise.all(identitiesPromise);
|
||||
return identitiesOutput;
|
||||
},
|
||||
|
||||
async sortTabs() {
|
||||
const windows = await browser.windows.getAll();
|
||||
for (let windowObj of windows) { // eslint-disable-line prefer-const
|
||||
// First the pinned tabs, then the normal ones.
|
||||
await this._sortTabsInternal(windowObj, true);
|
||||
await this._sortTabsInternal(windowObj, false);
|
||||
}
|
||||
},
|
||||
|
||||
async _sortTabsInternal(windowObj, pinnedTabs) {
|
||||
const tabs = await browser.tabs.query({windowId: windowObj.id});
|
||||
let pos = 0;
|
||||
|
||||
// Let's collect UCIs/tabs for this window.
|
||||
const map = new Map;
|
||||
for (const tab of tabs) {
|
||||
if (pinnedTabs && !tab.pinned) {
|
||||
// We don't have, or we already handled all the pinned tabs.
|
||||
break;
|
||||
}
|
||||
|
||||
if (!pinnedTabs && tab.pinned) {
|
||||
// pinned tabs must be consider as taken positions.
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
|
||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId);
|
||||
if (!map.has(userContextId)) {
|
||||
map.set(userContextId, []);
|
||||
}
|
||||
map.get(userContextId).push(tab);
|
||||
}
|
||||
|
||||
// Let's sort the map.
|
||||
const sortMap = new Map([...map.entries()].sort((a, b) => a[0] > b[0]));
|
||||
|
||||
// Let's move tabs.
|
||||
sortMap.forEach(tabs => {
|
||||
for (const tab of tabs) {
|
||||
++pos;
|
||||
browser.tabs.move(tab.id, {
|
||||
windowId: windowObj.id,
|
||||
index: pos
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async hideTabs(options) {
|
||||
const requiredArguments = ["cookieStoreId", "windowId"];
|
||||
this.checkArgs(requiredArguments, options, "hideTabs");
|
||||
const { cookieStoreId, windowId } = options;
|
||||
|
||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(cookieStoreId);
|
||||
|
||||
const containerState = await identityState.storeHidden(cookieStoreId, windowId);
|
||||
await this._closeTabs(userContextId, windowId);
|
||||
return containerState;
|
||||
},
|
||||
|
||||
async showTabs(options) {
|
||||
if (!("cookieStoreId" in options)) {
|
||||
return Promise.reject("showTabs must be called with cookieStoreId argument.");
|
||||
}
|
||||
|
||||
const userContextId = backgroundLogic.getUserContextIdFromCookieStoreId(options.cookieStoreId);
|
||||
const promises = [];
|
||||
|
||||
const containerState = await identityState.storageArea.get(options.cookieStoreId);
|
||||
|
||||
for (let object of containerState.hiddenTabs) { // eslint-disable-line prefer-const
|
||||
// do not show already opened url
|
||||
if (object.url !== options.alreadyShowingUrl) {
|
||||
promises.push(this.openNewTab({
|
||||
userContextId: userContextId,
|
||||
url: object.url,
|
||||
nofocus: options.nofocus || false,
|
||||
noload: true,
|
||||
pinned: object.pinned,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
containerState.hiddenTabs = [];
|
||||
|
||||
await Promise.all(promises);
|
||||
return identityState.storageArea.set(options.cookieStoreId, containerState);
|
||||
},
|
||||
|
||||
cookieStoreId(userContextId) {
|
||||
return `firefox-container-${userContextId}`;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
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);
|
||||
},
|
||||
|
||||
async displayBrowserActionBadge() {
|
||||
const extensionInfo = await backgroundLogic.getExtensionInfo();
|
||||
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" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
badge.init();
|
||||
@@ -0,0 +1,163 @@
|
||||
window.identityState = {
|
||||
storageArea: {
|
||||
area: browser.storage.local,
|
||||
|
||||
getContainerStoreKey(cookieStoreId) {
|
||||
const storagePrefix = "identitiesState@@_";
|
||||
return `${storagePrefix}${cookieStoreId}`;
|
||||
},
|
||||
|
||||
async get(cookieStoreId) {
|
||||
const storeKey = this.getContainerStoreKey(cookieStoreId);
|
||||
const storageResponse = await this.area.get([storeKey]);
|
||||
if (storageResponse && storeKey in storageResponse) {
|
||||
if (!storageResponse[storeKey].macAddonUUID){
|
||||
storageResponse[storeKey].macAddonUUID = uuidv4();
|
||||
await this.set(cookieStoreId, storageResponse[storeKey]);
|
||||
}
|
||||
return storageResponse[storeKey];
|
||||
}
|
||||
// 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) {
|
||||
const storeKey = this.getContainerStoreKey(cookieStoreId);
|
||||
return this.area.set({
|
||||
[storeKey]: data
|
||||
});
|
||||
},
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_createTabObject(tab) {
|
||||
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});
|
||||
tabsByContainer.forEach((tab) => {
|
||||
const tabObject = this._createTabObject(tab);
|
||||
if (!backgroundLogic.isPermissibleURL(tab.url)) {
|
||||
return;
|
||||
}
|
||||
// This tab is going to be closed. Let's mark this tabObject as
|
||||
// non-active.
|
||||
tabObject.active = false;
|
||||
tabObject.hiddenState = true;
|
||||
containerState.hiddenTabs.push(tabObject);
|
||||
});
|
||||
|
||||
return this.storageArea.set(cookieStoreId, containerState);
|
||||
},
|
||||
|
||||
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: [],
|
||||
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)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<!--
|
||||
This didn't work for debugging in the manifest.
|
||||
"scripts": [
|
||||
"js/background/backgroundLogic.js",
|
||||
"js/background/assignManager.js",
|
||||
"js/background/badge.js",
|
||||
"js/background/identityState.js",
|
||||
"js/background/messageHandler.js",
|
||||
]
|
||||
-->
|
||||
<script type="text/javascript" src="backgroundLogic.js"></script>
|
||||
<script type="text/javascript" src="assignManager.js"></script>
|
||||
<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>
|
||||
@@ -0,0 +1,223 @@
|
||||
const messageHandler = {
|
||||
// After the timer completes we assume it's a tab the user meant to keep open
|
||||
// We use this to catch redirected tabs that have just opened
|
||||
// If this were in platform we would change how the tab opens based on "new tab" link navigations such as ctrl+click
|
||||
LAST_CREATED_TAB_TIMER: 2000,
|
||||
|
||||
init() {
|
||||
// Handles messages from webextension code
|
||||
browser.runtime.onMessage.addListener((m) => {
|
||||
let response;
|
||||
|
||||
switch (m.method) {
|
||||
case "resetSync":
|
||||
response = sync.resetSync();
|
||||
break;
|
||||
case "resetBookmarksContext":
|
||||
response = assignManager.resetBookmarksMenuItem();
|
||||
break;
|
||||
case "deleteContainer":
|
||||
response = backgroundLogic.deleteContainer(m.message.userContextId);
|
||||
break;
|
||||
case "createOrUpdateContainer":
|
||||
response = backgroundLogic.createOrUpdateContainer(m.message);
|
||||
break;
|
||||
case "neverAsk":
|
||||
assignManager._neverAsk(m);
|
||||
break;
|
||||
case "getAssignment":
|
||||
response = browser.tabs.get(m.tabId).then((tab) => {
|
||||
return assignManager._getAssignment(tab);
|
||||
});
|
||||
break;
|
||||
case "getAssignmentObjectByContainer":
|
||||
response = assignManager._getByContainer(m.message.userContextId);
|
||||
break;
|
||||
case "setOrRemoveAssignment":
|
||||
// m.tabId is used for where to place the in content message
|
||||
// m.url is the assignment to be removed/added
|
||||
response = browser.tabs.get(m.tabId).then((tab) => {
|
||||
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.userContextId, m.value);
|
||||
});
|
||||
break;
|
||||
case "sortTabs":
|
||||
backgroundLogic.sortTabs();
|
||||
break;
|
||||
case "showTabs":
|
||||
backgroundLogic.unhideContainer(m.cookieStoreId);
|
||||
break;
|
||||
case "hideTabs":
|
||||
backgroundLogic.hideTabs({
|
||||
cookieStoreId: m.cookieStoreId,
|
||||
windowId: m.windowId
|
||||
});
|
||||
break;
|
||||
case "checkIncompatibleAddons":
|
||||
// TODO
|
||||
break;
|
||||
case "moveTabsToWindow":
|
||||
response = backgroundLogic.moveTabsToWindow({
|
||||
cookieStoreId: m.cookieStoreId,
|
||||
windowId: m.windowId
|
||||
});
|
||||
break;
|
||||
case "getTabs":
|
||||
response = backgroundLogic.getTabs({
|
||||
cookieStoreId: m.cookieStoreId,
|
||||
windowId: m.windowId
|
||||
});
|
||||
break;
|
||||
case "queryIdentitiesState":
|
||||
response = backgroundLogic.queryIdentitiesState(m.message.windowId);
|
||||
break;
|
||||
case "exemptContainerAssignment":
|
||||
response = assignManager._exemptTab(m);
|
||||
break;
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
// Handles external messages from webextensions
|
||||
const externalExtensionAllowed = {};
|
||||
browser.runtime.onMessageExternal.addListener(async (message, sender) => {
|
||||
if (!externalExtensionAllowed[sender.id]) {
|
||||
const extensionInfo = await browser.management.get(sender.id);
|
||||
if (!extensionInfo.permissions.includes("contextualIdentities")) {
|
||||
throw new Error("Missing contextualIdentities permission");
|
||||
}
|
||||
// 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);
|
||||
backgroundLogic.deleteContainer(userContextId, true);
|
||||
});
|
||||
}
|
||||
|
||||
browser.tabs.onActivated.addListener((info) => {
|
||||
assignManager.removeContextMenu();
|
||||
browser.tabs.get(info.tabId).then((tab) => {
|
||||
assignManager.calculateContextMenu(tab);
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
|
||||
browser.windows.onFocusChanged.addListener((windowId) => {
|
||||
this.onFocusChangedCallback(windowId);
|
||||
});
|
||||
|
||||
browser.webRequest.onCompleted.addListener((details) => {
|
||||
if (details.frameId !== 0 || details.tabId === -1) {
|
||||
return {};
|
||||
}
|
||||
assignManager.removeContextMenu();
|
||||
|
||||
browser.tabs.get(details.tabId).then((tab) => {
|
||||
assignManager.calculateContextMenu(tab);
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}, {urls: ["<all_urls>"], types: ["main_frame"]});
|
||||
|
||||
browser.tabs.onCreated.addListener((tab) => {
|
||||
// lets remember the last tab created so we can close it if it looks like a redirect
|
||||
this.lastCreatedTab = tab;
|
||||
if (tab.cookieStoreId) {
|
||||
// Don't count firefox-default, firefox-private, nor our own confirm page loads
|
||||
if (tab.cookieStoreId !== "firefox-default" &&
|
||||
tab.cookieStoreId !== "firefox-private" &&
|
||||
!tab.url.startsWith("moz-extension")) {
|
||||
// increment the counter of container tabs opened
|
||||
this.incrementCountOfContainerTabsOpened();
|
||||
|
||||
this.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;
|
||||
}, this.LAST_CREATED_TAB_TIMER);
|
||||
});
|
||||
},
|
||||
|
||||
async incrementCountOfContainerTabsOpened() {
|
||||
const key = "containerTabsOpened";
|
||||
const count = await browser.storage.local.get({[key]: 0});
|
||||
const countOfContainerTabsOpened = ++count[key];
|
||||
browser.storage.local.set({[key]: countOfContainerTabsOpened});
|
||||
|
||||
// When the user opens their _ tab, give them the achievement
|
||||
if (countOfContainerTabsOpened === 100) {
|
||||
const storage = await browser.storage.local.get({achievements: []});
|
||||
storage.achievements.push({"name": "manyContainersOpened", "done": false});
|
||||
// use set and spread to create a unique array
|
||||
const achievements = [...new Set(storage.achievements)];
|
||||
browser.storage.local.set({achievements});
|
||||
browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"});
|
||||
browser.browserAction.setBadgeText({text: "NEW"});
|
||||
}
|
||||
},
|
||||
|
||||
async onFocusChangedCallback(windowId) {
|
||||
assignManager.removeContextMenu();
|
||||
// browserAction loses background color in new windows ...
|
||||
// https://bugzil.la/1314674
|
||||
// https://github.com/mozilla/testpilot-containers/issues/608
|
||||
// ... so re-call displayBrowserActionBadge on window changes
|
||||
badge.displayBrowserActionBadge();
|
||||
browser.tabs.query({active: true, windowId}).then((tabs) => {
|
||||
if (tabs && tabs[0]) {
|
||||
assignManager.calculateContextMenu(tabs[0]);
|
||||
}
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Lets do this last as theme manager did a check before connecting before
|
||||
messageHandler.init();
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
async function load() {
|
||||
const searchParams = new URL(window.location).searchParams;
|
||||
const redirectUrl = searchParams.get("url");
|
||||
const cookieStoreId = searchParams.get("cookieStoreId");
|
||||
const currentCookieStoreId = searchParams.get("currentCookieStoreId");
|
||||
const redirectUrlElement = document.getElementById("redirect-url");
|
||||
redirectUrlElement.textContent = redirectUrl;
|
||||
appendFavicon(redirectUrl, redirectUrlElement);
|
||||
|
||||
const container = await browser.contextualIdentities.get(cookieStoreId);
|
||||
[...document.querySelectorAll(".container-name")].forEach((containerNameElement) => {
|
||||
containerNameElement.textContent = container.name;
|
||||
});
|
||||
|
||||
// If default container, button will default to normal HTML content
|
||||
if (currentCookieStoreId) {
|
||||
const currentContainer = await browser.contextualIdentities.get(currentCookieStoreId);
|
||||
document.getElementById("current-container-name").textContent = currentContainer.name;
|
||||
}
|
||||
|
||||
document.getElementById("redirect-form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
let button = "confirm"; // Confirm is the form default.
|
||||
let buttonTarget = e.explicitOriginalTarget;
|
||||
if (buttonTarget.tagName !== "BUTTON") {
|
||||
buttonTarget = buttonTarget.closest("button");
|
||||
}
|
||||
if (buttonTarget && buttonTarget.id) {
|
||||
button = buttonTarget.id;
|
||||
}
|
||||
switch (button) {
|
||||
case "deny":
|
||||
denySubmit(redirectUrl);
|
||||
break;
|
||||
case "confirm":
|
||||
confirmSubmit(redirectUrl, cookieStoreId);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function appendFavicon(pageUrl, redirectUrlElement) {
|
||||
const origin = new URL(pageUrl).origin;
|
||||
const favIconElement = Utils.createFavIconElement(`${origin}/favicon.ico`);
|
||||
|
||||
redirectUrlElement.prepend(favIconElement);
|
||||
}
|
||||
|
||||
function confirmSubmit(redirectUrl, cookieStoreId) {
|
||||
const neverAsk = document.getElementById("never-ask").checked;
|
||||
// Sending neverAsk message to background to store for next time we see this process
|
||||
if (neverAsk) {
|
||||
browser.runtime.sendMessage({
|
||||
method: "neverAsk",
|
||||
neverAsk: true,
|
||||
pageUrl: redirectUrl
|
||||
});
|
||||
}
|
||||
openInContainer(redirectUrl, cookieStoreId);
|
||||
}
|
||||
|
||||
function getCurrentTab() {
|
||||
return browser.tabs.query({
|
||||
active: true,
|
||||
windowId: browser.windows.WINDOW_ID_CURRENT
|
||||
});
|
||||
}
|
||||
|
||||
async function denySubmit(redirectUrl) {
|
||||
const tab = await getCurrentTab();
|
||||
await browser.runtime.sendMessage({
|
||||
method: "exemptContainerAssignment",
|
||||
tabId: tab[0].id,
|
||||
pageUrl: redirectUrl
|
||||
});
|
||||
document.location.replace(redirectUrl);
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
async function openInContainer(redirectUrl, cookieStoreId) {
|
||||
const tab = await getCurrentTab();
|
||||
await browser.tabs.create({
|
||||
index: tab[0].index + 1,
|
||||
cookieStoreId,
|
||||
url: redirectUrl
|
||||
});
|
||||
if (tab.length > 0) {
|
||||
browser.tabs.remove(tab[0].id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
async function delayAnimation(delay = 350) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, delay);
|
||||
});
|
||||
}
|
||||
|
||||
async function doAnimation(element, property, value) {
|
||||
return new Promise((resolve) => {
|
||||
const handler = () => {
|
||||
resolve();
|
||||
element.removeEventListener("transitionend", handler);
|
||||
};
|
||||
element.addEventListener("transitionend", handler);
|
||||
window.requestAnimationFrame(() => {
|
||||
element.style[property] = value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function addMessage(message) {
|
||||
const divElement = document.createElement("div");
|
||||
divElement.classList.add("container-notification");
|
||||
// Ideally we would use https://bugzilla.mozilla.org/show_bug.cgi?id=1340930 when this is available
|
||||
divElement.innerText = message.text;
|
||||
|
||||
const imageElement = document.createElement("img");
|
||||
const imagePath = browser.extension.getURL("/img/container-site-d-24.png");
|
||||
const response = await fetch(imagePath);
|
||||
const blob = await response.blob();
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
imageElement.src = objectUrl;
|
||||
divElement.prepend(imageElement);
|
||||
|
||||
document.body.appendChild(divElement);
|
||||
|
||||
await delayAnimation(100);
|
||||
await doAnimation(divElement, "transform", "translateY(0)");
|
||||
await delayAnimation(3000);
|
||||
await doAnimation(divElement, "transform", "translateY(-100%)");
|
||||
|
||||
divElement.remove();
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
addMessage(message);
|
||||
});
|
||||
@@ -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);
|
||||
@@ -0,0 +1,23 @@
|
||||
const DEFAULT_FAVICON = "/img/blank-favicon.svg";
|
||||
|
||||
// TODO use export here instead of globals
|
||||
window.Utils = {
|
||||
|
||||
createFavIconElement(url) {
|
||||
const imageElement = document.createElement("img");
|
||||
imageElement.classList.add("icon", "offpage");
|
||||
imageElement.src = url;
|
||||
const loadListener = (e) => {
|
||||
e.target.classList.remove("offpage");
|
||||
e.target.removeEventListener("load", loadListener);
|
||||
e.target.removeEventListener("error", errorListener);
|
||||
};
|
||||
const errorListener = (e) => {
|
||||
e.target.src = DEFAULT_FAVICON;
|
||||
};
|
||||
imageElement.addEventListener("error", errorListener);
|
||||
imageElement.addEventListener("load", loadListener);
|
||||
return imageElement;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Firefox Multi-Account Containers",
|
||||
"version": "6.2.0",
|
||||
"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": {
|
||||
"id": "@testpilot-containers",
|
||||
"strict_min_version": "67.0"
|
||||
}
|
||||
},
|
||||
"homepage_url": "https://github.com/mozilla/multi-account-containers#readme",
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"activeTab",
|
||||
"cookies",
|
||||
"contextMenus",
|
||||
"contextualIdentities",
|
||||
"history",
|
||||
"idle",
|
||||
"management",
|
||||
"storage",
|
||||
"tabs",
|
||||
"webRequestBlocking",
|
||||
"webRequest"
|
||||
],
|
||||
"optional_permissions": [
|
||||
"bookmarks"
|
||||
],
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Period",
|
||||
"mac": "MacCtrl+Period"
|
||||
},
|
||||
"description": "Open containers panel"
|
||||
}
|
||||
},
|
||||
"browser_action": {
|
||||
"browser_style": true,
|
||||
"default_icon": "img/container-site.svg",
|
||||
"default_title": "Multi-Account Containers",
|
||||
"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"
|
||||
],
|
||||
"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>
|
||||
@@ -0,0 +1,239 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Multi-Account Containers</title>
|
||||
<link rel="stylesheet" href="css/popup.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hide panel onboarding onboarding-panel-1">
|
||||
<img class="onboarding-img" alt="Container Tabs Overview" src="/img/onboarding-1.png" />
|
||||
<h3 class="onboarding-title">A better way to manage all the things you do online</h3>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="hide panel onboarding security-onboarding-panel-1">
|
||||
<img class="onboarding-img" alt="Container Tabs Overview" src="/img/onboarding-1.png" />
|
||||
<h3 class="onboarding-title">A simple and secure way to manage your online life</h3>
|
||||
<p>
|
||||
Use containers to organize tasks, manage accounts, and store sensitive data.
|
||||
</p>
|
||||
<a href="#" class="onboarding-button onboarding-start-button">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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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">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">Not Now</a>
|
||||
<a href="#" id="start-sync-button" class="half-onboarding-button">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">Not Now</a>
|
||||
<a href="#" id="sign-in" class="half-onboarding-button">Sign In</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel achievement-panel hide" id="achievement-panel">
|
||||
<img class="onboarding-img" alt="You achieved a Containers milestone!" src="/img/onboarding-3.png" />
|
||||
<h3 class="onboarding-title">100 tabs!</h3>
|
||||
<p>You've opened 100 Container tabs.</p>
|
||||
<p>If you enjoy Containers, help us spread the word!</p>
|
||||
<p class="share-ctas">
|
||||
<a class="cta-link" href="https://mzl.la/2gJtIZ4" id="achievement-rate-button" target="_blank">
|
||||
<span class="cta amo-rate-cta">
|
||||
<img src="/img/amo-icon.svg" class="cta-icon" alt="addons.mozilla.org Icon">
|
||||
Rate
|
||||
</span>
|
||||
</a>
|
||||
<a class="cta-link" href="https://bit.ly/fb-share-mac-addon" target="_blank">
|
||||
<span class="cta fb-share-cta">
|
||||
<img src="/img/webicon-facebook.svg" class="cta-icon" alt="Facebook Icon">
|
||||
Share
|
||||
</span>
|
||||
</a>
|
||||
<a class="cta-link" href="http://bit.ly/tweet-100-tabs-mac-addon" target="_blank">
|
||||
<span class="cta tweet-cta">
|
||||
<img src="/img/webicon-twitter.svg" class="cta-icon" alt="Twitter Icon">
|
||||
Tweet
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<a href="#" id="achievement-done-button" class="onboarding-button">Done</a>
|
||||
</div>
|
||||
|
||||
<div class="panel container-panel hide" id="container-panel">
|
||||
<div id="current-tab">
|
||||
<h3>Current Tab</h3>
|
||||
<div id="current-page"></div>
|
||||
<label for="container-page-assigned">
|
||||
<input type="checkbox" id="container-page-assigned" />
|
||||
<span class="truncate-text">
|
||||
Always open in
|
||||
<span id="current-container"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="container-panel-controls">
|
||||
<a href="#" class="action-link" id="sort-containers-link" title="Sort tabs into container order">Sort Tabs</a>
|
||||
</div>
|
||||
<div class="scrollable panel-content" tabindex="-1">
|
||||
<table class="identities-list">
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer edit-identities">
|
||||
<div class="edit-containers-text panel-footer-secondary">
|
||||
<a href="#" tabindex="0" id="edit-containers-link">Edit Containers</a>
|
||||
</div>
|
||||
<a href="#" tabindex="0" class="add-container-link pop-button" id="container-add-link" title="Create new container">
|
||||
<img class="pop-button-image-small icon" alt="Create new container icon" src="/img/container-add.svg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="hide panel container-info-panel" id="container-info-panel" tabindex="-1">
|
||||
<div class="columns">
|
||||
<div class="panel-back-arrow" id="close-container-info-panel">
|
||||
<img alt="Panel Back Arrow" src="/img/container-arrow.svg" class="back-arrow-img" />
|
||||
</div>
|
||||
<div class="column-panel-content">
|
||||
<div class="panel-header container-info-panel-header">
|
||||
<span class="usercontext-icon" id="container-info-icon"></span>
|
||||
<h3 id="container-info-name" class="panel-header-text container-name truncate-text"></h3>
|
||||
</div>
|
||||
<div class="select-row clickable container-info-panel-hide container-info-has-tabs" id="container-info-hideorshow">
|
||||
<img id="container-info-hideorshow-icon" alt="Hide Container icon" src="/img/container-hide.svg" class="icon container-info-panel-hideorshow-icon"/>
|
||||
<span id="container-info-hideorshow-label">Hide this container</span>
|
||||
</div>
|
||||
<div class="select-row clickable container-info-panel-movetabs container-info-has-tabs" id="container-info-movetabs">Move tabs to a new window</div>
|
||||
<div class="scrollable">
|
||||
<table id="container-info-table" class="container-info-list">
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="panel edit-containers-panel hide" id="edit-containers-panel">
|
||||
<div class="panel-header">
|
||||
<h3 class="panel-header-text">Edit Containers</h3>
|
||||
</div>
|
||||
<div class="scrollable panel-content">
|
||||
<table class="unstriped">
|
||||
<tbody id="edit-identities-list"></tbody>
|
||||
</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">
|
||||
<img src="/img/container-arrow.svg"/>Exit Edit Mode</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="panel edit-container-panel hide" id="edit-container-panel">
|
||||
<div class="columns">
|
||||
<div class="panel-back-arrow" id="edit-container-panel-back-arrow">
|
||||
<img alt="Panel Back Arrow" src="/img/container-arrow.svg" class="back-arrow-img" />
|
||||
</div>
|
||||
<div class="column-panel-content">
|
||||
<form id="edit-container-panel-form">
|
||||
<input type="hidden" name="container-id" id="edit-container-panel-usercontext-input" />
|
||||
<fieldset>
|
||||
<legend>Name</legend>
|
||||
<input type="text" name="container-name" id="edit-container-panel-name-input" maxlength="25"/>
|
||||
</fieldset>
|
||||
<fieldset id="edit-container-panel-choose-color" class="radio-choice">
|
||||
<legend>Choose a color</legend>
|
||||
</fieldset>
|
||||
<fieldset id="edit-container-panel-choose-icon" class="radio-choice">
|
||||
<legend>Choose an icon</legend>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div id="edit-sites-assigned" class="scrollable" hidden>
|
||||
<h3>Sites assigned to this container</h3>
|
||||
<div class="assigned-sites-list">
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<a href="#" class="button secondary expanded footer-button cancel-button" id="edit-container-cancel-link">Cancel</a>
|
||||
<a class="button primary expanded footer-button" id="edit-container-ok-link">OK</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide panel delete-container-panel" id="delete-container-panel">
|
||||
<div class="panel-header">
|
||||
<span class="usercontext-icon" id="delete-container-icon"></span>
|
||||
<h3 id="delete-container-name" class="panel-header-text container-name"></h3>
|
||||
</div>
|
||||
<div class="panel-content delete-container-confirm">
|
||||
<h4 class="delete-container-confirm-title">Remove This Container</h4>
|
||||
<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>
|
||||
<a href="#" class="button expanded primary footer-button" id="delete-container-ok-link">OK</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
const main = require("../");
|
||||
|
||||
exports["test main"] = function(assert) {
|
||||
assert.pass("Unit test running!");
|
||||
};
|
||||
|
||||
exports["test main async"] = function(assert, done) {
|
||||
assert.pass("async Unit test running!");
|
||||
done();
|
||||
};
|
||||
|
||||
exports["test dummy"] = function(assert, done) {
|
||||
main.dummy("foo", function(text) {
|
||||
assert.ok((text === "foo"), "Is the text actually 'foo'");
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
require("sdk/test").run(exports);
|
||||
@@ -1,333 +0,0 @@
|
||||
// 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/.
|
||||
|
||||
/**
|
||||
* Class that represents a metrics event broker. Events are sent to Google
|
||||
* Analytics if the `tid` parameter is set. Events are sent to Mozilla's
|
||||
* data pipeline via the Test Pilot add-on. No metrics code changes are
|
||||
* needed when the experiment is added to or removed from Test Pilot.
|
||||
* @constructor
|
||||
* @param {string} $0.id - addon ID, e.g. '@testpilot-addon'. See https://mdn.io/add_on_id.
|
||||
* @param {string} $0.version - addon version, e.g. '1.0.2'.
|
||||
* @param {string} [$0.uid] - unique identifier for a specific instance of an addon.
|
||||
* Optional, but required to send events to Google Analytics. Sent to Google Analytics
|
||||
* but not Mozilla services.
|
||||
* @param {string} [$0.tid] - Google Analytics tracking ID. Optional, but required
|
||||
* to send events to Google Analytics.
|
||||
* @param {string} [$0.type=webextension] - addon type. one of: 'webextension',
|
||||
* 'sdk', 'bootstrapped'.
|
||||
* @param {boolean} [$0.debug=false] - if true, enables logging. Note that this
|
||||
* value can be changed on a running instance, by modifying its `debug` property.
|
||||
* @throws {SyntaxError} If the required properties are missing, or if the
|
||||
* 'type' property is unrecognized.
|
||||
* @throws {Error} if initializing the transports fails.
|
||||
*/
|
||||
function Metrics({id, version, uid, tid = null, type = 'webextension', debug = false}) {
|
||||
if (!id) {
|
||||
throw new SyntaxError(`'id' property is required.`);
|
||||
} else if (!version) {
|
||||
throw new SyntaxError(`'version' property is required.`);
|
||||
} else if (tid && !uid) {
|
||||
throw new SyntaxError(`'uid' property is required to send events to Google Analytics.`);
|
||||
}
|
||||
|
||||
if (!['webextension', 'sdk', 'bootstrapped'].includes(type)) {
|
||||
throw new SyntaxError(`'type' property must be one of: 'webextension', 'sdk', or 'bootstrapped'`);
|
||||
}
|
||||
Object.assign(this, {id, uid, version, tid, type, debug});
|
||||
|
||||
// The test pilot add-on uses its own nsIObserverService topic for sending
|
||||
// pings to Telemetry. Otherwise, the topic is based on add-on type.
|
||||
if (id === '@testpilot-addon') {
|
||||
this.topic = 'testpilot';
|
||||
} else if (type === 'webextension') {
|
||||
this.topic = 'testpilot-telemetry';
|
||||
} else {
|
||||
this.topic = 'testpilottest';
|
||||
}
|
||||
|
||||
// NOTE: order is important here. _initTransports uses console.log, which may
|
||||
// not be available before _initConsole has run.
|
||||
this._initConsole();
|
||||
this._initTransports();
|
||||
|
||||
this.sendEvent = this.sendEvent.bind(this);
|
||||
|
||||
this._log(`Initialized topic to ${this.topic}`);
|
||||
if (!tid) {
|
||||
this._log(`Google Analytics disabled: 'tid' value not passed to constructor.`);
|
||||
} else {
|
||||
this._log(`Google Analytics enabled for Tracking ID ${tid}.`);
|
||||
}
|
||||
this._log('Constructor finished successfully.');
|
||||
}
|
||||
Metrics.prototype = {
|
||||
/**
|
||||
* Sends an event to the Mozilla data pipeline (and Google Analytics, if
|
||||
* a `tid` was passed to the constructor). Note: to avoid breaking callers,
|
||||
* if sending the event fails, no Errors will be thrown. Instead, the message
|
||||
* will be silently dropped, and, if debug mode is enabled, an error will be
|
||||
* logged to the Browser Console.
|
||||
*
|
||||
* If you want to pass extra fields to GA, or use a GA hit type other than
|
||||
* `Event`, you can transform the output data object yourself using the
|
||||
* `transform` parameter. You will need to add Custom Dimensions to GA for any
|
||||
* extra fields: https://support.google.com/analytics/answer/2709828. Note
|
||||
* that, by convention, the `variant` argument is mapped to the first Custom
|
||||
* Dimension (`cd1`) when constructing the GA Event hit.
|
||||
*
|
||||
* Note: the data object format is currently different for each experiment,
|
||||
* and should be defined based on the result of conversations with the Mozilla
|
||||
* data team.
|
||||
*
|
||||
* A suggested default format is:
|
||||
* @param {string} [$0.method] - What is happening? e.g. `click`
|
||||
* @param {string} [$0.object] - What is being affected? e.g. `home-button-1`
|
||||
* @param {string} [$0.category=interactions] - If you want to add a category
|
||||
* for easy reporting later. e.g. `mainmenu`
|
||||
* @param {string} [$0.variant=null] - An identifying string if you're running
|
||||
* different variants. e.g. `cohort-A`
|
||||
* @param {function} [transform] - Transform function used to alter the
|
||||
* parameters sent to GA. The `transform` function signature is
|
||||
* `transform(input, output)`, where `input` is the object passed to
|
||||
* `sendEvent` (excluding `transform`), and `output` is the default GA
|
||||
* object generated by the `_gaTransform` method. The `transform` function
|
||||
* should return an object whose keys are GA Measurement Protocol parameters.
|
||||
* The returned object will be form encoded and sent to GA.
|
||||
*/
|
||||
sendEvent: function(params = {}, transform) {
|
||||
const args = this._clone(params);
|
||||
args.object = params.object || null;
|
||||
args.category = params.category || 'interactions';
|
||||
args.variant = params.variant || null;
|
||||
|
||||
this._log(`sendEvent called with method = ${args.method}, object = ${args.object}, category = ${args.category}, variant = ${args.variant}.`);
|
||||
|
||||
const clientData = this._clone(args);
|
||||
const gaData = this._clone(args);
|
||||
if (!clientData) {
|
||||
this._error(`Unable to process data object. Dropping packet.`);
|
||||
return;
|
||||
}
|
||||
this._sendToClient(clientData);
|
||||
|
||||
if (this.tid && this.uid) {
|
||||
const defaultEvent = this._gaTransform(gaData);
|
||||
|
||||
let userEvent;
|
||||
if (transform) {
|
||||
userEvent = transform.call(null, gaData, defaultEvent);
|
||||
}
|
||||
|
||||
this._gaSend(userEvent || defaultEvent);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clone a data object by serializing / deserializing it.
|
||||
* @private
|
||||
* @param {object} o - Object to be cloned.
|
||||
* @returns A clone of the object, or `null` if cloning failed.
|
||||
*/
|
||||
_clone: function(o) {
|
||||
let cloned;
|
||||
try {
|
||||
cloned = JSON.parse(JSON.stringify(o));
|
||||
} catch (ex) {
|
||||
this._error(`Unable to clone object: ${ex}.`);
|
||||
return null;
|
||||
}
|
||||
return cloned;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends an event to the Mozilla data pipeline via the Test Pilot add-on.
|
||||
* Uses BroadcastChannel for WebExtensions, and nsIObserverService for other
|
||||
* add-on types.
|
||||
* @private
|
||||
* @param {object} params - Entire object sent to `sendEvent`.
|
||||
*/
|
||||
_sendToClient: function(params) {
|
||||
if (this.type === 'webextension') {
|
||||
this._channel.postMessage(params);
|
||||
this._log(`Sent client message via postMessage: ${params}`);
|
||||
} else {
|
||||
let stringified;
|
||||
|
||||
try {
|
||||
stringified = JSON.stringify(params);
|
||||
} catch(ex) {
|
||||
this._error(`Unable to serialize metrics event: ${ex}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = {
|
||||
wrappedJSObject: {
|
||||
observersModuleSubjectWrapper: true,
|
||||
object: this.id
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
Services.obs.notifyObservers(subject, 'testpilot::send-metric', stringified);
|
||||
this._log(`Sent client message via nsIObserverService: ${stringified}`);
|
||||
} catch (ex) {
|
||||
this._error(`Failed to send nsIObserver client ping: ${ex}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Transforms `sendEvent()` arguments into a Google Analytics `Event` hit.
|
||||
* @private
|
||||
* @param {string} method - see `sendEvent` docs
|
||||
* @param {string} [object] - see `sendEvent` docs
|
||||
* @param {string} category - see `sendEvent` docs. Note that `category` is
|
||||
* required here, assuming the default value was filled in by `sendEvent()`.
|
||||
* @param {string} variant - see `sendEvent` docs. Note that `variant` is
|
||||
* required here, assuming the default value was filled in by `sendEvent()`.
|
||||
*/
|
||||
_gaTransform: function({method, object, category, variant}) {
|
||||
const data = {
|
||||
v: 1,
|
||||
an: this.id,
|
||||
av: this.version,
|
||||
tid: this.tid,
|
||||
uid: this.uid,
|
||||
t: 'event',
|
||||
ec: category,
|
||||
ea: method
|
||||
};
|
||||
if (object) {
|
||||
data.el = object;
|
||||
}
|
||||
if (variant) {
|
||||
data.cd1 = variant;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Encodes and sends an event message to Google Analytics.
|
||||
* @private
|
||||
* @param {object} msg - An object whose keys correspond to parameters in the
|
||||
* Google Analytics Measurement Protocol.
|
||||
*/
|
||||
_gaSend: function(msg) {
|
||||
const encoded = this._formEncode(msg);
|
||||
const GA_URL = 'https://ssl.google-analytics.com/collect';
|
||||
if (this.type === 'webextension') {
|
||||
navigator.sendBeacon(GA_URL, encoded);
|
||||
} else {
|
||||
// SDK and bootstrapped types might not have a window reference, so get
|
||||
// the sendBeacon DOM API from the hidden window.
|
||||
Services.appShell.hiddenDOMWindow.navigator.sendBeacon(GA_URL, encoded);
|
||||
}
|
||||
this._log(`Sent GA message: ${encoded}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* URL encodes an object. Encodes spaces as '%20', not '+', following the
|
||||
* GA docs.
|
||||
*
|
||||
* @example
|
||||
* // returns 'a=b&foo=b%20ar'
|
||||
* metrics._formEncode({a: 'b', foo: 'b ar'});
|
||||
* @private
|
||||
* @param {Object} obj - Any JS object
|
||||
* @returns {string}
|
||||
*/
|
||||
_formEncode: function(obj) {
|
||||
const params = [];
|
||||
if (!obj) { return ''; }
|
||||
Object.keys(obj).forEach(item => {
|
||||
const encoded = encodeURIComponent(item) + '=' + encodeURIComponent(obj[item]);
|
||||
params.push(encoded);
|
||||
});
|
||||
return params.join('&');
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes transports used for sending messages. For WebExtensions,
|
||||
* creates a `BroadcastChannel` (transport for client pings). WebExtensions
|
||||
* use navigator.sendBeacon for GA transport, and they always have access
|
||||
* to DOM APIs, so there's no setup work required. For other types, loads
|
||||
* `Services.jsm`, which exposes the nsIObserverService (transport for client
|
||||
* pings), and exposes the navigator.sendBeacon API (GA transport) via the
|
||||
* appShell service's hidden window.
|
||||
* @private
|
||||
* @throws {Error} if transport setup unexpectedly fails
|
||||
*/
|
||||
_initTransports: function() {
|
||||
if (this.type === 'webextension') {
|
||||
try {
|
||||
this._channel = new BroadcastChannel(this.topic);
|
||||
} catch(ex) {
|
||||
throw new Error(`Unable to create BroadcastChannel: ${ex}`);
|
||||
}
|
||||
} else if (this.type === 'sdk') {
|
||||
try {
|
||||
const { Cu } = require('chrome');
|
||||
Cu.import('resource://gre/modules/Services.jsm');
|
||||
} catch(ex) {
|
||||
throw new Error(`Unable to load Services.jsm: ${ex}`);
|
||||
}
|
||||
} else { /* this.type === 'bootstrapped' */
|
||||
try {
|
||||
Components.utils.import('resource://gre/modules/Services.jsm');
|
||||
} catch(ex) {
|
||||
throw new Error(`Unable to load Services.jsm: ${ex}`);
|
||||
}
|
||||
}
|
||||
this._log('Successfully initialized transports.');
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes a console for 'bootstrapped' add-ons.
|
||||
* @private
|
||||
*/
|
||||
_initConsole: function() {
|
||||
if (this.type === 'bootstrapped') {
|
||||
try {
|
||||
Components.utils.import('resource://gre/modules/Console.jsm');
|
||||
this._log('Successfully initialized console.');
|
||||
} catch(ex) {
|
||||
throw new Error(`Unable to initialize console: ${ex}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Logs messages to the console. Only enabled if `this.debug` is truthy.
|
||||
* @private
|
||||
* @param {string} msg - A message
|
||||
*/
|
||||
_log: function(msg) {
|
||||
if (this.debug) {
|
||||
console.log(msg); // eslint-disable-line no-console
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Logs errors to the console. Only enabled if `this.debug` is truthy.
|
||||
* @private
|
||||
* @param {string} msg - An error message
|
||||
*/
|
||||
_error: function(msg) {
|
||||
if (this.debug) {
|
||||
console.error(msg); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// WebExtensions don't support CommonJS module style, so 'module' might not be
|
||||
// defined.
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = Metrics;
|
||||
}
|
||||
|
||||
// Export the Metrics constructor in Gecko JSM style, for legacy addons
|
||||
// that use the JSM loader. See also: https://mdn.io/jsm/using
|
||||
const EXPORTED_SYMBOLS = ['Metrics']; // eslint-disable-line no-unused-vars
|
||||
@@ -1,473 +0,0 @@
|
||||
const assignManager = {
|
||||
CLOSEABLE_WINDOWS: new Set([
|
||||
"about:startpage",
|
||||
"about:newtab",
|
||||
"about:home",
|
||||
"about:blank"
|
||||
]),
|
||||
MENU_ASSIGN_ID: "open-in-this-container",
|
||||
MENU_REMOVE_ID: "remove-open-in-this-container",
|
||||
storageArea: {
|
||||
area: browser.storage.local,
|
||||
|
||||
getSiteStoreKey(pageUrl) {
|
||||
const url = new window.URL(pageUrl);
|
||||
const storagePrefix = "siteContainerMap@@_";
|
||||
return `${storagePrefix}${url.hostname}`;
|
||||
},
|
||||
|
||||
get(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.area.get([siteStoreKey]).then((storageResponse) => {
|
||||
if (storageResponse && siteStoreKey in storageResponse) {
|
||||
resolve(storageResponse[siteStoreKey]);
|
||||
}
|
||||
resolve(null);
|
||||
}).catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
set(pageUrl, data) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
return this.area.set({
|
||||
[siteStoreKey]: data
|
||||
});
|
||||
},
|
||||
|
||||
remove(pageUrl) {
|
||||
const siteStoreKey = this.getSiteStoreKey(pageUrl);
|
||||
return this.area.remove([siteStoreKey]);
|
||||
},
|
||||
|
||||
deleteContainer(userContextId) {
|
||||
const removeKeys = [];
|
||||
this.area.get().then((siteConfigs) => {
|
||||
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)) {
|
||||
removeKeys.push(key);
|
||||
}
|
||||
});
|
||||
this.area.remove(removeKeys);
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
init() {
|
||||
browser.runtime.onMessage.addListener((neverAskMessage) => {
|
||||
const pageUrl = neverAskMessage.pageUrl;
|
||||
if (neverAskMessage.neverAsk === true) {
|
||||
// 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;
|
||||
this.storageArea.set(pageUrl, siteSettings);
|
||||
}
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
||||
if (userContextId) {
|
||||
let actionName;
|
||||
let storageAction;
|
||||
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
||||
actionName = "added";
|
||||
storageAction = this.storageArea.set(info.pageUrl, {
|
||||
userContextId,
|
||||
neverAsk: false
|
||||
});
|
||||
} else {
|
||||
actionName = "removed";
|
||||
storageAction = this.storageArea.remove(info.pageUrl);
|
||||
}
|
||||
storageAction.then(() => {
|
||||
browser.notifications.create({
|
||||
type: "basic",
|
||||
title: "Containers",
|
||||
message: `Successfully ${actionName} site to always open in this container`,
|
||||
iconUrl: browser.extension.getURL("/img/onboarding-1.png")
|
||||
});
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
event: `${actionName}-container-assignment`,
|
||||
userContextId: userContextId,
|
||||
});
|
||||
this.calculateContextMenu(tab);
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Before a request is handled by the browser we decide if we should route through a different container
|
||||
browser.webRequest.onBeforeRequest.addListener((options) => {
|
||||
if (options.frameId !== 0 || options.tabId === -1) {
|
||||
return {};
|
||||
}
|
||||
return Promise.all([
|
||||
browser.tabs.get(options.tabId),
|
||||
this.storageArea.get(options.url)
|
||||
]).then(([tab, siteSettings]) => {
|
||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
if (!siteSettings
|
||||
|| userContextId === siteSettings.userContextId
|
||||
|| tab.incognito) {
|
||||
return {};
|
||||
}
|
||||
|
||||
this.reloadPageInContainer(options.url, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk);
|
||||
this.calculateContextMenu(tab);
|
||||
|
||||
/* Removal of existing tabs:
|
||||
We aim to open the new assigned container tab / warning prompt in it's own tab:
|
||||
- As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back()
|
||||
- When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421
|
||||
If we are coming from an internal url that are used for the new tab page (CLOSEABLE_WINDOWS), 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 (this.CLOSEABLE_WINDOWS.has(tab.url)
|
||||
|| (messageHandler.lastCreatedTab
|
||||
&& messageHandler.lastCreatedTab.id === tab.id)) {
|
||||
browser.tabs.remove(tab.id);
|
||||
}
|
||||
return {
|
||||
cancel: true,
|
||||
};
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
|
||||
},
|
||||
|
||||
|
||||
deleteContainer(userContextId) {
|
||||
this.storageArea.deleteContainer(userContextId);
|
||||
},
|
||||
|
||||
getUserContextIdFromCookieStore(tab) {
|
||||
if (!("cookieStoreId" in tab)) {
|
||||
return false;
|
||||
}
|
||||
const cookieStore = tab.cookieStoreId;
|
||||
const container = cookieStore.replace("firefox-container-", "");
|
||||
if (container !== cookieStore) {
|
||||
return container;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
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:"
|
||||
|| tab.incognito) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
calculateContextMenu(tab) {
|
||||
// There is a focus issue in this menu where if you change window with a context menu click
|
||||
// you get the wrong menu display because of async
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16
|
||||
// We also can't change for always private mode
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
|
||||
const cookieStore = this.getUserContextIdFromCookieStore(tab);
|
||||
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
|
||||
browser.contextMenus.remove(this.MENU_REMOVE_ID);
|
||||
// Ensure we have a cookieStore to assign to
|
||||
if (cookieStore
|
||||
&& this.isTabPermittedAssign(tab)) {
|
||||
this.storageArea.get(tab.url).then((siteSettings) => {
|
||||
// ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418
|
||||
let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick
|
||||
let menuId = this.MENU_ASSIGN_ID;
|
||||
if (siteSettings) {
|
||||
prefix = "✓";
|
||||
menuId = this.MENU_REMOVE_ID;
|
||||
}
|
||||
browser.contextMenus.create({
|
||||
id: menuId,
|
||||
title: `${prefix} Always Open in This Container`,
|
||||
checked: true,
|
||||
contexts: ["all"],
|
||||
});
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
reloadPageInContainer(url, userContextId, index, neverAsk = false) {
|
||||
const loadPage = browser.extension.getURL("confirm-page.html");
|
||||
// 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: `firefox-container-${userContextId}`, index});
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
event: "auto-reload-page-in-container",
|
||||
userContextId: userContextId,
|
||||
});
|
||||
} else {
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
event: "prompt-to-reload-page-in-container",
|
||||
userContextId: userContextId,
|
||||
});
|
||||
const confirmUrl = `${loadPage}?url=${url}`;
|
||||
browser.tabs.create({url: confirmUrl, cookieStoreId: `firefox-container-${userContextId}`, index}).then(() => {
|
||||
// We don't want to sync this URL ever nor clutter the users history
|
||||
browser.history.deleteUrl({url: confirmUrl});
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const messageHandler = {
|
||||
// After the timer completes we assume it's a tab the user meant to keep open
|
||||
// We use this to catch redirected tabs that have just opened
|
||||
// If this were in platform we would change how the tab opens based on "new tab" link navigations such as ctrl+click
|
||||
LAST_CREATED_TAB_TIMER: 2000,
|
||||
|
||||
init() {
|
||||
// Handles messages from index.js
|
||||
const port = browser.runtime.connect();
|
||||
port.onMessage.addListener(m => {
|
||||
switch (m.type) {
|
||||
case "lightweight-theme-changed":
|
||||
themeManager.update(m.message);
|
||||
break;
|
||||
case "delete-container":
|
||||
assignManager.deleteContainer(m.message.userContextId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled message type: ${m.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
browser.tabs.onCreated.addListener((tab) => {
|
||||
// This works at capturing the tabs as they are created
|
||||
// However we need onFocusChanged and onActivated to capture the initial tab
|
||||
if (tab.id === -1) {
|
||||
return {};
|
||||
}
|
||||
tabPageCounter.initTabCounter(tab);
|
||||
});
|
||||
|
||||
browser.tabs.onRemoved.addListener((tabId) => {
|
||||
if (tabId === -1) {
|
||||
return {};
|
||||
}
|
||||
tabPageCounter.sendTabCountAndDelete(tabId);
|
||||
});
|
||||
|
||||
browser.tabs.onActivated.addListener((info) => {
|
||||
browser.tabs.get(info.tabId).then((tab) => {
|
||||
tabPageCounter.initTabCounter(tab);
|
||||
assignManager.calculateContextMenu(tab);
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
|
||||
browser.windows.onFocusChanged.addListener((windowId) => {
|
||||
browser.tabs.query({active: true, windowId}).then((tabs) => {
|
||||
if (tabs && tabs[0]) {
|
||||
tabPageCounter.initTabCounter(tabs[0]);
|
||||
assignManager.calculateContextMenu(tabs[0]);
|
||||
}
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
|
||||
browser.idle.onStateChanged.addListener((newState) => {
|
||||
browser.tabs.query({}).then(tabs => {
|
||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
||||
if (newState === "idle") {
|
||||
tabPageCounter.sendTabCountAndDelete(tab.id, "user-went-idle");
|
||||
} else if (newState === "active" && tab.active) {
|
||||
tabPageCounter.initTabCounter(tab);
|
||||
}
|
||||
}
|
||||
}).catch(e => {
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
|
||||
browser.webRequest.onCompleted.addListener((details) => {
|
||||
if (details.frameId !== 0 || details.tabId === -1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
browser.tabs.get(details.tabId).then((tab) => {
|
||||
tabPageCounter.incrementTabCount(tab);
|
||||
assignManager.calculateContextMenu(tab);
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}, {urls: ["<all_urls>"], types: ["main_frame"]});
|
||||
|
||||
// lets remember the last tab created so we can close it if it looks like a redirect
|
||||
browser.tabs.onCreated.addListener((details) => {
|
||||
this.lastCreatedTab = details;
|
||||
setTimeout(() => {
|
||||
this.lastCreatedTab = null;
|
||||
}, this.LAST_CREATED_TAB_TIMER);
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const themeManager = {
|
||||
existingTheme: null,
|
||||
init() {
|
||||
this.check();
|
||||
},
|
||||
setPopupIcon(theme) {
|
||||
let icons = {
|
||||
16: "img/container-site-d-24.png",
|
||||
32: "img/container-site-d-48.png"
|
||||
};
|
||||
if (theme === "firefox-compact-dark@mozilla.org") {
|
||||
icons = {
|
||||
16: "img/container-site-w-24.png",
|
||||
32: "img/container-site-w-48.png"
|
||||
};
|
||||
}
|
||||
browser.browserAction.setIcon({
|
||||
path: icons
|
||||
});
|
||||
},
|
||||
check() {
|
||||
browser.runtime.sendMessage({
|
||||
method: "getTheme"
|
||||
}).then((theme) => {
|
||||
this.update(theme);
|
||||
}).catch(() => {
|
||||
throw new Error("Unable to get theme");
|
||||
});
|
||||
},
|
||||
update(theme) {
|
||||
if (this.existingTheme !== theme) {
|
||||
this.setPopupIcon(theme);
|
||||
this.existingTheme = theme;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tabPageCounter = {
|
||||
counters: {},
|
||||
|
||||
initTabCounter(tab) {
|
||||
if (tab.id in this.counters) {
|
||||
if (!("activity" in this.counters[tab.id])) {
|
||||
this.counters[tab.id].activity = {
|
||||
"cookieStoreId": tab.cookieStoreId,
|
||||
"pageRequests": 0
|
||||
};
|
||||
}
|
||||
if (!("tab" in this.counters[tab.id])) {
|
||||
this.counters[tab.id].tab = {
|
||||
"cookieStoreId": tab.cookieStoreId,
|
||||
"pageRequests": 0
|
||||
};
|
||||
}
|
||||
} else {
|
||||
this.counters[tab.id] = {};
|
||||
this.counters[tab.id].tab = {
|
||||
"cookieStoreId": tab.cookieStoreId,
|
||||
"pageRequests": 0
|
||||
};
|
||||
this.counters[tab.id].activity = {
|
||||
"cookieStoreId": tab.cookieStoreId,
|
||||
"pageRequests": 0
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
sendTabCountAndDelete(tabId, why = "user-closed-tab") {
|
||||
if (!(this.counters[tabId])) {
|
||||
return;
|
||||
}
|
||||
if (why === "user-closed-tab" && this.counters[tabId].tab) {
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
event: "page-requests-completed-per-tab",
|
||||
userContextId: this.counters[tabId].tab.cookieStoreId,
|
||||
pageRequestCount: this.counters[tabId].tab.pageRequests
|
||||
});
|
||||
// When we send the ping because the user closed the tab,
|
||||
// delete both the 'tab' and 'activity' counters
|
||||
delete this.counters[tabId];
|
||||
} else if (why === "user-went-idle" && this.counters[tabId].activity) {
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
event: "page-requests-completed-per-activity",
|
||||
userContextId: this.counters[tabId].activity.cookieStoreId,
|
||||
pageRequestCount: this.counters[tabId].activity.pageRequests
|
||||
});
|
||||
// When we send the ping because the user went idle,
|
||||
// only reset the 'activity' counter
|
||||
this.counters[tabId].activity = {
|
||||
"cookieStoreId": this.counters[tabId].tab.cookieStoreId,
|
||||
"pageRequests": 0
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
incrementTabCount(tab) {
|
||||
this.counters[tab.id].tab.pageRequests++;
|
||||
this.counters[tab.id].activity.pageRequests++;
|
||||
}
|
||||
};
|
||||
|
||||
assignManager.init();
|
||||
themeManager.init();
|
||||
// Lets do this last as theme manager did a check before connecting before
|
||||
messageHandler.init();
|
||||
|
||||
browser.runtime.sendMessage({
|
||||
method: "getPreference",
|
||||
pref: "browser.privatebrowsing.autostart"
|
||||
}).then(pbAutoStart => {
|
||||
|
||||
// We don't want to disable the addon if we are in auto private-browsing.
|
||||
if (!pbAutoStart) {
|
||||
browser.tabs.onCreated.addListener(tab => {
|
||||
if (tab.incognito) {
|
||||
disableAddon(tab.id);
|
||||
}
|
||||
});
|
||||
|
||||
browser.tabs.query({}).then(tabs => {
|
||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
||||
if (tab.incognito) {
|
||||
disableAddon(tab.id);
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
function disableAddon(tabId) {
|
||||
browser.browserAction.disable(tabId);
|
||||
browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" });
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Containers confirm navigation</title>
|
||||
<link xmlns="http://www.w3.org/1999/xhtml" rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
|
||||
<link rel="stylesheet" href="/css/confirm-page.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="title">
|
||||
<h1 class="title-text">Should we open this in your container?</h1>
|
||||
</div>
|
||||
<form id="redirect-form">
|
||||
<p>
|
||||
Looks like you requested:
|
||||
</p>
|
||||
<div id="redirect-url"></div>
|
||||
<p>
|
||||
You asked <dfn id="browser-name" title="Thanks for trying out Containers. Sorry we may have got your browser name wrong. #FxNightly" >Firefox</dfn> to always open <dfn id="redirect-site"></dfn> in <dfn>this</dfn> type of container. Would you like to proceed?<br />
|
||||
</p>
|
||||
<br />
|
||||
<br />
|
||||
<input id="never-ask" type="checkbox" /><label for="never-ask">Remember my decision for this site</label>
|
||||
<br />
|
||||
<div class="button-container">
|
||||
<button id="confirm" class="button primary" autofocus>Take me there</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script src="js/confirm-page.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,41 +0,0 @@
|
||||
/* General Rules and Resets */
|
||||
.title {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
main {
|
||||
background: url(/img/onboarding-1.png) no-repeat;
|
||||
background-position: -10px -15px;
|
||||
background-size: 285px;
|
||||
margin-inline-start: -285px;
|
||||
padding-inline-start: 285px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1300px) {
|
||||
main {
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* for a mid sized window we have enough for this but not our image */
|
||||
.title {
|
||||
background-image: url("chrome://global/skin/icons/info.svg");
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font: message-box;
|
||||
}
|
||||
|
||||
#redirect-url,
|
||||
#redirect-origin {
|
||||
font-weight: bold;
|
||||
|
||||
/* max-inline-size is needed to force this text smaller than the layout at a mid-sized window */
|
||||
max-inline-size: 40rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
dfn {
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -1,593 +0,0 @@
|
||||
/* General Rules and Resets */
|
||||
body {
|
||||
inline-size: 300px;
|
||||
max-inline-size: 300px;
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 0;
|
||||
border-spacing: 0;
|
||||
inline-size: 100%;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
/* Helper Classes */
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
inline-size: 100%;
|
||||
max-block-size: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Color and icon helpers */
|
||||
[data-identity-color="blue"] {
|
||||
--identity-tab-color: #37adff;
|
||||
--identity-icon-color: #37adff;
|
||||
}
|
||||
|
||||
[data-identity-color="turquoise"] {
|
||||
--identity-tab-color: #00c79a;
|
||||
--identity-icon-color: #00c79a;
|
||||
}
|
||||
|
||||
[data-identity-color="green"] {
|
||||
--identity-tab-color: #51cd00;
|
||||
--identity-icon-color: #51cd00;
|
||||
}
|
||||
|
||||
[data-identity-color="yellow"] {
|
||||
--identity-tab-color: #ffcb00;
|
||||
--identity-icon-color: #ffcb00;
|
||||
}
|
||||
|
||||
[data-identity-color="orange"] {
|
||||
--identity-tab-color: #ff9f00;
|
||||
--identity-icon-color: #ff9f00;
|
||||
}
|
||||
|
||||
[data-identity-color="red"] {
|
||||
--identity-tab-color: #ff613d;
|
||||
--identity-icon-color: #ff613d;
|
||||
}
|
||||
|
||||
[data-identity-color="pink"] {
|
||||
--identity-tab-color: #ff4bda;
|
||||
--identity-icon-color: #ff4bda;
|
||||
}
|
||||
|
||||
[data-identity-color="purple"] {
|
||||
--identity-tab-color: #af51f5;
|
||||
--identity-icon-color: #af51f5;
|
||||
}
|
||||
|
||||
[data-identity-icon="fingerprint"] {
|
||||
--identity-icon: url("/img/usercontext.svg#fingerprint");
|
||||
}
|
||||
|
||||
[data-identity-icon="briefcase"] {
|
||||
--identity-icon: url("/img/usercontext.svg#briefcase");
|
||||
}
|
||||
|
||||
[data-identity-icon="dollar"] {
|
||||
--identity-icon: url("/img/usercontext.svg#dollar");
|
||||
}
|
||||
|
||||
[data-identity-icon="cart"] {
|
||||
--identity-icon: url("/img/usercontext.svg#cart");
|
||||
}
|
||||
|
||||
[data-identity-icon="circle"] {
|
||||
--identity-icon: url("/img/usercontext.svg#circle");
|
||||
}
|
||||
|
||||
[data-identity-icon="food"] {
|
||||
--identity-icon: url("/img/usercontext.svg#food");
|
||||
}
|
||||
|
||||
[data-identity-icon="gift"] {
|
||||
--identity-icon: url("/img/usercontext.svg#gift");
|
||||
}
|
||||
|
||||
[data-identity-icon="vacation"] {
|
||||
--identity-icon: url("/img/usercontext.svg#vacation");
|
||||
}
|
||||
|
||||
[data-identity-icon="fruit"] {
|
||||
--identity-icon: url("/img/usercontext.svg#fruit");
|
||||
}
|
||||
|
||||
[data-identity-icon="pet"] {
|
||||
--identity-icon: url("/img/usercontext.svg#pet");
|
||||
}
|
||||
|
||||
[data-identity-icon="tree"] {
|
||||
--identity-icon: url("/img/usercontext.svg#tree");
|
||||
}
|
||||
|
||||
[data-identity-icon="chill"] {
|
||||
--identity-icon: url("/img/usercontext.svg#chill");
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.button.primary {
|
||||
background-color: #0996f8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button.primary:hover {
|
||||
background-color: #0675d3;
|
||||
}
|
||||
|
||||
.button.secondary:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Panels keep everything togethert */
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-block-size: 400px;
|
||||
}
|
||||
|
||||
.panel.onboarding {
|
||||
align-items: center;
|
||||
block-size: 360px;
|
||||
margin-block-end: 16px;
|
||||
margin-block-start: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: 16px;
|
||||
min-block-size: 360px;
|
||||
}
|
||||
|
||||
.panel .columns {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Column panels for edit screens */
|
||||
.column-panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
inline-size: 268px;
|
||||
}
|
||||
|
||||
.column-panel-content .panel-footer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.column-panel-content .button,
|
||||
.panel-footer .button {
|
||||
align-items: center;
|
||||
block-size: 54px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Column panels have a special back arrow */
|
||||
.panel-back-arrow {
|
||||
align-items: center;
|
||||
background: #ebebeb;
|
||||
box-shadow: inset -2px 0 4px -2px rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
flex: 0 0 32px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.panel-back-arrow:hover {
|
||||
background: #dedede;
|
||||
}
|
||||
|
||||
.back-arrow-img {
|
||||
block-size: 16px;
|
||||
inline-size: 16px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Onboarding styles */
|
||||
.onboarding * {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.onboarding-img {
|
||||
block-size: 132px;
|
||||
inline-size: 180px;
|
||||
}
|
||||
|
||||
.onboarding-title {
|
||||
color: #43484e;
|
||||
font-size: 16px;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
max-inline-size: 80%;
|
||||
}
|
||||
|
||||
.onboarding p {
|
||||
color: #4a4a4a;
|
||||
font-size: 14px;
|
||||
margin-block-end: 16px;
|
||||
max-inline-size: 84%;
|
||||
}
|
||||
|
||||
.onboarding-button {
|
||||
align-items: center;
|
||||
background-color: #0996f8;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex: 0 0 44px;
|
||||
font-size: 14px;
|
||||
inline-size: 100%;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
transition: background-color 75ms;
|
||||
}
|
||||
|
||||
.onboarding-button:hover {
|
||||
background-color: #0675d3;
|
||||
}
|
||||
|
||||
/* Pop buttons are the square shaped buttons used to
|
||||
manage things like container crud */
|
||||
.pop-button {
|
||||
align-items: center;
|
||||
block-size: 48px;
|
||||
display: flex;
|
||||
flex: 0 0 48px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pop-button:hover,
|
||||
.panel-footer-secondary:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.pop-button:active,
|
||||
.panel-footer-secondary:active {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.pop-button-image {
|
||||
block-size: 20px;
|
||||
flex: 0 0 20px;
|
||||
}
|
||||
|
||||
.pop-button-image-small {
|
||||
block-size: 12px;
|
||||
flex: 0 0 12px;
|
||||
}
|
||||
|
||||
/* Panel Header */
|
||||
.panel-header {
|
||||
align-items: center;
|
||||
block-size: 48px;
|
||||
border-block-end: 1px solid #ebebeb;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.column-panel-content .panel-header {
|
||||
flex: 0 0 48px;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.panel-header-text {
|
||||
color: #4a4a4a;
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
padding-block-end: 16px;
|
||||
padding-block-start: 16px;
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
span ~ .panel-header-text {
|
||||
padding-block-end: 0;
|
||||
padding-block-start: 0;
|
||||
padding-inline-end: 0;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
/* Rows used when iterating over panels */
|
||||
.container-panel-row {
|
||||
align-items: center;
|
||||
background-color: #fefefe !important;
|
||||
block-size: 48px;
|
||||
border-block-end: 1px solid #f1f1f1;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.container-panel-row .container-name {
|
||||
max-inline-size: 160px;
|
||||
overflow: hidden;
|
||||
padding-inline-end: 4px;
|
||||
padding-inline-start: 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.edit-containers-panel .userContext-wrapper {
|
||||
max-inline-size: 204px;
|
||||
}
|
||||
|
||||
.userContext-wrapper {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1 1;
|
||||
transition: background-color 75ms;
|
||||
}
|
||||
|
||||
.clickable.userContext-wrapper:hover {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.userContext-icon-wrapper {
|
||||
block-size: 48px;
|
||||
flex: 0 0 48px;
|
||||
}
|
||||
|
||||
/* .userContext-icon is used natively, Bug 1333811 was raised to fix */
|
||||
.userContext-icon,
|
||||
.usercontext-icon {
|
||||
background-image: var(--identity-icon);
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 20px 20px;
|
||||
block-size: 48px;
|
||||
fill: var(--identity-icon-color);
|
||||
filter: url('/img/filters.svg#fill');
|
||||
flex: 0 0 48px;
|
||||
}
|
||||
|
||||
.clickable:hover .userContext-icon,
|
||||
.clickable:hover .usercontext-icon {
|
||||
background-image: url('/img/container-newtab.svg');
|
||||
fill: 'gray';
|
||||
filter: url('/img/filters.svg#fill');
|
||||
}
|
||||
|
||||
/* Panel Footer */
|
||||
.panel-footer {
|
||||
align-items: center;
|
||||
background: #efefef;
|
||||
block-size: 54px;
|
||||
border-block-end: 1px solid #d8d8d8;
|
||||
color: #000;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
inline-size: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.panel-footer .pop-button {
|
||||
block-size: 54px;
|
||||
flex: 0 0 54px;
|
||||
}
|
||||
|
||||
.edit-containers-text {
|
||||
align-items: center;
|
||||
block-size: 54px;
|
||||
border-inline-end: solid 1px #d8d8d8;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.edit-containers-text a {
|
||||
align-items: center;
|
||||
block-size: 54px;
|
||||
color: #0a0a0a;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Container info list */
|
||||
#container-info-name {
|
||||
margin-inline-end: 0.5rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#container-info-hideorshow {
|
||||
margin-block-start: 4px;
|
||||
}
|
||||
|
||||
#container-info-movetabs-incompat {
|
||||
font-size: 10px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.container-info-tab-row:not(.clickable),
|
||||
.select-row:not(.clickable) {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.container-info-has-tabs,
|
||||
.container-info-tab-row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 0 0 28px;
|
||||
font-size: 14px;
|
||||
justify-content: flex-start;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
.container-info-has-tabs img,
|
||||
.container-info-tab-row img {
|
||||
block-size: 16px;
|
||||
flex: 0 0 16px;
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
|
||||
.container-info-tab-row img[src=""] {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
.container-info-tab-row td {
|
||||
max-inline-size: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.container-info-list {
|
||||
border-block-start: 1px solid #ebebeb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-block-start: 4px;
|
||||
padding-block-start: 4px;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickable:hover {
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
|
||||
.edit-containers-exit-text {
|
||||
align-items: center;
|
||||
background: #248aeb;
|
||||
block-size: 100%;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.exit-edit-mode-link::before {
|
||||
background: url('/img/container-arrow.svg') no-repeat;
|
||||
block-size: 16px;
|
||||
content: "";
|
||||
display: block;
|
||||
filter: grayscale(100%) brightness(5);
|
||||
float: left;
|
||||
inline-size: 16px;
|
||||
margin-inline-end: 5px;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.delete-container-confirm {
|
||||
padding-inline-end: 20px;
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
|
||||
.delete-container-confirm-title {
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Form info */
|
||||
.column-panel-content form {
|
||||
flex: 1;
|
||||
padding-block-end: 16px;
|
||||
padding-block-start: 16px;
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
.column-panel-content form span {
|
||||
align-items: center;
|
||||
block-size: 44px;
|
||||
display: flex;
|
||||
flex: 0 0 25%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.edit-container-panel label {
|
||||
background-image: var(--identity-icon);
|
||||
background-size: 26px 26px;
|
||||
block-size: 34px;
|
||||
fill: var(--identity-icon-color);
|
||||
filter: url('/img/filters.svg#fill');
|
||||
flex: 0 0 34px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.edit-container-panel label::before {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.edit-container-panel [type="radio"] {
|
||||
display: inline;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.edit-container-panel [type="radio"]:checked + label {
|
||||
outline: 2px solid grey;
|
||||
-moz-outline-radius: 50px;
|
||||
}
|
||||
|
||||
.edit-container-panel fieldset {
|
||||
background: none;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
inline-size: 100%;
|
||||
margin-block-end: 10px;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 0;
|
||||
padding-block-end: 0;
|
||||
padding-block-start: 0;
|
||||
padding-inline-end: 0;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.edit-container-panel input[type="text"] {
|
||||
block-size: 36px;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
inline-size: 100%;
|
||||
padding-block-end: 5px;
|
||||
padding-block-start: 5px;
|
||||
padding-inline-end: 5px;
|
||||
padding-inline-start: 5px;
|
||||
}
|
||||
|
||||
.edit-container-panel legend {
|
||||
flex: 1 0;
|
||||
font-size: 14px !important;
|
||||
padding-block-end: 5px;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
const redirectUrl = new URL(window.location).searchParams.get("url");
|
||||
document.getElementById("redirect-url").textContent = redirectUrl;
|
||||
const redirectSite = new URL(redirectUrl).hostname;
|
||||
document.getElementById("redirect-site").textContent = redirectSite;
|
||||
|
||||
document.getElementById("redirect-form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
const neverAsk = document.getElementById("never-ask").checked;
|
||||
// Sending neverAsk message to background to store for next time we see this process
|
||||
if (neverAsk) {
|
||||
browser.runtime.sendMessage({
|
||||
neverAsk: true,
|
||||
pageUrl: redirectUrl
|
||||
}).then(() => {
|
||||
redirect();
|
||||
}).catch(() => {
|
||||
// Can't really do much here user will have to click it again
|
||||
});
|
||||
}
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
event: "click-to-reload-page-in-container",
|
||||
});
|
||||
redirect();
|
||||
});
|
||||
|
||||
function redirect() {
|
||||
const redirectUrl = document.getElementById("redirect-url").textContent;
|
||||
document.location.replace(redirectUrl);
|
||||
}
|
||||
@@ -1,634 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
const CONTAINER_HIDE_SRC = "/img/container-hide.svg";
|
||||
const CONTAINER_UNHIDE_SRC = "/img/container-unhide.svg";
|
||||
|
||||
const DEFAULT_COLOR = "blue";
|
||||
const DEFAULT_ICON = "circle";
|
||||
|
||||
// List of panels
|
||||
const P_ONBOARDING_1 = "onboarding1";
|
||||
const P_ONBOARDING_2 = "onboarding2";
|
||||
const P_ONBOARDING_3 = "onboarding3";
|
||||
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";
|
||||
|
||||
/**
|
||||
* Escapes any occurances of &, ", <, > or / with XML entities.
|
||||
*
|
||||
* @param {string} str
|
||||
* The string to escape.
|
||||
* @return {string} The escaped string.
|
||||
*/
|
||||
function escapeXML(str) {
|
||||
const replacements = {"&": "&", "\"": """, "'": "'", "<": "<", ">": ">", "/": "/"};
|
||||
return String(str).replace(/[&"'<>/]/g, m => replacements[m]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A tagged template function which escapes any XML metacharacters in
|
||||
* interpolated values.
|
||||
*
|
||||
* @param {Array<string>} strings
|
||||
* An array of literal strings extracted from the templates.
|
||||
* @param {Array} values
|
||||
* An array of interpolated values extracted from the template.
|
||||
* @returns {string}
|
||||
* The result of the escaped values interpolated with the literal
|
||||
* strings.
|
||||
*/
|
||||
function escaped(strings, ...values) {
|
||||
const result = [];
|
||||
|
||||
for (const [i, string] of strings.entries()) {
|
||||
result.push(string);
|
||||
if (i < values.length)
|
||||
result.push(escapeXML(values[i]));
|
||||
}
|
||||
|
||||
return result.join("");
|
||||
}
|
||||
|
||||
// This object controls all the panels, identities and many other things.
|
||||
const Logic = {
|
||||
_identities: [],
|
||||
_currentIdentity: null,
|
||||
_currentPanel: null,
|
||||
_previousPanel: null,
|
||||
_panels: {},
|
||||
|
||||
init() {
|
||||
// Retrieve the list of identities.
|
||||
this.refreshIdentities()
|
||||
|
||||
// Routing to the correct panel.
|
||||
.then(() => {
|
||||
// If localStorage is disabled, we don't show the onboarding.
|
||||
if (!localStorage || localStorage.getItem("onboarded3")) {
|
||||
this.showPanel(P_CONTAINERS_LIST);
|
||||
} else if (localStorage.getItem("onboarded2")) {
|
||||
this.showPanel(P_ONBOARDING_3);
|
||||
} else if (localStorage.getItem("onboarded1")) {
|
||||
this.showPanel(P_ONBOARDING_2);
|
||||
} else {
|
||||
this.showPanel(P_ONBOARDING_1);
|
||||
}
|
||||
})
|
||||
|
||||
.catch(() => {
|
||||
throw new Error("Failed to retrieve the identities. We cannot continue.");
|
||||
});
|
||||
},
|
||||
|
||||
refreshIdentities() {
|
||||
return browser.runtime.sendMessage({
|
||||
method: "queryIdentities"
|
||||
})
|
||||
.then(identities => {
|
||||
this._identities = identities;
|
||||
});
|
||||
},
|
||||
|
||||
showPanel(panel, currentIdentity = null) {
|
||||
// Invalid panel... ?!?
|
||||
if (!(panel in this._panels)) {
|
||||
throw new Error("Something really bad happened. Unknown panel: " + panel);
|
||||
}
|
||||
|
||||
this._previousPanel = this._currentPanel;
|
||||
this._currentPanel = panel;
|
||||
|
||||
this._currentIdentity = currentIdentity;
|
||||
|
||||
// Initialize the panel before showing it.
|
||||
this._panels[panel].prepare().then(() => {
|
||||
for (let panelElement of document.querySelectorAll(".panel")) { // eslint-disable-line prefer-const
|
||||
panelElement.classList.add("hide");
|
||||
}
|
||||
document.querySelector(this._panels[panel].panelSelector).classList.remove("hide");
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error("Failed to show panel " + panel);
|
||||
});
|
||||
},
|
||||
|
||||
showPreviousPanel() {
|
||||
if (!this._previousPanel) {
|
||||
throw new Error("Current panel not set!");
|
||||
}
|
||||
|
||||
this.showPanel(this._previousPanel, this._currentIdentity);
|
||||
},
|
||||
|
||||
registerPanel(panelName, panelObject) {
|
||||
this._panels[panelName] = panelObject;
|
||||
panelObject.initialize();
|
||||
},
|
||||
|
||||
identities() {
|
||||
return this._identities;
|
||||
},
|
||||
|
||||
currentIdentity() {
|
||||
if (!this._currentIdentity) {
|
||||
throw new Error("CurrentIdentity must be set before calling Logic.currentIdentity.");
|
||||
}
|
||||
return this._currentIdentity;
|
||||
},
|
||||
|
||||
generateIdentityName() {
|
||||
const defaultName = "Container #";
|
||||
const ids = [];
|
||||
|
||||
// This loop populates the 'ids' array with all the already-used ids.
|
||||
this._identities.forEach(identity => {
|
||||
if (identity.name.startsWith(defaultName)) {
|
||||
const id = parseInt(identity.name.substr(defaultName.length), 10);
|
||||
if (id) {
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Here we find the first valid id.
|
||||
for (let id = 1;; ++id) {
|
||||
if (ids.indexOf(id) === -1) {
|
||||
return defaultName + (id < 10 ? "0" : "") + id;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// P_ONBOARDING_1: First page for Onboarding.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Logic.registerPanel(P_ONBOARDING_1, {
|
||||
panelSelector: ".onboarding-panel-1",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the next panel.
|
||||
document.querySelector("#onboarding-start-button").addEventListener("click", () => {
|
||||
localStorage.setItem("onboarded1", true);
|
||||
Logic.showPanel(P_ONBOARDING_2);
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
});
|
||||
|
||||
// P_ONBOARDING_2: Second page for Onboarding.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Logic.registerPanel(P_ONBOARDING_2, {
|
||||
panelSelector: ".onboarding-panel-2",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
document.querySelector("#onboarding-next-button").addEventListener("click", () => {
|
||||
localStorage.setItem("onboarded2", true);
|
||||
Logic.showPanel(P_ONBOARDING_3);
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
});
|
||||
|
||||
// P_ONBOARDING_3: Third page for Onboarding.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Logic.registerPanel(P_ONBOARDING_3, {
|
||||
panelSelector: ".onboarding-panel-3",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
// Let's move to the containers list panel.
|
||||
document.querySelector("#onboarding-done-button").addEventListener("click", () => {
|
||||
localStorage.setItem("onboarded3", true);
|
||||
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.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||
panelSelector: "#container-panel",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
document.querySelector("#container-add-link").addEventListener("click", () => {
|
||||
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
|
||||
});
|
||||
|
||||
document.querySelector("#edit-containers-link").addEventListener("click", () => {
|
||||
browser.runtime.sendMessage({
|
||||
method: "sendTelemetryPayload",
|
||||
event: "edit-containers"
|
||||
});
|
||||
Logic.showPanel(P_CONTAINERS_EDIT);
|
||||
});
|
||||
|
||||
document.querySelector("#sort-containers-link").addEventListener("click", () => {
|
||||
browser.runtime.sendMessage({
|
||||
method: "sortTabs"
|
||||
}).then(() => {
|
||||
window.close();
|
||||
}).catch(() => {
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
Logic.identities().forEach(identity => {
|
||||
const hasTabs = (identity.hasHiddenTabs || identity.hasOpenTabs);
|
||||
const tr = document.createElement("tr");
|
||||
const context = document.createElement("td");
|
||||
const manage = document.createElement("td");
|
||||
|
||||
tr.classList.add("container-panel-row");
|
||||
context.classList.add("userContext-wrapper", "open-newtab", "clickable");
|
||||
manage.classList.add("show-tabs", "pop-button");
|
||||
context.innerHTML = escaped`
|
||||
<div class="userContext-icon-wrapper open-newtab">
|
||||
<div class="userContext-icon"
|
||||
data-identity-icon="${identity.image}"
|
||||
data-identity-color="${identity.color}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-name"></div>`;
|
||||
context.querySelector(".container-name").textContent = identity.name;
|
||||
manage.innerHTML = "<img src='/img/container-arrow.svg' class='show-tabs pop-button-image-small' />";
|
||||
|
||||
fragment.appendChild(tr);
|
||||
|
||||
tr.appendChild(context);
|
||||
|
||||
if (hasTabs) {
|
||||
tr.appendChild(manage);
|
||||
}
|
||||
|
||||
tr.addEventListener("click", e => {
|
||||
if (e.target.matches(".open-newtab") || e.target.parentNode.matches(".open-newtab")) {
|
||||
browser.runtime.sendMessage({
|
||||
method: "openTab",
|
||||
userContextId: identity.userContextId,
|
||||
source: "pop-up"
|
||||
}).then(() => {
|
||||
window.close();
|
||||
}).catch(() => {
|
||||
window.close();
|
||||
});
|
||||
} else if (hasTabs) {
|
||||
Logic.showPanel(P_CONTAINER_INFO, identity);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const list = document.querySelector(".identities-list");
|
||||
|
||||
list.innerHTML = "";
|
||||
list.appendChild(fragment);
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
// P_CONTAINER_INFO: More info about a container.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Logic.registerPanel(P_CONTAINER_INFO, {
|
||||
panelSelector: "#container-info-panel",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
document.querySelector("#close-container-info-panel").addEventListener("click", () => {
|
||||
Logic.showPreviousPanel();
|
||||
});
|
||||
|
||||
document.querySelector("#container-info-hideorshow").addEventListener("click", () => {
|
||||
const identity = Logic.currentIdentity();
|
||||
browser.runtime.sendMessage({
|
||||
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
||||
userContextId: identity.userContextId
|
||||
}).then(() => {
|
||||
window.close();
|
||||
}).catch(() => {
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
|
||||
// Check if the user has incompatible add-ons installed
|
||||
browser.runtime.sendMessage({
|
||||
method: "checkIncompatibleAddons"
|
||||
}).then(incompatible => {
|
||||
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 {
|
||||
moveTabsEl.addEventListener("click", () => {
|
||||
return browser.runtime.sendMessage({
|
||||
method: "moveTabsToWindow",
|
||||
userContextId: Logic.currentIdentity().userContextId,
|
||||
}).then(() => {
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
throw new Error("Could not check for incompatible add-ons.");
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
const identity = Logic.currentIdentity();
|
||||
|
||||
// Populating the panel: name and icon
|
||||
document.getElementById("container-info-name").textContent = identity.name;
|
||||
|
||||
const icon = document.getElementById("container-info-icon");
|
||||
icon.setAttribute("data-identity-icon", identity.image);
|
||||
icon.setAttribute("data-identity-color", identity.color);
|
||||
|
||||
// Show or not the has-tabs section.
|
||||
for (let trHasTabs of document.getElementsByClassName("container-info-has-tabs")) { // eslint-disable-line prefer-const
|
||||
trHasTabs.style.display = !identity.hasHiddenTabs && !identity.hasOpenTabs ? "none" : "";
|
||||
}
|
||||
|
||||
const hideShowIcon = document.getElementById("container-info-hideorshow-icon");
|
||||
hideShowIcon.src = identity.hasHiddenTabs ? CONTAINER_UNHIDE_SRC : CONTAINER_HIDE_SRC;
|
||||
|
||||
const hideShowLabel = document.getElementById("container-info-hideorshow-label");
|
||||
hideShowLabel.textContent = identity.hasHiddenTabs ? "Show this container" : "Hide this container";
|
||||
|
||||
// Let's remove all the previous tabs.
|
||||
const table = document.getElementById("container-info-table");
|
||||
while (table.firstChild) {
|
||||
table.firstChild.remove();
|
||||
}
|
||||
|
||||
// Let's retrieve the list of tabs.
|
||||
return browser.runtime.sendMessage({
|
||||
method: "getTabs",
|
||||
userContextId: identity.userContextId,
|
||||
}).then(this.buildInfoTable);
|
||||
},
|
||||
|
||||
buildInfoTable(tabs) {
|
||||
// For each one, let's create a new line.
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
||||
const tr = document.createElement("tr");
|
||||
fragment.appendChild(tr);
|
||||
tr.classList.add("container-info-tab-row");
|
||||
tr.innerHTML = escaped`
|
||||
<td><img class="icon" src="${tab.favicon}" /></td>
|
||||
<td class="container-info-tab-title">${tab.title}</td>`;
|
||||
|
||||
// On click, we activate this tab. But only if this tab is active.
|
||||
if (tab.active) {
|
||||
tr.classList.add("clickable");
|
||||
tr.addEventListener("click", () => {
|
||||
browser.runtime.sendMessage({
|
||||
method: "showTab",
|
||||
tabId: tab.id,
|
||||
}).then(() => {
|
||||
window.close();
|
||||
}).catch(() => {
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("container-info-table").appendChild(fragment);
|
||||
},
|
||||
});
|
||||
|
||||
// P_CONTAINERS_EDIT: Makes the list editable.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Logic.registerPanel(P_CONTAINERS_EDIT, {
|
||||
panelSelector: "#edit-containers-panel",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
document.querySelector("#exit-edit-mode-link").addEventListener("click", () => {
|
||||
Logic.showPanel(P_CONTAINERS_LIST);
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
const fragment = document.createDocumentFragment();
|
||||
Logic.identities().forEach(identity => {
|
||||
const tr = document.createElement("tr");
|
||||
fragment.appendChild(tr);
|
||||
tr.classList.add("container-panel-row");
|
||||
tr.innerHTML = escaped`
|
||||
<td class="userContext-wrapper">
|
||||
<div class="userContext-icon-wrapper">
|
||||
<div class="userContext-icon"
|
||||
data-identity-icon="${identity.image}"
|
||||
data-identity-color="${identity.color}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-name"></div>
|
||||
</td>
|
||||
<td class="edit-container pop-button edit-container-icon">
|
||||
<img
|
||||
src="/img/container-edit.svg"
|
||||
class="pop-button-image" />
|
||||
</td>
|
||||
<td class="remove-container pop-button delete-container-icon" >
|
||||
<img
|
||||
class="pop-button-image"
|
||||
src="/img/container-delete.svg"
|
||||
/>
|
||||
</td>`;
|
||||
tr.querySelector(".container-name").textContent = identity.name;
|
||||
tr.querySelector(".edit-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`);
|
||||
tr.querySelector(".remove-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`);
|
||||
|
||||
|
||||
tr.addEventListener("click", e => {
|
||||
if (e.target.matches(".edit-container-icon") || e.target.parentNode.matches(".edit-container-icon")) {
|
||||
Logic.showPanel(P_CONTAINER_EDIT, identity);
|
||||
} else if (e.target.matches(".delete-container-icon") || e.target.parentNode.matches(".delete-container-icon")) {
|
||||
Logic.showPanel(P_CONTAINER_DELETE, identity);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const list = document.querySelector("#edit-identities-list");
|
||||
|
||||
list.innerHTML = "";
|
||||
list.appendChild(fragment);
|
||||
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
});
|
||||
|
||||
// P_CONTAINER_EDIT: Editor for a container.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||
panelSelector: "#edit-container-panel",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
this.initializeRadioButtons();
|
||||
|
||||
document.querySelector("#edit-container-panel-back-arrow").addEventListener("click", () => {
|
||||
Logic.showPreviousPanel();
|
||||
});
|
||||
|
||||
document.querySelector("#edit-container-cancel-link").addEventListener("click", () => {
|
||||
Logic.showPreviousPanel();
|
||||
});
|
||||
|
||||
this._editForm = document.getElementById("edit-container-panel-form");
|
||||
const editLink = document.querySelector("#edit-container-ok-link");
|
||||
editLink.addEventListener("click", this._submitForm.bind(this));
|
||||
editLink.addEventListener("submit", this._submitForm.bind(this));
|
||||
this._editForm.addEventListener("submit", this._submitForm.bind(this));
|
||||
},
|
||||
|
||||
_submitForm() {
|
||||
const identity = Logic.currentIdentity();
|
||||
const formValues = new FormData(this._editForm);
|
||||
browser.runtime.sendMessage({
|
||||
method: identity.userContextId ? "updateIdentity" : "createIdentity",
|
||||
userContextId: identity.userContextId || 0,
|
||||
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
|
||||
icon: formValues.get("container-icon") || DEFAULT_ICON,
|
||||
color: formValues.get("container-color") || DEFAULT_COLOR,
|
||||
}).then(() => {
|
||||
return Logic.refreshIdentities();
|
||||
}).then(() => {
|
||||
Logic.showPreviousPanel();
|
||||
}).catch(() => {
|
||||
Logic.showPanel(P_CONTAINERS_LIST);
|
||||
});
|
||||
},
|
||||
|
||||
initializeRadioButtons() {
|
||||
const colorRadioTemplate = (containerColor) => {
|
||||
return 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 colorRadioFieldset = document.getElementById("edit-container-panel-choose-color");
|
||||
colors.forEach((containerColor) => {
|
||||
const templateInstance = document.createElement("span");
|
||||
// eslint-disable-next-line no-unescaped/enforce
|
||||
templateInstance.innerHTML = colorRadioTemplate(containerColor);
|
||||
colorRadioFieldset.appendChild(templateInstance);
|
||||
});
|
||||
|
||||
const iconRadioTemplate = (containerIcon) => {
|
||||
return escaped`<input type="radio" value="${containerIcon}" name="container-icon" id="edit-container-panel-choose-icon-${containerIcon}" />
|
||||
<label for="edit-container-panel-choose-icon-${containerIcon}" class="usercontext-icon choose-color-icon" data-identity-color="grey" data-identity-icon="${containerIcon}">`;
|
||||
};
|
||||
const icons = ["fingerprint", "briefcase", "dollar", "cart", "vacation", "gift", "food", "fruit", "pet", "tree", "chill", "circle"];
|
||||
const iconRadioFieldset = document.getElementById("edit-container-panel-choose-icon");
|
||||
icons.forEach((containerIcon) => {
|
||||
const templateInstance = document.createElement("span");
|
||||
// eslint-disable-next-line no-unescaped/enforce
|
||||
templateInstance.innerHTML = iconRadioTemplate(containerIcon);
|
||||
iconRadioFieldset.appendChild(templateInstance);
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
const identity = Logic.currentIdentity();
|
||||
document.querySelector("#edit-container-panel-name-input").value = identity.name || "";
|
||||
[...document.querySelectorAll("[name='container-color']")].forEach(colorInput => {
|
||||
colorInput.checked = colorInput.value === identity.color;
|
||||
});
|
||||
[...document.querySelectorAll("[name='container-icon']")].forEach(iconInput => {
|
||||
iconInput.checked = iconInput.value === identity.image;
|
||||
});
|
||||
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
// P_CONTAINER_DELETE: Delete a container.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Logic.registerPanel(P_CONTAINER_DELETE, {
|
||||
panelSelector: "#delete-container-panel",
|
||||
|
||||
// This method is called when the object is registered.
|
||||
initialize() {
|
||||
document.querySelector("#delete-container-cancel-link").addEventListener("click", () => {
|
||||
Logic.showPreviousPanel();
|
||||
});
|
||||
|
||||
document.querySelector("#delete-container-ok-link").addEventListener("click", () => {
|
||||
browser.runtime.sendMessage({
|
||||
method: "removeIdentity",
|
||||
userContextId: Logic.currentIdentity().userContextId,
|
||||
}).then(() => {
|
||||
return Logic.refreshIdentities();
|
||||
}).then(() => {
|
||||
Logic.showPreviousPanel();
|
||||
}).catch(() => {
|
||||
Logic.showPanel(P_CONTAINERS_LIST);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
const identity = Logic.currentIdentity();
|
||||
|
||||
// Populating the panel: name and icon
|
||||
document.getElementById("delete-container-name").textContent = identity.name;
|
||||
|
||||
const icon = document.getElementById("delete-container-icon");
|
||||
icon.setAttribute("data-identity-icon", identity.image);
|
||||
icon.setAttribute("data-identity-color", identity.color);
|
||||
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
});
|
||||
|
||||
Logic.init();
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Containers Experiment",
|
||||
"version": "2.2.0",
|
||||
|
||||
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
||||
"icons": {
|
||||
"48": "img/container-site-d-48.png",
|
||||
"96": "img/container-site-d-96.png"
|
||||
},
|
||||
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"strict_min_version": "51.0",
|
||||
"update_url": "https://testpilot.firefox.com/files/@testpilot-containers/updates.json"
|
||||
}
|
||||
},
|
||||
|
||||
"homepage_url": "https://testpilot.firefox.com/",
|
||||
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"activeTab",
|
||||
"cookies",
|
||||
"contextMenus",
|
||||
"history",
|
||||
"idle",
|
||||
"notifications",
|
||||
"storage",
|
||||
"tabs",
|
||||
"webRequestBlocking",
|
||||
"webRequest"
|
||||
],
|
||||
|
||||
"browser_action": {
|
||||
"browser_style": true,
|
||||
"default_icon": {
|
||||
"16": "img/container-site-d-24.png",
|
||||
"32": "img/container-site-d-48.png"
|
||||
},
|
||||
"default_title": "Containers",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Containers browserAction Popup</title>
|
||||
<link rel="stylesheet" href="/css/popup.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hide panel onboarding onboarding-panel-1" id="onboarding-panel-1">
|
||||
<img class="onboarding-img" alt="Container Tabs Overview" src="/img/onboarding-1.png" />
|
||||
<h3 class="onboarding-title">A better way to manage all the things you do online</h3>
|
||||
<p>
|
||||
Use containers to organize tasks, manage accounts, and keep your focus where you want it.
|
||||
</p>
|
||||
<a href="#" id="onboarding-start-button" class="onboarding-button">Get Started</a>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="panel onboarding onboarding-panel-2 hide" id="onboarding-panel-2">
|
||||
<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="#" id="onboarding-next-button" class="onboarding-button">Next</a>
|
||||
</div>
|
||||
|
||||
<div class="panel onboarding onboarding-panel-3 hide" id="onboarding-panel-3">
|
||||
<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="#" id="onboarding-done-button" class="onboarding-button">Done</a>
|
||||
</div>
|
||||
|
||||
<div class="panel container-panel hide" id="container-panel">
|
||||
<div class="panel-header">
|
||||
<h3 class="panel-header-text">Containers</h3>
|
||||
<a class="pop-button" id="sort-containers-link"><img class="pop-button-image" alt="Sort Containers" title="Sort Containers" src="/img/container-sort.svg"></a>
|
||||
</div>
|
||||
<div class="scrollable panel-content">
|
||||
<table>
|
||||
<tbody class="identities-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer edit-identities">
|
||||
<div class="edit-containers-text panel-footer-secondary">
|
||||
<a id="edit-containers-link">Edit Containers</a>
|
||||
</div>
|
||||
<a class="add-container-link pop-button" id="container-add-link">
|
||||
<img class="pop-button-image-small icon" alt="Create new container icon" title="Create new container" src="/img/container-add.svg" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="hide panel container-info-panel" id="container-info-panel">
|
||||
<div class="columns">
|
||||
<div class="panel-back-arrow" id="close-container-info-panel">
|
||||
<img alt="Panel Back Arrow" src="/img/container-arrow.svg" class="back-arrow-img" />
|
||||
</div>
|
||||
<div class="column-panel-content">
|
||||
<div class="panel-header container-info-panel-header">
|
||||
<span class="usercontext-icon" id="container-info-icon"></span>
|
||||
<h3 id="container-info-name" class="panel-header-text container-name"></h3>
|
||||
</div>
|
||||
<div class="select-row clickable container-info-panel-hide container-info-has-tabs" id="container-info-hideorshow">
|
||||
<img id="container-info-hideorshow-icon" alt="Hide Container icon" src="/img/container-hide.svg" class="icon container-info-panel-hideorshow-icon"/>
|
||||
<span id="container-info-hideorshow-label">Hide this container</span>
|
||||
</div>
|
||||
<div class="select-row clickable container-info-panel-movetabs container-info-has-tabs" id="container-info-movetabs">Move tabs to a new window</div>
|
||||
<div class="scrollable">
|
||||
<table id="container-info-table" class="container-info-list">
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="panel edit-containers-panel hide" id="edit-containers-panel">
|
||||
<div class="panel-header">
|
||||
<h3 class="panel-header-text">Edit Containers</h3>
|
||||
</div>
|
||||
<div class="scrollable panel-content">
|
||||
<table class="unstriped">
|
||||
<tbody id="edit-identities-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer edit-containers-panel-footer">
|
||||
<a id="exit-edit-mode-link" class="exit-edit-mode-link edit-containers-exit-text">Exit Edit Mode</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="panel edit-container-panel hide" id="edit-container-panel">
|
||||
<div class="columns">
|
||||
<div class="panel-back-arrow" id="edit-container-panel-back-arrow">
|
||||
<img alt="Panel Back Arrow" src="/img/container-arrow.svg" class="back-arrow-img" />
|
||||
</div>
|
||||
<div class="column-panel-content">
|
||||
<form id="edit-container-panel-form">
|
||||
<fieldset>
|
||||
<legend>Name</legend>
|
||||
<input type="text" name="container-name" id="edit-container-panel-name-input" maxlength="25"/>
|
||||
</fieldset>
|
||||
<fieldset id="edit-container-panel-choose-color">
|
||||
<legend>Choose a color</legend>
|
||||
</fieldset>
|
||||
<fieldset id="edit-container-panel-choose-icon">
|
||||
<legend>Choose an icon</legend>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div class="panel-footer">
|
||||
<a class="button secondary expanded footer-button cancel-button" id="edit-container-cancel-link">Cancel</a>
|
||||
<a class="button primary expanded footer-button" id="edit-container-ok-link">OK</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide panel delete-container-panel" id="delete-container-panel">
|
||||
<div class="panel-header">
|
||||
<span class="usercontext-icon" id="delete-container-icon"></span>
|
||||
<h3 id="delete-container-name" class="panel-header-text container-name"></h3>
|
||||
</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>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<a class="button expanded secondary footer-button cancel-button" id="delete-container-cancel-link">Cancel</a>
|
||||
<a class="button expanded primary footer-button" id="delete-container-ok-link">OK</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="js/popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||