Merge branch 'master' into proxy-support

This commit is contained in:
luke crouch 2020-11-11 17:14:07 -06:00
commit 63996ce416
72 changed files with 4236 additions and 1819 deletions

View file

@ -1 +1,2 @@
lib/testpilot/*.js
coverage

View file

@ -47,7 +47,7 @@ module.exports = {
"error",
{
"escape": {
"taggedTemplates": ["escaped"]
"taggedTemplates": ["Utils.escaped"]
}
}
],

4
.gitignore vendored
View file

@ -12,3 +12,7 @@ src/web-ext-artifacts/*
# JetBrains IDE files
.idea
# IstanbulJS
.nyc_output
coverage

View file

@ -9,7 +9,12 @@
"rules": {
"declaration-block-no-duplicate-properties": true,
"order/declaration-block-properties-alphabetical-order": true,
"order/properties-alphabetical-order": true,
"property-no-unknown": [
true, {
ignoreProperties:
["inset-block-end", "inset-block-start"]
}],
"property-blacklist": [
"/(min[-]|max[-])height/",
"/width/",

View file

@ -19,10 +19,18 @@ For more info, see:
## Development
1. `npm install`
2. `./node_modules/.bin/web-ext run -s src/`
2. `./node_modules/web-ext/bin/web-ext run -s src/`
### Testing
TBD
`npm run test`
or
`npm run lint`
for just the linter
There is a timeout test that sometimes fails on certain machines, so make sure to run the tests on your clone before you make any changes to see if you have this problem.
### Distributing
#### Make the new version
@ -51,6 +59,6 @@ Finally, we also publish the release to GitHub for those followers.
Facebook & Twitter icons CC-Attrib https://fairheadcreative.com.
- [Licence](./LICENSE.txt)
- [License](./LICENSE.txt)
- [Contributing](./CONTRIBUTING.md)
- [Code Of Conduct](./CODE_OF_CONDUCT.md)

View file

@ -2,30 +2,31 @@
"name": "testpilot-containers",
"title": "Multi-Account Containers",
"description": "Containers helps you keep all the parts of your online life contained in different tabs. Custom labels and color-coded tabs help keep different activities — like online shopping, travel planning, or checking work email — separate.",
"version": "6.1.1",
"version": "7.1.0",
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
"bugs": {
"url": "https://github.com/mozilla/multi-account-containers/issues"
},
"dependencies": {},
"devDependencies": {
"ajv": "^6.6.2",
"addons-linter": "^1.3.2",
"chai": "^4.1.2",
"eslint": "^3.17.1",
"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.7",
"jsdom": "^11.6.2",
"json": "^9.0.6",
"mocha": "^5.0.0",
"mocha": "^6.2.2",
"npm-run-all": "^4.0.0",
"sinon": "^4.4.0",
"sinon-chai": "^2.14.0",
"stylelint": "^7.9.0",
"stylelint-config-standard": "^16.0.0",
"stylelint-order": "^0.3.0",
"web-ext": "^2.2.2"
"nyc": "^15.0.0",
"sinon": "^7.5.0",
"sinon-chai": "^3.3.0",
"stylelint-order": "^4.0.0",
"stylelint": "^13.5.0",
"stylelint-config-standard": "^20.0.0",
"web-ext": "^2.9.3",
"webextensions-jsdom": "^1.2.1"
},
"homepage": "https://github.com/mozilla/multi-account-containers#readme",
"license": "MPL-2.0",
@ -36,13 +37,16 @@
},
"scripts": {
"build": "npm test && cd src && web-ext build --overwrite-dest",
"webext": "web-ext run -s src/",
"lint": "npm-run-all lint:*",
"lint:addon": "addons-linter src --self-hosted",
"lint:css": "stylelint src/css/*.css",
"lint:html": "htmllint *.html",
"lint:js": "eslint .",
"package": "rm -rf src/web-ext-artifacts && npm run build && mv src/web-ext-artifacts/firefox_multi-account_containers-*.zip addon.xpi",
"test": "npm run lint && mocha ./test/setup.js test/**/*.test.js",
"test-watch": "mocha ./test/setup.js test/**/*.test.js --watch"
"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"
}
}

View file

@ -60,6 +60,7 @@ html {
@media (prefers-color-scheme: dark) {
#redirect-url {
background: #38383d; /* Grey 70 */
color: #eee; /* White 20 */
}
}
/* stylelint-enable */

29
src/css/options.css Normal file
View file

@ -0,0 +1,29 @@
body {
background: #fff;
color: #202023;
}
h3 {
margin-block-start: 2.5rem;
}
h3:first-of-type {
margin-block-start: 1rem;
}
p,
label {
color: rgb(74, 74, 79);
}
@media (prefers-color-scheme: dark) {
body {
background: #202023;
color: #fff;
}
p,
label {
color: rgb(177, 177, 179);
}
}

View file

@ -18,9 +18,18 @@ html {
}
body {
color: #000;
font-family: Roboto, Noto, "San Francisco", Ubuntu, "Segoe UI", "Fira Sans", message-box, Arial, sans-serif;
font-size: 13px;
inline-size: calc(var(--overflow-size) + 299px);
/* inline-size: 320px; */
letter-spacing: -0.1px;
max-inline-size: calc(var(--overflow-size) + 299px);
--highlight-blue: #1296f8;
--hr-grey: #e3e3e3;
--text-grey: #737373;
}
html,
@ -46,7 +55,6 @@ body {
--inline-start-size: 1.66rem; /* 20px */
--inline-button-size: 5.833rem; /* 70px */
--icon-size: 1.166rem; /* 14px */
--small-text-size: 0.833rem; /* 10px */
--small-radius: 3px;
--icon-button-size: calc(calc(var(--block-line-separation-size) * 2) + 1.66rem); /* 20px */
@ -91,7 +99,7 @@ table {
}
.scrollable {
border-block-start: 1px solid #f1f1f1;
flex: 1;
inline-size: 100%;
max-block-size: 400px;
overflow: auto;
@ -107,11 +115,28 @@ table {
/* Effect borrowed from tabs in Firefox, ensure that the element flexes to the full width */
.truncate-text {
mask-image: linear-gradient(to left, transparent, black 1em);
inline-size: 100%;
overflow: hidden;
position: relative;
white-space: nowrap;
}
.truncate-text::after {
background: white;
content: " ";
height: 100%;
inline-size: 50px;
inset-inline-end: 0;
mask-image: linear-gradient(to right, transparent, white 70%);
position: absolute;
}
.hover-highlight:hover .truncate-text::after,
.hover-highlight:focus .truncate-text::after {
background: var(--highlight-blue);
mask-image: linear-gradient(to right, transparent, var(--highlight-blue) 50%);
}
/* Color and icon helpers */
[data-identity-color="blue"] {
--identity-tab-color: #37adff;
@ -206,6 +231,10 @@ table {
--identity-icon: url("/img/usercontext.svg#chill");
}
[data-identity-icon="fence"] {
--identity-icon: url("/img/usercontext.svg#fence");
}
#current-tab [data-identity-icon="default-tab"] {
background: center center no-repeat url("/img/blank-tab.svg");
fill: currentColor;
@ -231,18 +260,6 @@ table {
background-color: rgba(0, 0, 0, 0.05);
}
/* Text links with actions */
.action-link:link {
color: var(--primary-action-color);
text-decoration: none;
}
.action-link:active,
.action-link:hover {
text-decoration: underline;
}
/* Panels keep everything together */
.panel {
display: flex;
@ -262,29 +279,11 @@ table {
min-block-size: 360px;
}
.panel .columns {
display: flex;
flex: 1;
}
.panel-content {
flex: 1;
padding-block-start: 16px;
}
/* Column panels for edit screens */
.column-panel-content {
display: flex;
flex-direction: column;
inline-size: var(--column-panel-inline-size);
}
.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: 100%;
@ -293,28 +292,6 @@ table {
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,
.panel-back-arrow:focus {
background: #dedede;
}
.back-arrow-img {
block-size: 16px;
inline-size: 16px;
transform: rotate(180deg);
}
/* Onboarding styles */
.onboarding * {
text-align: center;
@ -356,11 +333,45 @@ table {
transition: background-color 75ms;
}
.half-button-wrapper {
align-items: center;
display: flex;
flex-direction: row;
height: 44px;
inline-size: 100%;
}
.half-onboarding-button {
align-items: center;
background-color: #0996f8;
border-radius: 3px;
color: white;
display: flex;
flex: 1 0 auto;
font-size: 14px;
height: 44px;
inline-size: 50%;
justify-content: center;
margin-inline-end: 4px;
text-decoration: none;
transition: background-color 75ms;
}
.grey-button {
background-color: #e3e3e3;
color: #000;
}
.onboarding-button:hover,
.onboarding-button:active {
background-color: #0675d3;
}
.onboarding-button:focus,
.half-onboarding-button:focus {
box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3);
}
/* Pop buttons are the square shaped buttons used to
manage things like container crud */
.pop-button {
@ -372,226 +383,10 @@ manage things like container crud */
justify-content: center;
}
.pop-button:hover,
.pop-button:focus,
.panel-footer-secondary:focus,
.panel-footer-secondary:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.pop-button:focus,
.panel-footer-secondary:focus {
background-color: rgba(0, 0, 0, 0.08);
}
.pop-button a,
.panel-footer a,
.panel-footer-secondary a {
.panel-footer a {
text-decoration: none;
}
.pop-button-image {
block-size: 20px;
flex: 0 0 20px;
margin-block-end: auto;
margin-block-start: auto;
margin-inline-end: auto;
margin-inline-start: auto;
}
.pop-button-image-small {
block-size: 12px;
flex: 0 0 12px;
}
/* Panel Header */
.panel-header {
align-items: center;
block-size: 48px;
display: flex;
justify-content: space-between;
}
.panel-header .usercontext-icon {
inline-size: var(--icon-button-size);
}
.column-panel-content .panel-header {
flex: 0 0 48px;
inline-size: 100%;
}
.panel-header-text {
color: var(--text-normal-color);
flex: 1;
font-size: var(--font-size-heading);
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;
}
#container-panel .panel-header {
background-color: #efefef;
block-size: 26px;
font-size: 14px;
}
#container-panel .panel-header-text {
color: #727272;
font-size: 14px;
padding-block-end: 0;
padding-block-start: 0;
text-transform: uppercase;
}
.container-panel-controls {
display: flex;
justify-content: flex-end;
margin-block-end: var(--block-line-space-size);
margin-block-start: var(--block-line-space-size);
margin-inline-end: var(--inline-item-space-size);
margin-inline-start: var(--inline-item-space-size);
}
#container-panel #sort-containers-link {
align-items: center;
block-size: var(--block-url-label-size);
border: 1px solid #d8d8d8;
border-radius: var(--small-radius);
color: var(--title-text-color);
display: flex;
font-size: var(--small-text-size);
inline-size: var(--inline-button-size);
justify-content: center;
text-decoration: none;
}
#container-panel #sort-containers-link:hover,
#container-panel #sort-containers-link:focus {
background: #f2f2f2;
}
span ~ .panel-header-text {
padding-block-end: 0;
padding-block-start: 0;
padding-inline-end: 0;
padding-inline-start: 0;
}
#current-tab {
align-items: center;
color: var(--text-normal-color);
display: grid;
font-size: var(--small-text-size);
grid-column-gap: var(--inline-item-space-size);
grid-row-gap: var(--block-line-space-size);
grid-template-columns: var(--icon-size) var(--icon-size) 1fr;
margin-block-end: var(--block-line-space-size);
margin-block-start: var(--block-line-separation-size);
margin-inline-end: var(--inline-start-size);
margin-inline-start: var(--inline-start-size);
max-inline-size: 100%;
}
#current-tab img {
max-block-size: var(--icon-size);
}
#current-tab > h3 {
color: var(--text-heading-color);
font-weight: normal;
grid-column: span 3;
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
}
#current-page {
display: contents;
}
#current-tab .page-title {
font-size: var(--font-size-heading);
grid-column: 2 / 4;
}
#current-tab > label {
display: contents;
font-size: var(--small-text-size);
}
#current-tab > label > input {
-moz-appearance: none;
block-size: var(--icon-size);
border: 1px solid #d8d8d8;
border-radius: var(--small-radius);
display: block;
grid-column-start: 2;
inline-size: var(--icon-size);
margin-block-end: 0;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
}
#current-tab > label > input[disabled] {
background-color: #efefef;
}
#current-tab > label > input:checked {
background-image: url("/img/check.svg");
background-position: -1px -1px;
background-size: var(--icon-size);
}
#current-container {
color: var(--identity-tab-color);
flex: 1;
}
#current-tab > label > .usercontext-icon {
background-size: 16px;
block-size: 16px;
display: block;
flex: 0 0 20px;
inline-size: 20px;
margin-inline-end: 3px;
margin-inline-start: 3px;
}
/* Rows used when iterating over panels */
.container-panel-row {
align-items: center;
background-color: #fefefe !important;
border-block-end: 1px solid #f1f1f1;
box-sizing: border-box;
display: flex;
justify-content: space-between;
}
.container-panel-row .container-name {
flex: 1;
max-inline-size: 160px;
padding-inline-end: 4px;
padding-inline-start: 4px;
}
.edit-containers-panel .userContext-wrapper {
max-inline-size: calc(var(--overflow-size) + 203px);
}
.disable-edit-containers {
opacity: var(--inactive-opacity);
pointer-events: none;
}
.userContext-wrapper {
align-items: center;
display: flex;
@ -599,9 +394,13 @@ span ~ .panel-header-text {
transition: background-color 75ms;
}
.container-panel-row:hover .clickable.userContext-wrapper,
.container-panel-row:focus .clickable.userContext-wrapper {
background: #f2f2f2;
.edit-containers-panel .userContext-wrapper {
max-inline-size: calc(var(--overflow-size) + 203px);
}
.disable-edit-containers {
opacity: var(--inactive-opacity);
pointer-events: none;
}
.userContext-icon-wrapper {
@ -615,12 +414,20 @@ span ~ .panel-header-text {
background-image: var(--identity-icon);
background-position: center center;
background-repeat: no-repeat;
background-size: 20px 20px;
background-size: 16px;
block-size: 100%;
fill: var(--identity-icon-color);
filter: url('/img/filters.svg#fill');
}
.mac-icon {
background-image: url('/img/multiaccountcontainer-16.svg');
background-position: center center;
background-repeat: no-repeat;
background-size: 16px;
block-size: 100%;
}
.container-panel-row:hover .clickable .usercontext-icon,
.container-panel-row:focus .clickable .usercontext-icon,
.container-panel-row .clickable:focus .usercontext-icon {
@ -647,62 +454,6 @@ span ~ .panel-header-text {
justify-content: space-between;
}
.edit-containers-text {
align-items: center;
block-size: 100%;
border-inline-end: solid 1px #d8d8d8;
display: flex;
flex: 1;
justify-content: center;
}
.edit-containers-text a {
align-items: center;
block-size: 100%;
color: #0a0a0a;
display: flex;
flex: 1;
justify-content: center;
}
/* Container info list */
.container-info-tab-title {
display: flex;
}
.container-info-tab-row:hover .container-info-tab-title .truncate-text {
inline-size: calc(var(--column-panel-inline-size) - 58px);
}
#container-info-hideorshow {
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-close-tab {
transform: scale(0.7);
visibility: collapse;
}
.container-info-tab-row:hover .container-close-tab {
opacity: 0.5;
visibility: visible;
}
.container-info-tab-row .container-close-tab:hover {
opacity: 1;
visibility: visible;
}
.container-info-has-tabs,
.container-info-tab-row {
align-items: center;
@ -729,48 +480,6 @@ span ~ .panel-header-text {
margin-inline-end: 0;
}
.container-info-list {
display: flex;
flex-direction: column;
margin-block-start: 4px;
padding-block-start: 4px;
}
.container-info-list tbody {
display: contents;
}
.clickable {
cursor: pointer;
}
.clickable:hover,
.clickable:focus {
background-color: #ebebeb;
}
.edit-containers-exit-text {
align-items: center;
background: var(--primary-action-color);
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;
@ -781,23 +490,6 @@ span ~ .panel-header-text {
font-size: var(--font-size-heading);
}
/* Form info */
.column-panel-content form {
flex: 1;
padding-block-end: 16px;
padding-block-start: 16px;
padding-inline-end: 16px;
padding-inline-start: 16px;
}
.edit-container-panel .columns {
overflow: hidden; /* Bugfix: issue 948 */
}
#edit-sites-assigned {
flex: 1000; /* Bugfix: issue 948 */
}
#edit-sites-assigned h3 {
font-size: 14px;
font-weight: normal;
@ -817,21 +509,13 @@ span ~ .panel-header-text {
margin-inline-end: 10px;
}
.assigned-sites-list > div > .delete-assignment {
display: none;
}
.assigned-sites-list > div:hover > .delete-assignment {
display: block;
}
.assigned-sites-list > div > .hostname {
flex: 1;
}
.radio-choice > .radio-container {
align-items: center;
block-size: 29px;
block-size: 25px;
display: flex;
flex: 0 0 calc(100% / var(--icon-fit));
}
@ -891,7 +575,7 @@ span ~ .panel-header-text {
display: flex;
flex-direction: row;
flex-wrap: wrap;
inline-size: 100%;
inline-size: 80%;
margin-block-end: 10px;
margin-inline-end: 0;
margin-inline-start: 0;
@ -916,10 +600,17 @@ span ~ .panel-header-text {
padding-inline-start: 5px;
}
.edit-container-panel legend {
.edit-container-panel legend,
.options-header {
flex: 1 0;
font-size: 14px !important;
padding-block-end: 6px;
margin-block-end: 4px;
margin-block-start: -6px;
}
.options-header {
margin-block-end: 8px;
margin-block-start: 6px;
}
/* Achievement panel elements */
@ -971,3 +662,272 @@ span ~ .panel-header-text {
.amo-rate-cta {
background: #0f1126;
}
h3.title {
block-size: 40px;
color: #000;
font-size: 13px;
font-weight: bold;
inline-size: 100%;
letter-spacing: -0.1px;
line-height: 40px;
text-align: center;
}
.menu {
border-style: none;
inline-size: 100%;
}
.menu-item {
cursor: pointer;
height: 24px;
inline-size: 100%;
line-height: 24px;
}
.menu-item td {
display: flex;
max-inline-size: 300px;
}
.disabled-menu-item {
color: grey;
cursor: default;
font-style: italic;
}
.hover-highlight:hover,
.hover-highlight:focus {
background: var(--highlight-blue);
color: #fff;
}
.menu-item-name {
display: flex;
inline-size: calc(100% - 40px);
max-inline-size: 260px;
}
.menu-text {
line-height: 24px;
}
.menu-icon {
display: block;
height: 16px;
inline-size: 23px;
margin-block-end: 4px;
margin-block-start: 4px;
margin-inline-end: 8px;
margin-inline-start: 16px;
text-align: center;
}
/* Maintain 1:1 square ratio for Favicons of websites added to a specific container */
#edit-sites-assigned .menu-icon,
#container-info-table .menu-icon {
inline-size: 16px;
}
.menu-right-float {
height: 24px;
inline-size: 60px;
text-align: right;
}
.container-count {
opacity: 0.6;
padding-block-end: 0;
padding-block-start: 0;
padding-inline-end: 6px;
padding-inline-start: 0;
text-align: right;
}
.menu-arrow {
display: inline-block;
float: right;
height: 24px;
inline-size: 18px;
padding-block-end: 6px;
padding-block-start: 6px;
padding-inline-end: 12px;
padding-inline-start: 0;
text-align: center;
}
.menu-arrow img {
height: 12px;
inline-size: 12px;
padding-block-end: 2px;
padding-block-start: 2px;
padding-inline-end: 2px;
padding-inline-start: 2px;
}
hr {
border: 0;
border-block-start: 1px solid var(--hr-grey);
display: block;
margin-block-end: 0;
margin-block-start: 6px;
margin-inline-end: 0;
margin-inline-start: 0;
padding-block-end: 6px;
padding-block-start: 0;
padding-inline-end: 0;
padding-inline-start: 0;
}
.sub-header {
color: var(--text-grey);
height: 24px;
line-height: 24px;
padding-block-end: 0;
padding-block-start: 0;
padding-inline-end: 16px;
padding-inline-start: 16px;
}
.edit-form {
color: var(--text-grey);
flex: 1;
padding-block-end: 0;
padding-block-start: 0;
padding-inline-end: 16px;
padding-inline-start: 16px;
}
.identities-list {
margin-block-end: 41px;
margin-block-start: 0;
margin-inline-end: 0;
margin-inline-start: 0;
}
.bottom-btn {
background-color: var(--hr-grey);
border: solid 1px #d9d9d9;
cursor: pointer;
height: 41px;
inline-size: 100%;
inset-block-end: 0;
line-height: 41px;
padding-block-end: 0;
padding-block-start: 0;
padding-inline-end: 16px;
padding-inline-start: 16px;
position: fixed;
}
.delete-container {
background-color: #fff;
border-block-start: solid 1px var(--hr-grey);
cursor: default;
display: flex;
height: 65px;
inline-size: 100%;
justify-content: space-between;
padding-block-end: 27px;
padding-block-start: 9px;
padding-inline-end: 18px;
padding-inline-start: 17px;
}
.delete-btn {
background-color: rgba(12, 12, 13, 0.1);
border: 0;
border-radius: 2px;
cursor: pointer;
height: 30px;
inline-size: 100%;
line-height: 30px;
text-align: center;
}
.btn-return.arrow-left {
background-color: rgba(255, 255, 255, 1);
background-image: url("/img/arrow-icon-left.svg");
border: 0;
cursor: pointer;
height: 1.2rem;
inline-size: 1.2rem;
inset-block-start: 15px;
left: 15px;
position: absolute;
}
input {
border: solid 1px #bebebe;
border-radius: 2px;
}
.form-header {
height: 23px;
line-height: 23px;
padding-block-end: 0;
padding-block-start: 0;
padding-inline-end: 0;
padding-inline-start: 0;
}
.edit-container-panel-name-input {
height: 29px;
}
.container-options {
height: 23px;
}
.site-isolation {
inset-block-end: auto;
position: fixed;
}
.options-label {
cursor: pointer;
padding-inline-start: 25px;
}
.manage-assigned-sites-list {
color: var(--highlight-blue);
}
.info-icon {
cursor: pointer;
height: 16px;
inline-size: 16px;
inset-block-start: 13px;
position: absolute;
right: 13px;
text-align: center;
text-decoration: none;
}
.delete-warning {
padding-block-end: 8px;
padding-block-start: 8px;
padding-inline-end: 0;
padding-inline-start: 0;
}
.trash-button {
display: inline-block;
float: right;
height: 16px;
inline-size: 16px;
margin-block-end: 4px;
margin-block-start: 4px;
margin-inline-end: 10px;
margin-inline-start: 0;
text-align: center;
}
tr > td > .trash-button {
display: none;
}
tr:hover > td > .trash-button {
display: block;
}

3
src/img/Account.svg Normal file
View file

@ -0,0 +1,3 @@
<!-- 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 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: 887 B

3
src/img/Sync.svg Normal file
View file

@ -0,0 +1,3 @@
<!-- 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 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.5 KiB

View file

@ -1 +1,3 @@
<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>
<!-- 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 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>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,3 @@
<!-- 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="context-fill" d="M6.414 8l4.293-4.293a1 1 0 0 0-1.414-1.414l-5 5a1 1 0 0 0 0 1.414l5 5a1 1 0 0 0 1.414-1.414z"></path></svg>

After

Width:  |  Height:  |  Size: 431 B

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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 width="5px" height="8px" viewBox="0 0 5 8" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
<title>Arrow</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M5.00090934,9.5006316 C4.79840046,9.50099392 4.61567086,9.37916873 4.53812503,9.19209489 C4.4605792,9.00502105 4.5035325,8.78964579 4.64690934,8.6466316 L7.29490934,6.0006316 L4.64690934,3.3546316 C4.45140054,3.1591228 4.45140054,2.8421404 4.64690934,2.6466316 C4.84241814,2.4511228 5.15940054,2.4511228 5.35490934,2.6466316 L8.35490934,5.6466316 C8.44895104,5.74043586 8.50180313,5.86780434 8.50180313,6.0006316 C8.50180313,6.13345886 8.44895104,6.26082734 8.35490934,6.3546316 L5.35490934,9.3546316 C5.26095861,9.44834555 5.13360821,9.5008686 5.00090934,9.5006316 Z" id="path-1"></path>
</defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.6">
<g id="Icons-/-12-/-Arrowhead-Right-12---Thin" transform="translate(-4.000000, -2.000000)">
<rect id="bouding-box" x="0" y="0" width="12" height="12"></rect>
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Shape" fill-rule="nonzero"></g>
<g id="Color-/-Photon-/-Primary---Grey-90-80%" mask="url(#mask-2)" fill="#0C0C0D" fill-opacity="0.8" fill-rule="evenodd">
<rect id="Rectangle" x="0" y="0" width="12" height="12"></rect>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,3 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<!-- 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="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>

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 518 B

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 11 11" style="enable-background:new 0 0 11 11;" xml:space="preserve">
<style type="text/css">
.st0{fill:#858585;}
</style>
<title>firefox</title>
<g id="General-icons">
<polygon class="st0" points="10.8,4.4 6.4,4.4 6.4,0.2 4.6,0.2 4.6,4.4 0.2,4.4 0.2,6.4 4.6,6.4 4.6,10.8 6.4,10.8 6.4,6.4
10.8,6.4 "/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 595 B

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 6 8" style="enable-background:new 0 0 6 8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#4C4C4C;}
</style>
<polygon id="Arrow---Disclosure---Collapsed-Copy" class="st0" points="5.5,4 1.5,7.7 0.5,6.8 3.5,4 0.5,1.2 1.5,0.3 "/>
</svg>

Before

Width:  |  Height:  |  Size: 520 B

View file

@ -1,3 +1,5 @@
<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"/>
<!-- 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" viewBox="0 0 7 7">
<polygon fill="#FFFFFF" 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>

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 394 B

View file

@ -1,10 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 12 12" style="enable-background:new 0 0 12 12;" xml:space="preserve">
<style type="text/css">
.st0{fill:#858585;}
</style>
<path class="st0" d="M4.6,0.3h2.7c0.1,0,0.2,0.1,0.2,0.2v1H4.4v-1C4.4,0.4,4.5,0.3,4.6,0.3z M1.7,1.5h8.6c0.1,0,0.2,0.1,0.2,0.2
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 12 12" style="enable-background:new 0 0 12 12;" xml:space="preserve">
<path fill="#FFFFFF" d="M4.6,0.3h2.7c0.1,0,0.2,0.1,0.2,0.2v1H4.4v-1C4.4,0.4,4.5,0.3,4.6,0.3z M1.7,1.5h8.6c0.1,0,0.2,0.1,0.2,0.2
l0.2,1.4H1.3l0.2-1.4C1.5,1.6,1.6,1.5,1.7,1.5z M6,11.7H3.2L2.1,3.9H6h3.9l-1.1,7.8H6L6,11.7z"/>
</svg>

Before

Width:  |  Height:  |  Size: 626 B

After

Width:  |  Height:  |  Size: 779 B

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 12 12" style="enable-background:new 0 0 12 12;" xml:space="preserve">
<style type="text/css">
.st0{fill:#858585;}
</style>
<g>
<path class="st0" d="M11.4,2.6L9.6,0.9c-0.1-0.1-0.2-0.1-0.3,0l-7,7l2,2l7-7C11.4,2.8,11.4,2.7,11.4,2.6z"/>
<path class="st0" d="M0.8,10.9c-0.1,0.3,0,0.4,0.4,0.4l2.3-0.6l-2-2L0.8,10.9z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 603 B

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 12 12" style="enable-background:new 0 0 12 12;" xml:space="preserve">
<style type="text/css">
.st0{fill:#858585;}
</style>
<polygon class="st0" points="10.3,0.5 8.9,2 2,8.9 0.5,10.3 1.3,11.1 11.1,1.3 "/>
<g>
<path class="st0" d="M4.8,7.5l2.8-2.8C7.2,4.3,6.7,4,6.1,4c-1.1,0-2,0.9-2,2C4.1,6.6,4.4,7.1,4.8,7.5z"/>
<path class="st0" d="M5.9,7.9c0,0,0.1,0,0.2,0c1.1,0,2-0.9,2-2c0,0,0-0.1,0-0.2L5.9,7.9z"/>
</g>
<g>
<path class="st0" d="M4.1,8.2C2.6,7.4,1.3,6.1,1.2,6C1.3,5.8,3.8,3.1,6,3.1c0.8,0,1.6,0.4,2.4,0.8L9,3.3C8.1,2.7,7,2.2,6,2.2
c-2.6,0-5.4,2.9-5.5,3.1C0.3,5.6,0.2,5.7,0.2,6v0c0,0.2,0.1,0.4,0.2,0.6c0.1,0.1,1.3,1.4,2.9,2.3L4.1,8.2z"/>
<path class="st0" d="M9.9,3.9L9.2,4.5c0.9,0.7,1.5,1.3,1.6,1.4C10.6,6.2,8.1,8.8,6,8.8c-0.3,0-0.6,0-0.9-0.1L4.4,9.4
C4.9,9.6,5.5,9.7,6,9.7c2.6,0,5.4-2.9,5.5-3.1c0.2-0.2,0.2-0.4,0.2-0.6v0c0-0.2,0-0.4-0.2-0.6C11.4,5.3,10.8,4.6,9.9,3.9z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">

Before

Width:  |  Height:  |  Size: 883 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,9 @@
<!-- 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 data-name="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<rect x="1" y="1" width="6" height="6" rx="1"/>
<rect x="1" y="9" width="6" height="6" rx="1"/>
<rect x="9" y="9" width="6" height="6" rx="1"/>
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.92 1.62a1 1 0 0 0-0.54-0.54A1 1 0 0 0 14 1h-4a1 1 0 0 0 0 2h1.59l-2.3 2.29a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0L13 4.41V6a1 1 0 0 0 2 0V2a1 1 0 0 0-0.08-0.38z"/>
</svg>

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,3 +0,0 @@
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path fill="context-fill #bebebe" fill-opacity="context-fill-opacity" d="M12.9137931,3.0862069 L12.9137931,1.27586207 C12.9137931,0.84736528 12.5664278,0.5 12.137931,0.5 C11.7094342,0.5 11.362069,0.84736528 11.362069,1.27586207 L11.362069,1.27586207 L11.362069,3.0862069 L9.55172414,3.0862069 C9.12322735,3.0862069 8.77586207,3.43357218 8.77586207,3.86206897 C8.77586207,4.29056575 9.12322735,4.63793103 9.55172414,4.63793103 L11.362069,4.63793103 L11.362069,6.44827586 C11.362069,6.87677265 11.7094342,7.22413793 12.137931,7.22413793 L12.137931,7.22413793 C12.5664278,7.22413793 12.9137931,6.87677265 12.9137931,6.44827586 L12.9137931,6.44827586 L12.9137931,4.63793103 L14.7241379,4.63793103 C15.1526347,4.63793103 15.5,4.29056575 15.5,3.86206897 L15.5,3.86206897 C15.5,3.43357218 15.1526347,3.0862069 14.7241379,3.0862069 L14.7241379,3.0862069 L12.9137931,3.0862069 Z M0.5,9.76803178 C0.5,9.22007158 0.94118947,8.77586207 1.49216971,8.77586207 L6.23196822,8.77586207 C6.77992842,8.77586207 7.22413793,9.21705154 7.22413793,9.76803178 L7.22413793,14.5078303 C7.22413793,15.0557905 6.78294846,15.5 6.23196822,15.5 L1.49216971,15.5 C0.94420951,15.5 0.5,15.0588105 0.5,14.5078303 L0.5,9.76803178 Z M8.77586207,9.76803178 C8.77586207,9.22007158 9.21705154,8.77586207 9.76803178,8.77586207 L14.5078303,8.77586207 C15.0557905,8.77586207 15.5,9.21705154 15.5,9.76803178 L15.5,14.5078303 C15.5,15.0557905 15.0588105,15.5 14.5078303,15.5 L9.76803178,15.5 C9.22007158,15.5 8.77586207,15.0588105 8.77586207,14.5078303 L8.77586207,9.76803178 Z M0.5,1.49216971 C0.5,0.94420951 0.94118947,0.5 1.49216971,0.5 L6.23196822,0.5 C6.77992842,0.5 7.22413793,0.94118947 7.22413793,1.49216971 L7.22413793,6.23196822 C7.22413793,6.77992842 6.78294846,7.22413793 6.23196822,7.22413793 L1.49216971,7.22413793 C0.94420951,7.22413793 0.5,6.78294846 0.5,6.23196822 L0.5,1.49216971 Z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,3 +0,0 @@
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path fill="context-fill #4c4c4c" fill-opacity="context-fill-opacity" d="M12.9137931,3.0862069 L12.9137931,1.27586207 C12.9137931,0.84736528 12.5664278,0.5 12.137931,0.5 C11.7094342,0.5 11.362069,0.84736528 11.362069,1.27586207 L11.362069,1.27586207 L11.362069,3.0862069 L9.55172414,3.0862069 C9.12322735,3.0862069 8.77586207,3.43357218 8.77586207,3.86206897 C8.77586207,4.29056575 9.12322735,4.63793103 9.55172414,4.63793103 L11.362069,4.63793103 L11.362069,6.44827586 C11.362069,6.87677265 11.7094342,7.22413793 12.137931,7.22413793 L12.137931,7.22413793 C12.5664278,7.22413793 12.9137931,6.87677265 12.9137931,6.44827586 L12.9137931,6.44827586 L12.9137931,4.63793103 L14.7241379,4.63793103 C15.1526347,4.63793103 15.5,4.29056575 15.5,3.86206897 L15.5,3.86206897 C15.5,3.43357218 15.1526347,3.0862069 14.7241379,3.0862069 L14.7241379,3.0862069 L12.9137931,3.0862069 Z M0.5,9.76803178 C0.5,9.22007158 0.94118947,8.77586207 1.49216971,8.77586207 L6.23196822,8.77586207 C6.77992842,8.77586207 7.22413793,9.21705154 7.22413793,9.76803178 L7.22413793,14.5078303 C7.22413793,15.0557905 6.78294846,15.5 6.23196822,15.5 L1.49216971,15.5 C0.94420951,15.5 0.5,15.0588105 0.5,14.5078303 L0.5,9.76803178 Z M8.77586207,9.76803178 C8.77586207,9.22007158 9.21705154,8.77586207 9.76803178,8.77586207 L14.5078303,8.77586207 C15.0557905,8.77586207 15.5,9.21705154 15.5,9.76803178 L15.5,14.5078303 C15.5,15.0557905 15.0588105,15.5 14.5078303,15.5 L9.76803178,15.5 C9.22007158,15.5 8.77586207,15.0588105 8.77586207,14.5078303 L8.77586207,9.76803178 Z M0.5,1.49216971 C0.5,0.94420951 0.94118947,0.5 1.49216971,0.5 L6.23196822,0.5 C6.77992842,0.5 7.22413793,0.94118947 7.22413793,1.49216971 L7.22413793,6.23196822 C7.22413793,6.77992842 6.78294846,7.22413793 6.23196822,7.22413793 L1.49216971,7.22413793 C0.94420951,7.22413793 0.5,6.78294846 0.5,6.23196822 L0.5,1.49216971 Z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 18 18" style="enable-background:new 0 0 18 18;" xml:space="preserve">
<style type="text/css">
.st0{fill:#858585;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#858585;}
</style>
<path class="st0" d="M11.9,7.9c-1.2,0-1.5-0.8-1.8-2.4C10,4.4,9.8,2.5,8,2.5H4.6c-1.8,0-2,1.9-2.1,2.9C2.2,7.1,1.9,7.9,0.7,7.9v1.9
h11.2h5.4V7.9H11.9z"/>
<path class="st0" d="M16,5.7c-0.1-1-0.3-2.9-2.1-2.9c0,0-3.8,0-3.9,0c-0.1,0-0.1,0.2-0.1,0.2c1.1,0.5,1.3,1.9,1.4,2.7
c0.1,1.1,0.3,1.5,0.8,1.5c0.1,0,4.1,0,4.1,0s0.1,0,0.1-0.1C16.2,6.6,16.1,6.2,16,5.7z"/>
<path class="st1" d="M8,12.1H3.7v-1.2c0-0.3-0.2-0.4-0.5-0.2l-2.2,1.9c-0.3,0.2-0.3,0.6,0,0.9l2.2,1.9c0.3,0.2,0.5,0.2,0.5-0.2v-1.2
H8V12.1z"/>
<path class="st1" d="M17.1,12.6l-2.2-1.9c-0.3-0.2-0.5-0.2-0.5,0.2v1.2H10v1.9h4.3v1.2c0,0.3,0.2,0.4,0.5,0.2l2.2-1.9
C17.4,13.2,17.4,12.8,17.1,12.6z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 12 12" style="enable-background:new 0 0 12 12;" xml:space="preserve">
<style type="text/css">
.st0{fill:#858585;}
</style>
<circle class="st0" cx="6" cy="6" r="2"/>
<path class="st0" d="M11.5,5.4C11.4,5.2,8.6,2.3,6,2.3s-5.4,3-5.5,3.1C0.3,5.6,0.2,5.8,0.2,6v0c0,0.2,0.1,0.4,0.2,0.6
C0.6,6.8,3.4,9.8,6,9.8s5.4-3,5.5-3.1c0.2-0.2,0.2-0.4,0.2-0.6v0C11.7,5.8,11.7,5.6,11.5,5.4z M10.8,6C10.6,6.2,8.1,8.9,6,8.9
S1.3,6.2,1.1,6l0,0C1.3,5.8,3.8,3.1,6,3.1S10.6,5.8,10.8,6L10.8,6z"/>
</svg>

Before

Width:  |  Height:  |  Size: 755 B

13
src/img/info-thin-16.svg Normal file
View file

@ -0,0 +1,13 @@
<!-- 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 id="Flat_For_Export_" data-name="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs>
<style>
.cls-1{fill-rule:evenodd}
</style>
</defs>
<path fill="context-fill" fill-opacity="context-fill-opacity" class="cls-1" d="M8 1a7 7 0 1 0 7 7 7 7 0 0 0-7-7zm0 13a6 6 0 1 1 6-6 6 6 0 0 1-6 6z"/>
<path fill="context-fill" fill-opacity="context-fill-opacity" class="cls-1" d="M8 7a1 1 0 0 0-1 1v3a1 1 0 0 0 2 0V8a1 1 0 0 0-1-1z"/>
<circle cx="8" cy="5" r="1.19"/>
</svg>

After

Width:  |  Height:  |  Size: 718 B

View file

@ -0,0 +1,7 @@
<!-- 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 data-name="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M15.85 12.15l-3-3a0.49 0.49 0 0 0-0.7 0.7L14.29 12H9.5a0.5 0.5 0 0 0 0 1h4.79l-2.14 2.15a0.48 0.48 0 0 0 0 0.7 0.48 0.48 0 0 0 0.7 0l3-3a0.36 0.36 0 0 0 0.11-0.16 0.5 0.5 0 0 0 0-0.38 0.36 0.36 0 0 0-0.11-0.16z"/>
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M13 1H3a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h4a1 1 0 0 0 0-2H3a1 1 0 0 1-1-1V6h12v2a1 1 0 0 0 2 0V4a3 3 0 0 0-3-3zM2 5V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1z"/>
</svg>

After

Width:  |  Height:  |  Size: 796 B

View file

@ -0,0 +1,9 @@
<!-- 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 data-name="Flat (For Export)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<style>rect,path {fill: rgba(249, 249, 250, 0.8);}</style>
<rect x="1" y="1" width="6" height="6" rx="1"/>
<path d="M14.75 3H13V1.25A0.25 0.25 0 0 0 12.75 1h-1.5A0.25 0.25 0 0 0 11 1.25V3H9.25A0.25 0.25 0 0 0 9 3.25v1.5A0.25 0.25 0 0 0 9.25 5H11v1.75A0.25 0.25 0 0 0 11.25 7h1.5A0.25 0.25 0 0 0 13 6.75V5h1.75A0.25 0.25 0 0 0 15 4.75v-1.5A0.25 0.25 0 0 0 14.75 3z" fill-rule="evenodd"/>
<rect x="1" y="9" width="6" height="6" rx="1"/>
<rect x="9" y="9" width="6" height="6" rx="1"/>
</svg>

After

Width:  |  Height:  |  Size: 801 B

View file

@ -0,0 +1,7 @@
<svg data-name="Flat (For Export)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<style>rect,path {fill: rgba(24, 25, 26, 01);}</style>
<rect x="1" y="1" width="6" height="6" rx="1"/>
<path d="M14.75 3H13V1.25A0.25 0.25 0 0 0 12.75 1h-1.5A0.25 0.25 0 0 0 11 1.25V3H9.25A0.25 0.25 0 0 0 9 3.25v1.5A0.25 0.25 0 0 0 9.25 5H11v1.75A0.25 0.25 0 0 0 11.25 7h1.5A0.25 0.25 0 0 0 13 6.75V5h1.75A0.25 0.25 0 0 0 15 4.75v-1.5A0.25 0.25 0 0 0 14.75 3z" fill-rule="evenodd"/>
<rect x="1" y="9" width="6" height="6" rx="1"/>
<rect x="9" y="9" width="6" height="6" rx="1"/>
</svg>

After

Width:  |  Height:  |  Size: 586 B

View file

@ -2,5 +2,5 @@
- 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"/>
<path fill="context-fill" d="M14 7H9V2a1 1 0 0 0-2 0v5H2a1 1 0 1 0 0 2h5v5a1 1 0 0 0 2 0V9h5a1 1 0 0 0 0-2z"/>
</svg>

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 416 B

View file

@ -0,0 +1,9 @@
<!-- 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 data-name="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M12 7l-4 4a4 4 0 0 0 4-4z"/>
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M15.66 7.71a7.78 7.78 0 0 0-1.55-2.82L12.7 6.3a5.9 5.9 0 0 1 1 1.7A6 6 0 0 1 8 12a7.28 7.28 0 0 1-.93-.07l-1.64 1.64A7.92 7.92 0 0 0 8 14a8 8 0 0 0 7.66-5.71 1 1 0 0 0 0-.58zM14.71 1.29a1 1 0 0 0-1.42 0l-1.63 1.64A7.8 7.8 0 0 0 8 2a8 8 0 0 0-7.66 5.71 1 1 0 0 0 0 .58 7.8 7.8 0 0 0 2.34 3.62l-1.39 1.38a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l12-12a1 1 0 0 0 0-1.42zM8.5 5a1.43 1.43 0 0 1 .82.26L7.26 7.32A1.43 1.43 0 0 1 7 6.5 1.5 1.5 0 0 1 8.5 5zM2.35 8a6 6 0 0 1 2.11-2.82A3.91 3.91 0 0 0 5 9.61l-.9.9A5.91 5.91 0 0 1 2.35 8z"/>
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M12 7l-4 4a4 4 0 0 0 4-4z"/>
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M16 7.7a8.06 8.06 0 0 0-1.72-2.94l-1.45 1.41A5.91 5.91 0 0 1 13.94 8 6.33 6.33 0 0 1 8 12a7.28 7.28 0 0 1-.93-.07l-1.66 1.66A8.56 8.56 0 0 0 8 14a8.34 8.34 0 0 0 8-5.7 1.22 1.22 0 0 0 0-.6zM14.71 1.29a1 1 0 0 0-1.42 0L11.7 2.88A8.43 8.43 0 0 0 8 2a8.34 8.34 0 0 0-8 5.7 1.22 1.22 0 0 0 0 .6A7.87 7.87 0 0 0 2.58 12l-1.29 1.29a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l12-12a1 1 0 0 0 0-1.42zM8.5 5a1.43 1.43 0 0 1 .82.26L7.26 7.32A1.43 1.43 0 0 1 7 6.5 1.5 1.5 0 0 1 8.5 5zM2.06 8a6 6 0 0 1 2.49-3A4 4 0 0 0 4 7a4 4 0 0 0 1 2.61l-1 1A5.94 5.94 0 0 1 2.06 8z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

6
src/img/refresh-16.svg Normal file
View file

@ -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 data-name="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M15 1a1 1 0 0 0-1 1v2.42A7 7 0 1 0 13 13a1 1 0 0 0-1.41-1.41 5 5 0 1 1 1-5.54H10a1 1 0 0 0 0 2h5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 495 B

6
src/img/sort-16_1.svg Normal file
View file

@ -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 data-name="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.71 4.29l-3-3a1 1 0 0 0-1.42 1.42L11.59 4H4a1 1 0 0 0 0 2h7.59l-1.3 1.29a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3-3a1 1 0 0 0 0-1.42zM12 10H4.41l1.3-1.29a1 1 0 1 0-1.42-1.42l-3 3a1 1 0 0 0 0 1.42l3 3a1 1 0 0 0 1.42 0 1 1 0 0 0 0-1.42L4.41 12H12a1 1 0 0 0 0-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 625 B

6
src/img/tab-new-16.svg Normal file
View file

@ -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 data-name="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill="context-fill" fill-opacity="context-fill-opacity" d="M11 11V9a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1V5a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v6H1a1 1 0 0 0 0 2h7v-1a1 1 0 0 1 1-1zm4.5 1H13V9.5a0.5 0.5 0 0 0-1 0V12H9.5a0.5 0.5 0 0 0 0 1H12v2.5a0.5 0.5 0 0 0 1 0V13h2.5a0.5 0.5 0 0 0 0-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 583 B

View file

@ -13,6 +13,7 @@
display: none;
}
</style>
<path id="fence" d="M28 4l-2 2v4h-4V6l-2-2-2 2v4h-4V6l-2-2-2 2v4H6V6L4 4 2 6v22h4v-4h4v4h4v-4h4v4h4v-4h4v4h4V6l-2-2zM6 22V12h4v10H6zm8 0V12h4v10h-4zm8 0V12h4v10h-4z"/>
<path id="dollar" d="M16.2,0c-8.9,0-16,7.3-16,16c0,8.9,7.1,16,15.8,16s15.8-7.1,15.8-16C32,7.3,24.9,0,16.2,0z M17.1,25.1v1.6
c0,0.4-0.4,0.5-0.7,0.5c-0.4,0-0.7-0.2-0.7-0.5v-1.6c-3.2-0.2-5-1.8-5.5-4.3c0-0.2,0-0.2,0-0.4c0-0.5,0.4-0.9,0.9-0.9
c0.2,0,0.2,0,0.4,0c0.5,0,0.9,0.2,1.1,0.7c0.4,1.8,1.2,2.7,3.4,2.8v-6.8c-3.6-0.4-5.3-1.8-5.3-4.6c0-3,2.5-4.6,5.2-4.8V5.7
@ -69,4 +70,12 @@
s1.6,0.5,1.8,1.4v0.2c0,0.2,0,0.2,0,0.4c0,2,0.7,3.7,2.1,5c1.4,1.4,3,2.1,5,2.1l0,0c2,0,3.6-0.7,5-2.1c1.4-1.2,2.1-3.2,2.1-5V9.2
C32,5.2,28.8,2,24.7,2"/>
<circle id="circle" r="16" cx="16" cy="16" fill-rule="evenodd"/>
<path id="bullhorn" d="M1.5 5A.5.5 0 0 0 1 5.5v5a.5.5 0 0 0 1 0v-5A.5.5 0 0 0 1.5 5zM14.6 2.2A1 1 0 0 0 13.71 2l-10 3A1 1 0 0 0 3 6v4a1 1 0 0 0 .71 1L5 11.35a.4.4 0 0 0 0 .15v1a2.5 2.5 0 0 0 5 .33L13.71 14A1 1 0 0 0 14 14a1 1 0 0 0 .6-.2A1 1 0 0 0 15 13V3a1 1 0 0 0-.4-.8zM7.5 14A1.5 1.5 0 0 1 6 12.5v-.86l3 .9A1.51 1.51 0 0 1 7.5 14zm5.5-2.34l-8-2.4V6.74l8-2.4z"/>
<path id="folder" d="M13 4H8.41L7 2.59A2 2 0 0 0 5.59 2H3a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"/>
<path id="hat" d="M15 10a1 1 0 0 0-1 1 1 1 0 0 1-1 1V6a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v6a1 1 0 0 1-1-1 1 1 0 0 0-2 0 3 3 0 0 0 3 3h10a3 3 0 0 0 3-3 1 1 0 0 0-1-1zM7 4h2a2 2 0 0 1 2 2v3H5V6a2 2 0 0 1 2-2z"/>
<path id="wallet" d="M14 2H2C1 2 0.06 3 0 4.86v6.34A1.94 1.94 0 0 0 1.22 13l7 2.86A2 2 0 0 0 9 16a2 2 0 0 0 1.14-0.35A1.9 1.9 0 0 0 11 14.07V7.8A1.94 1.94 0 0 0 9.78 6L4.85 4h8.65a0.5 0.5 0 0 1 0 1H9.92l0.24 0.1a2.93 2.93 0 0 1 1.2 0.9h2.14A0.5 0.5 0 0 1 14 6.5v3a0.51 0.51 0 0 1-0.5 0.5H12v2h2a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2zm-6 8.5a0.5 0.5 0 0 1 1 0v1a0.5 0.5 0 0 1-1 0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<!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">

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<!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">

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -7,6 +7,8 @@ module.exports = {
"badge": true,
"backgroundLogic": true,
"identityState": true,
"messageHandler": true
"messageHandler": true,
"sync": true,
"Utils": true
}
};

View file

@ -1,16 +1,17 @@
const assignManager = {
window.assignManager = {
MENU_ASSIGN_ID: "open-in-this-container",
MENU_REMOVE_ID: "remove-open-in-this-container",
MENU_SEPARATOR_ID: "separator",
MENU_HIDE_ID: "hide-container",
MENU_MOVE_ID: "move-to-new-window-container",
OPEN_IN_CONTAINER: "open-bookmark-in-container-tab",
storageArea: {
area: browser.storage.local,
exemptedTabs: {},
getSiteStoreKey(pageUrl) {
const url = new window.URL(pageUrl);
getSiteStoreKey(pageUrlorUrlKey) {
if (pageUrlorUrlKey.includes("siteContainerMap@@_")) return pageUrlorUrlKey;
const url = new window.URL(pageUrlorUrlKey);
const storagePrefix = "siteContainerMap@@_";
if (url.port === "80" || url.port === "443") {
return `${storagePrefix}${url.hostname}`;
@ -19,29 +20,43 @@ const assignManager = {
}
},
setExempted(pageUrl, tabId) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
setExempted(pageUrlorUrlKey, tabId) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
if (!(siteStoreKey in this.exemptedTabs)) {
this.exemptedTabs[siteStoreKey] = [];
}
this.exemptedTabs[siteStoreKey].push(tabId);
},
removeExempted(pageUrl) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
removeExempted(pageUrlorUrlKey) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
this.exemptedTabs[siteStoreKey] = [];
},
isExempted(pageUrl, tabId) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
isExempted(pageUrlorUrlKey, tabId) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
if (!(siteStoreKey in this.exemptedTabs)) {
return false;
}
return this.exemptedTabs[siteStoreKey].includes(tabId);
},
get(pageUrl) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
get(pageUrlorUrlKey) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
return this.getByUrlKey(siteStoreKey);
},
async getSyncEnabled() {
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
return !!syncEnabled;
},
async getReplaceTabEnabled() {
const { replaceTabEnabled } = await browser.storage.local.get("replaceTabEnabled");
return !!replaceTabEnabled;
},
getByUrlKey(siteStoreKey) {
return new Promise((resolve, reject) => {
this.area.get([siteStoreKey]).then((storageResponse) => {
if (storageResponse && siteStoreKey in storageResponse) {
@ -54,51 +69,103 @@ const assignManager = {
});
},
set(pageUrl, data, exemptedTabIds) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
async set(pageUrlorUrlKey, data, exemptedTabIds, backup = true) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
if (exemptedTabIds) {
exemptedTabIds.forEach((tabId) => {
this.setExempted(pageUrl, tabId);
this.setExempted(pageUrlorUrlKey, tabId);
});
}
return this.area.set({
// eslint-disable-next-line require-atomic-updates
data.identityMacAddonUUID =
await identityState.lookupMACaddonUUID(data.userContextId);
await this.area.set({
[siteStoreKey]: data
});
const syncEnabled = await this.getSyncEnabled();
if (backup && syncEnabled) {
await sync.storageArea.backup({undeleteSiteStoreKey: siteStoreKey});
}
return;
},
remove(pageUrl) {
const siteStoreKey = this.getSiteStoreKey(pageUrl);
async remove(pageUrlorUrlKey) {
const siteStoreKey = this.getSiteStoreKey(pageUrlorUrlKey);
// When we remove an assignment we should clear all the exemptions
this.removeExempted(pageUrl);
return this.area.remove([siteStoreKey]);
this.removeExempted(pageUrlorUrlKey);
await this.area.remove([siteStoreKey]);
const syncEnabled = await this.getSyncEnabled();
if (syncEnabled) await sync.storageArea.backup({siteStoreKey});
return;
},
async deleteContainer(userContextId) {
const sitesByContainer = await this.getByContainer(userContextId);
const sitesByContainer = await this.getAssignedSites(userContextId);
this.area.remove(Object.keys(sitesByContainer));
},
async getByContainer(userContextId) {
async getAssignedSites(userContextId = null) {
const sites = {};
const siteConfigs = await this.area.get();
Object.keys(siteConfigs).forEach((key) => {
// For some reason this is stored as string... lets check them both as that
if (String(siteConfigs[key].userContextId) === String(userContextId)) {
const site = siteConfigs[key];
for(const urlKey of Object.keys(siteConfigs)) {
if (urlKey.includes("siteContainerMap@@_")) {
// For some reason this is stored as string... lets check
// them both as that
if (!!userContextId &&
String(siteConfigs[urlKey].userContextId)
!== String(userContextId)) {
continue;
}
const site = siteConfigs[urlKey];
// In hindsight we should have stored this
// TODO file a follow up to clean the storage onLoad
site.hostname = key.replace(/^siteContainerMap@@_/, "");
sites[key] = site;
site.hostname = urlKey.replace(/^siteContainerMap@@_/, "");
sites[urlKey] = site;
}
}
});
return sites;
},
/*
* Looks for abandoned site assignments. If there is no identity with
* the site assignment's userContextId (cookieStoreId), then the assignment
* is removed.
*/
async upgradeData() {
const identitiesList = await browser.contextualIdentities.query({});
const macConfigs = await this.area.get();
for(const configKey of Object.keys(macConfigs)) {
if (configKey.includes("siteContainerMap@@_")) {
const cookieStoreId =
"firefox-container-" + macConfigs[configKey].userContextId;
const match = identitiesList.find(
localIdentity => localIdentity.cookieStoreId === cookieStoreId
);
if (!match) {
await this.remove(configKey);
continue;
}
const updatedSiteAssignment = macConfigs[configKey];
updatedSiteAssignment.identityMacAddonUUID =
await identityState.lookupMACaddonUUID(match.cookieStoreId);
await this.set(
configKey,
updatedSiteAssignment,
false,
false
);
}
}
}
},
_neverAsk(m) {
const pageUrl = m.pageUrl;
if (m.neverAsk === true) {
// If we have existing data and for some reason it hasn't been deleted etc lets update it
// If we have existing data and for some reason it hasn't been
// deleted etc lets update it
this.storageArea.get(pageUrl).then((siteSettings) => {
if (siteSettings) {
siteSettings.neverAsk = true;
@ -113,12 +180,11 @@ const assignManager = {
// We return here so the confirm page can load the tab when exempted
async _exemptTab(m) {
const pageUrl = m.pageUrl;
this.storageArea.setExempted(pageUrl, m.tabId);
await this.storageArea.setExempted(pageUrl, m.tabId);
return true;
},
async handleProxifiedRequest(requestInfo) {
// The following blocks potentially dangerous requests for privacy that come without a tabId
if(requestInfo.tabId === -1)
return Utils.getBogusProxy();
@ -129,7 +195,8 @@ const assignManager = {
return proxy;
},
// Before a request is handled by the browser we decide if we should route through a different container
// Before a request is handled by the browser we decide if we should
// route through a different container
async onBeforeRequest(options) {
if (options.frameId !== 0 || options.tabId === -1) {
return {};
@ -141,31 +208,57 @@ const assignManager = {
]);
let container;
try {
container = await browser.contextualIdentities.get(backgroundLogic.cookieStoreId(siteSettings.userContextId));
container = await browser.contextualIdentities
.get(backgroundLogic.cookieStoreId(siteSettings.userContextId));
} catch (e) {
container = false;
}
// The container we have in the assignment map isn't present any more so lets remove it
// then continue the existing load
// 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);
// https://github.com/mozilla/multi-account-containers/issues/847
//
// Handle the case where this request's URL is not assigned to any particular
// container. We must do the following check:
//
// If the current tab's container is "unlocked", we can just go ahead
// and open the URL in the current tab, since an "unlocked" container accepts
// any-and-all sites.
//
// But if the current tab's container has been "locked" by the user, then we must
// re-open the page in the default container, because the user doesn't want random
// sites polluting their locked container.
//
// For example:
// - the current tab's container is locked and only allows "www.google.com"
// - the incoming request is for "www.amazon.com", which has no specific container assignment
// - in this case, we must re-open "www.amazon.com" in a new tab in the default container
const siteIsolatedReloadInDefault =
await this._maybeSiteIsolatedReloadInDefault(siteSettings, tab);
if (!siteIsolatedReloadInDefault) {
if (!siteSettings
|| userContextId === siteSettings.userContextId
|| tab.incognito
|| this.storageArea.isExempted(options.url, tab.id)) {
return {};
}
}
const replaceTabEnabled = await this.storageArea.getReplaceTabEnabled();
const removeTab = backgroundLogic.NEW_TAB_PAGES.has(tab.url)
|| (messageHandler.lastCreatedTab
&& messageHandler.lastCreatedTab.id === tab.id);
&& messageHandler.lastCreatedTab.id === tab.id)
|| replaceTabEnabled;
const openTabId = removeTab ? tab.openerTabId : tab.id;
if (!this.canceledRequests[tab.id]) {
// we decided to cancel the request at this point, register canceled request
// we decided to cancel the request at this point, register
// canceled request
this.canceledRequests[tab.id] = {
requestIds: {
[options.requestId]: true
@ -175,8 +268,10 @@ const assignManager = {
}
};
// 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
// 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]) {
@ -188,10 +283,12 @@ const assignManager = {
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
// 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
// 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) {
@ -201,6 +298,14 @@ const assignManager = {
}
}
if (siteIsolatedReloadInDefault) {
this.reloadPageInDefaultContainer(
options.url,
tab.index + 1,
tab.active,
openTabId
);
} else {
this.reloadPageInContainer(
options.url,
userContextId,
@ -210,18 +315,31 @@ const assignManager = {
siteSettings.neverAsk,
openTabId
);
}
this.calculateContextMenu(tab);
/* Removal of existing tabs:
We aim to open the new assigned container tab / warning prompt in it's own tab:
- As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back()
- When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421
If we are coming from an internal url that are used for the new tab page (NEW_TAB_PAGES), we can safely close as user is unlikely losing history
Detecting redirects on "new tab" opening actions is pretty hard as we don't get tab history:
- Redirects happen from Short URLs and tracking links that act as a gateway
- Extensions don't provide a way to history crawl for tabs, we could inject content scripts to do this
however they don't run on about:blank so this would likely be just as hacky.
We capture the time the tab was created and close if it was within the timeout to try to capture pages which haven't had user interaction or history.
We aim to open the new assigned container tab / warning prompt in
it's own tab:
- As the history won't span from one container to another it
seems most sane to not try and reopen a tab on history.back()
- When users open a new tab themselves we want to make sure we
don't end up with three tabs as per:
https://github.com/mozilla/testpilot-containers/issues/421
If we are coming from an internal url that are used for the new
tab page (NEW_TAB_PAGES), we can safely close as user is unlikely
losing history
Detecting redirects on "new tab" opening actions is pretty hard
as we don't get tab history:
- Redirects happen from Short URLs and tracking links that act as
a gateway
- Extensions don't provide a way to history crawl for tabs, we
could inject content scripts to do this
however they don't run on about:blank so this would likely be
just as hacky.
We capture the time the tab was created and close if it was within
the timeout to try to capture pages which haven't had user
interaction or history.
*/
if (removeTab) {
browser.tabs.remove(tab.id);
@ -231,15 +349,41 @@ const assignManager = {
};
},
async _maybeSiteIsolatedReloadInDefault(siteSettings, tab) {
// Tab doesn't support cookies, so containers not supported either.
if (!("cookieStoreId" in tab)) {
return false;
}
// Requested page has been assigned to a specific container.
// I.e. it will be opened in that container anyway, so we don't need to check if the
// current tab's container is locked or not.
if (siteSettings) {
return false;
}
//tab is alredy reopening in the default container
if (tab.cookieStoreId === "firefox-default") {
return false;
}
// Requested page is not assigned to a specific container. If the current tab's container
// is locked, then the page must be reloaded in the default container.
const currentContainerState = await identityState.storageArea.get(tab.cookieStoreId);
return currentContainerState && currentContainerState.isIsolated;
},
init() {
browser.contextMenus.onClicked.addListener((info, tab) => {
info.bookmarkId ?
this._onClickedBookmark(info) :
this._onClickedHandler(info, tab);
});
// Before anything happens we decide if the request should be proxified
browser.proxy.onRequest.addListener(this.handleProxifiedRequest, {urls: ["<all_urls>"]});
// Before a request is handled by the browser we decide if we should route through a different container
// Before a request is handled by the browser we decide if we should
// route through a different container
this.canceledRequests = {};
browser.webRequest.onBeforeRequest.addListener((options) => {
return this.onBeforeRequest(options);
@ -257,6 +401,60 @@ const assignManager = {
}
},{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) {
@ -272,7 +470,9 @@ const assignManager = {
} else {
remove = true;
}
await this._setOrRemoveAssignment(tab.id, info.pageUrl, userContextId, remove);
await this._setOrRemoveAssignment(
tab.id, info.pageUrl, userContextId, remove
);
break;
case this.MENU_MOVE_ID:
backgroundLogic.moveTabsToWindow({
@ -290,6 +490,41 @@ const assignManager = {
}
},
async _onClickedBookmark(info) {
async function _getBookmarksFromInfo(info) {
const [bookmarkTreeNode] =
await browser.bookmarks.get(info.bookmarkId);
if (bookmarkTreeNode.type === "folder") {
return browser.bookmarks.getChildren(bookmarkTreeNode.id);
}
return [bookmarkTreeNode];
}
const bookmarks = await _getBookmarksFromInfo(info);
for (const bookmark of bookmarks) {
// Some checks on the urls from
// https://github.com/Rob--W/bookmark-container-tab/ thanks!
if ( !/^(javascript|place):/i.test(bookmark.url) &&
bookmark.type !== "folder") {
const openInReaderMode = bookmark.url.startsWith("about:reader");
if(openInReaderMode) {
try {
const parsed = new URL(bookmark.url);
bookmark.url = parsed.searchParams.get("url") + parsed.hash;
} catch (err) {
return err.message;
}
}
browser.tabs.create({
cookieStoreId: info.menuItemId,
url: bookmark.url,
openInReaderMode: openInReaderMode
});
}
}
},
deleteContainer(userContextId) {
this.storageArea.deleteContainer(userContextId);
@ -299,16 +534,16 @@ const assignManager = {
if (!("cookieStoreId" in tab)) {
return false;
}
return backgroundLogic.getUserContextIdFromCookieStoreId(tab.cookieStoreId);
return backgroundLogic.getUserContextIdFromCookieStoreId(
tab.cookieStoreId
);
},
isTabPermittedAssign(tab) {
// Ensure we are not an important about url
// Ensure we are not in incognito mode
const url = new URL(tab.url);
if (url.protocol === "about:"
|| url.protocol === "moz-extension:"
|| tab.incognito) {
|| url.protocol === "moz-extension:") {
return false;
}
return true;
@ -316,7 +551,6 @@ const assignManager = {
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
@ -341,16 +575,40 @@ const assignManager = {
userContextId,
neverAsk: false
}, exemptedTabIds);
actionName = "added";
actionName = "assigned site to always open in this container";
} else {
// Remove assignment
await this.storageArea.remove(pageUrl);
actionName = "removed";
actionName = "removed from assigned sites list";
// remove site isolation if now empty
await this._maybeRemoveSiteIsolation(userContextId);
}
browser.tabs.sendMessage(tabId, {
text: `Successfully ${actionName} site to always open in this container`
});
if (tabId) {
const tab = await browser.tabs.get(tabId);
setTimeout(function(){
browser.tabs.sendMessage(tabId, {
text: `Successfully ${actionName}`
});
}, 1000);
this.calculateContextMenu(tab);
}
},
async _maybeRemoveSiteIsolation(userContextId) {
const assignments = await this.storageArea.getByContainer(userContextId);
const hasAssignments = assignments && Object.keys(assignments).length > 0;
if (hasAssignments) {
return;
}
await backgroundLogic.addRemoveSiteIsolation(
backgroundLogic.cookieStoreId(userContextId),
true
);
},
async _getAssignment(tab) {
@ -358,13 +616,13 @@ const assignManager = {
// Ensure we have a cookieStore to assign to
if (cookieStore
&& this.isTabPermittedAssign(tab)) {
return await this.storageArea.get(tab.url);
return this.storageArea.get(tab.url);
}
return false;
},
_getByContainer(userContextId) {
return this.storageArea.getByContainer(userContextId);
return this.storageArea.getAssignedSites(userContextId);
},
removeContextMenu() {
@ -430,13 +688,35 @@ const assignManager = {
});
},
reloadPageInDefaultContainer(url, index, active, openerTabId) {
// To create a new tab in the default container, it is easiest just to omit the
// cookieStoreId entirely.
//
// Unfortunately, if you create a new tab WITHOUT a cookieStoreId but WITH an openerTabId,
// then the new tab automatically inherits the opener tab's cookieStoreId.
// I.e. it opens in the wrong container!
//
// So we have to explicitly pass in a cookieStoreId when creating the tab, since we
// are specifying the openerTabId. There doesn't seem to be any way
// to look up the default container's cookieStoreId programatically, so sadly
// we have to hardcode it here as "firefox-default". This is potentially
// not cross-browser compatible.
//
// Note that we could have just omitted BOTH cookieStoreId and openerTabId. But the
// drawback then is that if the user later closes the newly-created tab, the browser
// does not automatically return to the original opener tab. To get this desired behaviour,
// we MUST specify the openerTabId when creating the new tab.
const cookieStoreId = "firefox-default";
browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
},
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});
return browser.tabs.create({url, cookieStoreId, index, active, openerTabId});
} else {
let confirmUrl = `${loadPage}?url=${this.encodeURLProperty(url)}&cookieStoreId=${cookieStoreId}`;
let currentCookieStoreId;
@ -444,7 +724,7 @@ const assignManager = {
currentCookieStoreId = backgroundLogic.cookieStoreId(currentUserContextId);
confirmUrl += `&currentCookieStoreId=${currentCookieStoreId}`;
}
browser.tabs.create({
return browser.tabs.create({
url: confirmUrl,
cookieStoreId: currentCookieStoreId,
openerTabId,
@ -457,7 +737,33 @@ const assignManager = {
throw e;
});
}
},
async initBookmarksMenu() {
browser.contextMenus.create({
id: this.OPEN_IN_CONTAINER,
title: "Open Bookmark in Container Tab",
contexts: ["bookmark"],
});
const identities = await browser.contextualIdentities.query({});
for (const identity of identities) {
browser.contextMenus.create({
parentId: this.OPEN_IN_CONTAINER,
id: identity.cookieStoreId,
title: identity.name,
icons: { "16": `img/usercontext.svg#${identity.icon}` }
});
}
},
async removeBookmarksMenu() {
browser.contextMenus.remove(this.OPEN_IN_CONTAINER);
const identities = await browser.contextualIdentities.query({});
for (const identity of identities) {
browser.contextMenus.remove(identity.cookieStoreId);
}
},
};
assignManager.init();

View file

@ -6,7 +6,20 @@ const backgroundLogic = {
"about:home",
"about:blank"
]),
NUMBER_OF_KEYBOARD_SHORTCUTS: 10,
unhideQueue: [],
init() {
browser.commands.onCommand.addListener(function (command) {
for (let i=0; i < backgroundLogic.NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
const key = "open_container_" + i;
const cookieStoreId = identityState.keyboardShortcut[key];
if (command === key) {
if (cookieStoreId === "none") return;
browser.tabs.create({cookieStoreId});
}
}
});
},
async getExtensionInfo() {
const manifestPath = browser.extension.getURL("manifest.json");
@ -59,15 +72,13 @@ const backgroundLogic = {
});
}
await donePromise;
browser.runtime.sendMessage({
method: "refreshNeeded"
});
},
async openNewTab(options) {
let url = options.url || undefined;
const userContextId = ("userContextId" in options) ? options.userContextId : 0;
const active = ("nofocus" in options) ? options.nofocus : true;
const discarded = ("noload" in options) ? options.noload : false;
const cookieStoreId = backgroundLogic.cookieStoreId(userContextId);
// Autofocus url bar will happen in 54: https://bugzilla.mozilla.org/show_bug.cgi?id=1295072
@ -84,6 +95,7 @@ const backgroundLogic = {
return browser.tabs.create({
url,
active,
discarded,
pinned: options.pinned || false,
cookieStoreId
});
@ -126,16 +138,31 @@ const backgroundLogic = {
return list.concat(containerState.hiddenTabs);
},
async unhideContainer(cookieStoreId) {
async unhideContainer(cookieStoreId, alreadyShowingUrl) {
if (!this.unhideQueue.includes(cookieStoreId)) {
this.unhideQueue.push(cookieStoreId);
await this.showTabs({
cookieStoreId
cookieStoreId,
alreadyShowingUrl
});
this.unhideQueue.splice(this.unhideQueue.indexOf(cookieStoreId), 1);
}
},
// https://github.com/mozilla/multi-account-containers/issues/847
async addRemoveSiteIsolation(cookieStoreId, remove = false) {
const containerState = await identityState.storageArea.get(cookieStoreId);
try {
if ("isIsolated" in containerState || remove) {
delete containerState.isIsolated;
} else {
containerState.isIsolated = "locked";
}
return await identityState.storageArea.set(cookieStoreId, containerState);
} catch (error) {
console.error(`No container: ${cookieStoreId}`);
}
},
async moveTabsToWindow(options) {
const requiredArguments = ["cookieStoreId", "windowId"];
@ -242,7 +269,8 @@ const backgroundLogic = {
hasHiddenTabs: !!containerState.hiddenTabs.length,
hasOpenTabs: !!openTabs.length,
numberOfHiddenTabs: containerState.hiddenTabs.length,
numberOfOpenTabs: openTabs.length
numberOfOpenTabs: openTabs.length,
isIsolated: !!containerState.isIsolated
};
return;
});
@ -322,21 +350,30 @@ const backgroundLogic = {
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
const noload = !object.pinned;
if (object.url !== options.alreadyShowingUrl) {
promises.push(this.openNewTab({
userContextId: userContextId,
url: object.url,
nofocus: options.nofocus || false,
noload: noload,
pinned: object.pinned,
}));
}
}
containerState.hiddenTabs = [];
await Promise.all(promises);
return await identityState.storageArea.set(options.cookieStoreId, containerState);
return identityState.storageArea.set(options.cookieStoreId, containerState);
},
cookieStoreId(userContextId) {
if(userContextId === 0) return "firefox-default";
return `firefox-container-${userContextId}`;
}
};
backgroundLogic.init();

View file

@ -1,13 +1,8 @@
const MAJOR_VERSIONS = ["2.3.0", "2.4.0"];
const MAJOR_VERSIONS = ["2.3.0", "2.4.0", "6.2.0"];
const badge = {
async init() {
const currentWindow = await browser.windows.getCurrent();
this.displayBrowserActionBadge(currentWindow.incognito);
},
disableAddon(tabId) {
browser.browserAction.disable(tabId);
browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" });
this.displayBrowserActionBadge(currentWindow);
},
async displayBrowserActionBadge() {

View file

@ -1,4 +1,5 @@
const identityState = {
window.identityState = {
keyboardShortcut: {},
storageArea: {
area: browser.storage.local,
@ -11,12 +12,23 @@ const identityState = {
const storeKey = this.getContainerStoreKey(cookieStoreId);
const storageResponse = await this.area.get([storeKey]);
if (storageResponse && storeKey in storageResponse) {
if (!storageResponse[storeKey].macAddonUUID){
storageResponse[storeKey].macAddonUUID = uuidv4();
await this.set(cookieStoreId, storageResponse[storeKey]);
}
return storageResponse[storeKey];
}
// 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;
}
return false;
},
set(cookieStoreId, data) {
@ -26,16 +38,82 @@ const identityState = {
});
},
remove(cookieStoreId) {
async remove(cookieStoreId) {
const storeKey = this.getContainerStoreKey(cookieStoreId);
return this.area.remove([storeKey]);
},
async setKeyboardShortcut(shortcutId, cookieStoreId) {
identityState.keyboardShortcut[shortcutId] = cookieStoreId;
return this.area.set({[shortcutId]: cookieStoreId});
},
async loadKeyboardShortcuts () {
const identities = await browser.contextualIdentities.query({});
for (let i=0; i < backgroundLogic.NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
const key = "open_container_" + i;
const storageObject = await this.area.get(key);
if (storageObject[key]){
identityState.keyboardShortcut[key] = storageObject[key];
continue;
}
if (identities[i]) {
identityState.keyboardShortcut[key] = identities[i].cookieStoreId;
continue;
}
identityState.keyboardShortcut[key] = "none";
}
return identityState.keyboardShortcut;
},
/*
* 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});
@ -54,9 +132,63 @@ const identityState = {
return this.storageArea.set(cookieStoreId, containerState);
},
async updateUUID(cookieStoreId, uuid) {
if (!cookieStoreId || !uuid) {
throw new Error ("cookieStoreId or uuid missing");
}
const containerState = await this.storageArea.get(cookieStoreId);
containerState.macAddonUUID = uuid;
await this.storageArea.set(cookieStoreId, containerState);
return uuid;
},
async addUUID(cookieStoreId) {
await this.storageArea.get(cookieStoreId);
},
async lookupMACaddonUUID(cookieStoreId) {
// This stays a lookup, because if the cookieStoreId doesn't
// exist, this.get() will create it, which is not what we want.
const cookieStoreIdKey = cookieStoreId.includes("firefox-container-") ?
cookieStoreId : "firefox-container-" + cookieStoreId;
const macConfigs = await this.storageArea.area.get();
for(const configKey of Object.keys(macConfigs)) {
if (configKey === this.storageArea.getContainerStoreKey(cookieStoreIdKey)) {
return macConfigs[configKey].macAddonUUID;
}
}
return false;
},
async lookupCookieStoreId(macAddonUUID) {
const macConfigs = await this.storageArea.area.get();
for(const configKey of Object.keys(macConfigs)) {
if (configKey.includes("identitiesState@@_")) {
if(macConfigs[configKey].macAddonUUID === macAddonUUID) {
return String(configKey).replace(/^identitiesState@@_/, "");
}
}
}
return false;
},
_createIdentityState() {
return {
hiddenTabs: []
hiddenTabs: [],
macAddonUUID: uuidv4()
};
},
init() {
this.storageArea.loadKeyboardShortcuts();
}
};
identityState.init();
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)
);
}

View file

@ -20,5 +20,6 @@
<script type="text/javascript" src="badge.js"></script>
<script type="text/javascript" src="identityState.js"></script>
<script type="text/javascript" src="messageHandler.js"></script>
<script type="text/javascript" src="sync.js"></script>
</body>
</html>

View file

@ -6,10 +6,23 @@ const messageHandler = {
init() {
// Handles messages from webextension code
browser.runtime.onMessage.addListener((m) => {
browser.runtime.onMessage.addListener(async (m) => {
let response;
let tab;
switch (m.method) {
case "getShortcuts":
response = identityState.storageArea.loadKeyboardShortcuts();
break;
case "setShortcut":
identityState.storageArea.setKeyboardShortcut(m.shortcut, m.cookieStoreId);
break;
case "resetSync":
response = sync.resetSync();
break;
case "resetBookmarksContext":
response = assignManager.resetBookmarksMenuItem();
break;
case "deleteContainer":
response = backgroundLogic.deleteContainer(m.message.userContextId);
break;
@ -19,6 +32,9 @@ const messageHandler = {
case "neverAsk":
assignManager._neverAsk(m);
break;
case "addRemoveSiteIsolation":
response = backgroundLogic.addRemoveSiteIsolation(m.cookieStoreId);
break;
case "getAssignment":
response = browser.tabs.get(m.tabId).then((tab) => {
return assignManager._getAssignment(tab);
@ -30,9 +46,7 @@ const messageHandler = {
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);
});
response = assignManager._setOrRemoveAssignment(m.tabId, m.url, m.userContextId, m.value);
break;
case "sortTabs":
backgroundLogic.sortTabs();
@ -67,6 +81,31 @@ const messageHandler = {
case "exemptContainerAssignment":
response = assignManager._exemptTab(m);
break;
case "reloadInContainer":
response = assignManager.reloadPageInContainer(
m.url,
m.currentUserContextId,
m.newUserContextId,
m.tabIndex,
m.active,
true
);
break;
case "assignAndReloadInContainer":
tab = await assignManager.reloadPageInContainer(
m.url,
m.currentUserContextId,
m.newUserContextId,
m.tabIndex,
m.active,
true
);
// 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(tab.id).then((tab) => {
return assignManager._setOrRemoveAssignment(tab.id, m.url, m.newUserContextId, m.value);
});
break;
}
return response;
});
@ -79,6 +118,7 @@ const messageHandler = {
if (!extensionInfo.permissions.includes("contextualIdentities")) {
throw new Error("Missing contextualIdentities permission");
}
// eslint-disable-next-line require-atomic-updates
externalExtensionAllowed[sender.id] = true;
}
let response;
@ -141,9 +181,6 @@ const messageHandler = {
}, {urls: ["<all_urls>"], types: ["main_frame"]});
browser.tabs.onCreated.addListener((tab) => {
if (tab.incognito) {
badge.disableAddon(tab.id);
}
// lets remember the last tab created so we can close it if it looks like a redirect
this.lastCreatedTab = tab;
if (tab.cookieStoreId) {
@ -153,9 +190,26 @@ const messageHandler = {
!tab.url.startsWith("moz-extension")) {
// increment the counter of container tabs opened
this.incrementCountOfContainerTabsOpened();
}
backgroundLogic.unhideContainer(tab.cookieStoreId);
this.tabUpdateHandler = (tabId, changeInfo) => {
if (tabId === tab.id && changeInfo.status === "complete") {
// get current tab's url to not open the same one from hidden tabs
browser.tabs.get(tabId).then(loadedTab => {
backgroundLogic.unhideContainer(tab.cookieStoreId, loadedTab.url);
}).catch((e) => {
throw e;
});
browser.tabs.onUpdated.removeListener(this.tabUpdateHandler);
}
};
// if it's a container tab wait for it to complete and
// unhide other tabs from this container
if (tab.cookieStoreId.startsWith("firefox-container")) {
browser.tabs.onUpdated.addListener(this.tabUpdateHandler);
}
}
}
setTimeout(() => {
this.lastCreatedTab = null;

580
src/js/background/sync.js Normal file
View file

@ -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}`);
}

View file

@ -1,6 +1,6 @@
async function load() {
const searchParams = new URL(window.location).searchParams;
const redirectUrl = decodeURIComponent(searchParams.get("url"));
const redirectUrl = searchParams.get("url");
const cookieStoreId = searchParams.get("cookieStoreId");
const currentCookieStoreId = searchParams.get("currentCookieStoreId");
const redirectUrlElement = document.getElementById("redirect-url");
@ -17,25 +17,14 @@ async function load() {
const currentContainer = await browser.contextualIdentities.get(currentCookieStoreId);
document.getElementById("current-container-name").textContent = currentContainer.name;
}
document.getElementById("redirect-form").addEventListener("submit", (e) => {
document.getElementById("deny").addEventListener("click", (e) => {
e.preventDefault();
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":
});
document.getElementById("confirm").addEventListener("click", (e) => {
e.preventDefault();
confirmSubmit(redirectUrl, cookieStoreId);
break;
}
});
}

90
src/js/options.js Normal file
View file

@ -0,0 +1,90 @@
const NUMBER_OF_KEYBOARD_SHORTCUTS = 10;
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");
await browser.storage.local.set({syncEnabled: !!checkbox.checked});
browser.runtime.sendMessage({ method: "resetSync" });
}
async function enableDisableReplaceTab() {
const checkbox = document.querySelector("#replaceTabCheck");
await browser.storage.local.set({replaceTabEnabled: !!checkbox.checked});
}
async function setupOptions() {
const hasPermission = await browser.permissions.contains({permissions: ["bookmarks"]});
const { syncEnabled } = await browser.storage.local.get("syncEnabled");
const { replaceTabEnabled } = await browser.storage.local.get("replaceTabEnabled");
if (hasPermission) {
document.querySelector("#bookmarksPermissions").checked = true;
}
document.querySelector("#syncCheck").checked = !!syncEnabled;
document.querySelector("#replaceTabCheck").checked = !!replaceTabEnabled;
setupContainerShortcutSelects();
}
async function setupContainerShortcutSelects () {
const keyboardShortcut = await browser.runtime.sendMessage({method: "getShortcuts"});
const identities = await browser.contextualIdentities.query({});
const fragment = document.createDocumentFragment();
const noneOption = document.createElement("option");
noneOption.value = "none";
noneOption.id = "none";
noneOption.textContent = "None";
fragment.append(noneOption);
for (const identity of identities) {
const option = document.createElement("option");
option.value = identity.cookieStoreId;
option.id = identity.cookieStoreId;
option.textContent = identity.name;
fragment.append(option);
}
for (let i=0; i < NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
const shortcutKey = "open_container_"+i;
const shortcutSelect = document.getElementById(shortcutKey);
shortcutSelect.appendChild(fragment.cloneNode(true));
if (keyboardShortcut && keyboardShortcut[shortcutKey]) {
const cookieStoreId = keyboardShortcut[shortcutKey];
shortcutSelect.querySelector("#" + cookieStoreId).selected = true;
}
}
}
function storeShortcutChoice (event) {
browser.runtime.sendMessage({
method: "setShortcut",
shortcut: event.target.id,
cookieStoreId: event.target.value
});
}
function resetOnboarding() {
browser.storage.local.set({"onboarding-stage": 0});
}
document.addEventListener("DOMContentLoaded", setupOptions);
document.querySelector("#bookmarksPermissions").addEventListener( "change", requestPermissions);
document.querySelector("#syncCheck").addEventListener( "change", enableDisableSync);
document.querySelector("#replaceTabCheck").addEventListener( "change", enableDisableReplaceTab);
document.querySelector("button").addEventListener("click", resetOnboarding);
for (let i=0; i < NUMBER_OF_KEYBOARD_SHORTCUTS; i++) {
document.querySelector("#open_container_"+i)
.addEventListener("change", storeShortcutChoice);
}

35
src/js/pageAction.js Normal file
View file

@ -0,0 +1,35 @@
async function init() {
const fragment = document.createDocumentFragment();
const identities = await browser.contextualIdentities.query({});
identities.forEach(identity => {
const tr = document.createElement("tr");
tr.classList.add("menu-item", "hover-highlight");
const td = document.createElement("td");
td.innerHTML = Utils.escaped`
<div class="menu-icon">
<div class="usercontext-icon"
data-identity-icon="${identity.icon}"
data-identity-color="${identity.color}">
</div>
</div>
<span class="menu-text">${identity.name}</span>`;
tr.appendChild(td);
fragment.appendChild(tr);
Utils.addEnterHandler(tr, async () => {
Utils.alwaysOpenInContainer(identity);
window.close();
});
});
const list = document.querySelector("#picker-identities-list");
list.innerHTML = "";
list.appendChild(fragment);
}
init();

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,11 @@
const DEFAULT_FAVICON = "/img/blank-favicon.svg";
// TODO use export here instead of globals
window.Utils = {
const Utils = {
createFavIconElement(url) {
const imageElement = document.createElement("img");
imageElement.classList.add("icon", "offpage");
imageElement.classList.add("icon", "offpage", "menu-icon");
imageElement.src = url;
const loadListener = (e) => {
e.target.classList.remove("offpage");
@ -54,6 +54,117 @@ window.Utils = {
}
}
/**
* Escapes any occurances of &, ", <, > or / with XML entities.
*
* @param {string} str
* The string to escape.
* @return {string} The escaped string.
*/
escapeXML(str) {
const replacements = { "&": "&amp;", "\"": "&quot;", "'": "&apos;", "<": "&lt;", ">": "&gt;", "/": "&#x2F;" };
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.
*/
escaped(strings, ...values) {
const result = [];
for (const [i, string] of strings.entries()) {
result.push(string);
if (i < values.length)
result.push(this.escapeXML(values[i]));
}
return result.join("");
},
async currentTab() {
const activeTabs = await browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT });
if (activeTabs.length > 0) {
return activeTabs[0];
}
return false;
},
addEnterHandler(element, handler) {
element.addEventListener("click", (e) => {
handler(e);
});
element.addEventListener("keydown", (e) => {
if (e.keyCode === 13) {
e.preventDefault();
handler(e);
}
});
},
addEnterOnlyHandler(element, handler) {
element.addEventListener("keydown", (e) => {
if (e.keyCode === 13) {
e.preventDefault();
handler(e);
}
});
},
userContextId(cookieStoreId = "") {
const userContextId = cookieStoreId.replace("firefox-container-", "");
return (userContextId !== cookieStoreId) ? Number(userContextId) : false;
},
setOrRemoveAssignment(tabId, url, userContextId, value) {
return browser.runtime.sendMessage({
method: "setOrRemoveAssignment",
tabId,
url,
userContextId,
value
});
},
async reloadInContainer(url, currentUserContextId, newUserContextId, tabIndex, active) {
return await browser.runtime.sendMessage({
method: "reloadInContainer",
url,
currentUserContextId,
newUserContextId,
tabIndex,
active
});
},
async alwaysOpenInContainer(identity) {
const currentTab = await this.currentTab();
const assignedUserContextId = this.userContextId(identity.cookieStoreId);
if (currentTab.cookieStoreId !== identity.cookieStoreId) {
return await browser.runtime.sendMessage({
method: "assignAndReloadInContainer",
url: currentTab.url,
currentUserContextId: false,
newUserContextId: assignedUserContextId,
tabIndex: currentTab.index +1,
active:currentTab.active
});
}
await Utils.setOrRemoveAssignment(
currentTab.id,
currentTab.url,
assignedUserContextId,
false
);
}
};
// The following creates a fake (but convincing) constant Utils.DEFAULT_PROXY
@ -65,3 +176,5 @@ Object.defineProperty(window.Utils, "DEFAULT_PROXY", {
// Setting configurable to false avoids deletion of Utils.DEFAULT_PROXY
configurable: false
});
window.Utils = Utils;

View file

@ -1,23 +1,20 @@
{
"manifest_version": 2,
"name": "Firefox Multi-Account Containers",
"version": "6.1.1",
"version": "7.1.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": "57.0"
"strict_min_version": "67.0"
}
},
"homepage_url": "https://github.com/mozilla/multi-account-containers#readme",
"permissions": [
"<all_urls>",
"activeTab",
@ -33,7 +30,9 @@
"webRequest",
"proxy"
],
"optional_permissions": [
"bookmarks"
],
"commands": {
"_execute_browser_action": {
"suggested_key": {
@ -41,35 +40,111 @@
"mac": "MacCtrl+Period"
},
"description": "Open containers panel"
},
"open_container_0": {
"suggested_key": {
"default": "Ctrl+Shift+1"
},
"description": "Container Shortcut 1"
},
"open_container_1": {
"suggested_key": {
"default": "Ctrl+Shift+2"
},
"description": "Container Shortcut 2"
},
"open_container_2": {
"suggested_key": {
"default": "Ctrl+Shift+3"
},
"description": "Container Shortcut 3"
},
"open_container_3": {
"suggested_key": {
"default": "Ctrl+Shift+4"
},
"description": "Container Shortcut 4"
},
"open_container_4": {
"suggested_key": {
"default": "Ctrl+Shift+5"
},
"description": "Container Shortcut 5"
},
"open_container_5": {
"suggested_key": {
"default": "Ctrl+Shift+6"
},
"description": "Container Shortcut 6"
},
"open_container_6": {
"suggested_key": {
"default": "Ctrl+Shift+7"
},
"description": "Container Shortcut 7"
},
"open_container_7": {
"suggested_key": {
"default": "Ctrl+Shift+8"
},
"description": "Container Shortcut 8"
},
"open_container_8": {
"suggested_key": {
"default": "Ctrl+Shift+9"
},
"description": "Container Shortcut 9"
},
"open_container_9": {
"suggested_key": {
"default": "Ctrl+Shift+0"
},
"description": "Container Shortcut 10"
}
},
"browser_action": {
"browser_style": true,
"default_icon": "img/container-site.svg",
"default_icon": "img/multiaccountcontainer-16.svg",
"default_title": "Multi-Account Containers",
"default_popup": "popup.html",
"theme_icons": [{
"light": "img/container-site-light.svg",
"dark": "img/container-site.svg",
"theme_icons": [
{
"light": "img/multiaccountcontainer-16-dark.svg",
"dark": "img/multiaccountcontainer-16.svg",
"size": 32
}]
}
]
},
"page_action": {
"browser_style": true,
"default_icon": "img/container-openin-16.svg",
"default_title": "Always open this in a Container",
"default_popup": "pageActionPopup.html",
"pinned": false,
"show_matches": ["*://*/*"]
},
"background": {
"page": "js/background/index.html"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["js/content-script.js"],
"css": ["css/content.css"],
"matches": [
"<all_urls>"
],
"js": [
"js/content-script.js"
],
"css": [
"css/content.css"
],
"run_at": "document_start"
}
],
"web_accessible_resources": [
"/img/container-site-d-24.png"
]
],
"options_ui": {
"page": "options.html",
"browser_style": true
}
}

88
src/options.html Normal file
View file

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="css/options.css">
</head>
<body>
<form>
<h3>Optional Permissions:</h3>
<label>
<input type="checkbox" id="bookmarksPermissions">
Enable Bookmark Menus
</label>
<p><em>This setting allows you to open a bookmark or folder of bookmarks in a container.</em></p>
<h3>Firefox Accounts Sync:</h3>
<label>
<input type="checkbox" id="syncCheck">
Enable Sync
</label>
<p><em>This setting allows you to sync your containers and site assignments across devices.</em></p>
<h3>Tab behaviour:</h3>
<label>
<input type="checkbox" id="replaceTabCheck">
Replace tab instead of creating a new one
</label>
<p><em>Replace the current tab if a page which is assigned to another container is opened (instead of keeping the current tab open).
Opening tabs with middle mouse button is not affected.</em></p>
<h3>Keyboard Shortcuts:</h3>
<p><em>Edit which container is opened when using the numbered shortcuts.</em></p>
<p><label>
Container to open with Keyboard Shortcut 1
<select id="open_container_0">
</select>
</label></p>
<p><label>
Container to open with Keyboard Shortcut 2
<select id="open_container_1">
</select>
</label></p>
<p><label>
Container to open with Keyboard Shortcut 3
<select id="open_container_2">
</select>
</label></p>
<p><label>
Container to open with Keyboard Shortcut 4
<select id="open_container_3">
</select>
</label></p>
<p><label>
Container to open with Keyboard Shortcut 5
<select id="open_container_4">
</select>
</label></p>
<p><label>
Container to open with Keyboard Shortcut 6
<select id="open_container_5">
</select>
</label></p>
<p><label>
Container to open with Keyboard Shortcut 7
<select id="open_container_6">
</select>
</label></p>
<p><label>
Container to open with Keyboard Shortcut 8
<select id="open_container_7">
</select>
</label></p>
<p><label>
Container to open with Keyboard Shortcut 9
<select id="open_container_8">
</select>
</label></p>
<p><label>
Container to open with Keyboard Shortcut 10
<select id="open_container_9">
</select>
</label></p>
<h3>Onboarding:</h3>
<button>Reset Onboarding Panels</button>
<p><em>Toggle this to see the onboarding panels again.</em></p>
</form>
<script src="js/options.js"></script>
</body>
</html>

34
src/pageActionPopup.html Normal file
View file

@ -0,0 +1,34 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Multi-Account Containers</title>
<link rel="stylesheet" type="text/css" href="css/popup.css">
</head>
<body>
<div class="page-action-container-picker" id="container-picker-panel">
<h3 class="title">
Always Open this Site in...
</h3>
<hr>
<div class="scrollable identities-list">
<table class="menu" id="picker-identities-list">
<tr class="menu-item hover-highlight">
<td>
<div class="menu-icon">
<div class="usercontext-icon"
data-identity-icon="pet"
data-identity-color="blue">
</div>
</div>
<span class="menu-text">Default</span>
</td>
</tr>
</table>
</div>
</div>
<script src="js/utils.js"></script>
<script src="js/pageAction.js"></script>
</body>
</html>

View file

@ -13,7 +13,7 @@
<p>
Use containers to organize tasks, manage accounts, and keep your focus where you want it.
</p>
<a href="#" class="onboarding-button onboarding-start-button">Get Started</a>
<a href="#" class="onboarding-button onboarding-start-button keyboard-nav" tabindex="0">Get Started</a>
</div>
<div class="hide panel onboarding security-onboarding-panel-1">
@ -22,49 +22,69 @@
<p>
Use containers to organize tasks, manage accounts, and store sensitive data.
</p>
<a href="#" class="onboarding-button onboarding-start-button">Get Started</a>
<a href="#" class="onboarding-button onboarding-start-button keyboard-nav" tabindex="0">Get Started</a>
</div>
<div class="panel onboarding onboarding-panel-2 hide">
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-2.png" />
<h3 class="onboarding-title">Put containers to work for you.</h3>
<p>Features like color-coding and separate container tabs help you find things easily, focus your attention, and minimize distractions.</p>
<a href="#" class="onboarding-button onboarding-next-button">Next</a>
<a href="#" class="onboarding-button onboarding-next-button keyboard-nav" tabindex="0">Next</a>
</div>
<div class="panel onboarding security-onboarding-panel-2 hide">
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-2.png" />
<h3 class="onboarding-title">Put containers to work for you.</h3>
<p>Color-coding helps you categorize your online life, find things easily, and minimize distractions.</p>
<a href="#" class="onboarding-button onboarding-next-button">Next</a>
<a href="#" class="onboarding-button onboarding-next-button keyboard-nav" tabindex="0">Next</a>
</div>
<div class="panel onboarding onboarding-panel-3 hide">
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3.png" />
<h3 class="onboarding-title">A place for everything, and everything in its place.</h3>
<p>Start with the containers we've created, or create your own.</p>
<a href="#" class="onboarding-button onboarding-almost-done-button">Next</a>
<a href="#" class="onboarding-button onboarding-almost-done-button keyboard-nav" tabindex="0">Next</a>
</div>
<div class="panel onboarding security-onboarding-panel-3 hide">
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3-security.png" />
<h3 class="onboarding-title">Set boundaries for your browsing.</h3>
<p>Cookies are stored within a container, so you can segment sensitive data and browsing history to stay organized and to limit the impact of online trackers.</p>
<a href="#" class="onboarding-button onboarding-almost-done-button">Next</a>
<a href="#" class="onboarding-button onboarding-almost-done-button keyboard-nav" tabindex="0">Next</a>
</div>
<div class="panel onboarding onboarding-panel-4 hide" id="onboarding-panel-4">
<img class="onboarding-img" alt="How to assign sites to containers" src="/img/onboarding-4.png" />
<h3 class="onboarding-title">Always open sites in the containers you want.</h3>
<p>Right-click inside a container tab to assign the site to always open in the container.</p>
<a href="#" id="onboarding-done-button" class="onboarding-button">Next</a>
<a href="#" id="onboarding-done-button" class="onboarding-button keyboard-nav" tabindex="0">Next</a>
</div>
<div class="panel onboarding onboarding-panel-5 hide" id="onboarding-panel-5">
<img class="onboarding-img" alt="Long-press the New Tab button to create a new container tab." src="/img/onboarding-3.png" />
<h3 class="onboarding-title">Container tabs when you need them.</h3>
<p>Long-press the New Tab button to create a new container tab.</p>
<a href="#" id="onboarding-longpress-button" class="onboarding-button">Done</a>
<a href="#" id="onboarding-longpress-button" class="onboarding-button keyboard-nav" tabindex="0">Next</a>
</div>
<div class="panel onboarding onboarding-panel-6 hide" id="onboarding-panel-6">
<img class="onboarding-img" alt="Syncing Containers is now Available!" src="/img/Sync.svg" />
<h3 class="onboarding-title">Syncing Containers is now Available!</h3>
<p>Turn on Sync to share container and site assignments with any computer connected to your Firefox Account.</p>
<div class="half-button-wrapper">
<a herf="#" id="no-sync" class="half-onboarding-button grey-button keyboard-nav" tabindex="0">Not Now</a>
<a href="#" id="start-sync-button" class="half-onboarding-button keyboard-nav" tabindex="0">Start Syncing</a>
</div>
</div>
<div class="panel onboarding onboarding-panel-7 hide" id="onboarding-panel-7">
<img class="onboarding-img" alt="Firefox Account is required to sync" src="/img/Account.svg" />
<h3 class="onboarding-title">Firefox Account is required to sync.</h3>
<p>Click Sign In to confirm that your Firefox Account is active.</p>
<div class="half-button-wrapper">
<a herf="#" id="no-sign-in" class="half-onboarding-button grey-button keyboard-nav" tabindex="0">Not Now</a>
<a href="#" id="sign-in" class="half-onboarding-button keyboard-nav" tabindex="0">Sign In</a>
</div>
</div>
<div class="panel achievement-panel hide" id="achievement-panel">
@ -92,123 +112,247 @@
</span>
</a>
</p>
<a href="#" id="achievement-done-button" class="onboarding-button">Done</a>
<a href="#" id="achievement-done-button" class="onboarding-button keyboard-nav">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" />
<div class="panel menu-panel container-panel hide" id="container-panel">
<h3 class="title">
Multi-Account Containers
</h3>
<a href="#" class="info-icon" id="info-icon" tabindex="10">
<img alt="info" src="/img/info-thin-16.svg" / >
</a>
<hr>
<table class="menu">
<tr class="menu-item hover-highlight keyboard-nav" id="open-new-tab-in" tabindex="0">
<td>
<img class="menu-icon" alt="Open in New Tab" src="/img/tab-new-16.svg" />
<span class="menu-text">Open New Tab in...</span>
<span class="menu-arrow">
<img alt="Container Info" src="/img/arrow-icon-right.svg" />
</span>
</td>
</tr>
<tr class="menu-item hover-highlight keyboard-nav" id="reopen-site-in" tabindex="0">
<td>
<img class="menu-icon" alt="Open in New Tab" src="/img/refresh-16.svg" />
<span class="menu-text">Reopen This Site in...</span>
<span class="menu-arrow">
<img alt="Container Info" src="/img/arrow-icon-right.svg" />
</span>
</td>
</tr>
</table>
<hr>
<table class="menu">
<tr class="menu-item hover-highlight keyboard-nav" id="sort-containers-link" tabindex="0">
<td>
<img class="menu-icon" alt="Open in New Tab" src="/img/sort-16_1.svg" />
<span class="menu-text">Sort Tabs by Container</span>
<span class="menu-arrow">
</span>
</td>
</tr>
<tr class="menu-item hover-highlight keyboard-nav" id="always-open-in" tabindex="0">
<td>
<img class="menu-icon" alt="Open in New Tab" src="/img/container-openin-16.svg" />
<span class="menu-text">Always Open This Site in...</span>
<span class="menu-arrow">
<img alt="Container Info" src="/img/arrow-icon-right.svg" />
</span>
</td>
</tr>
</table>
<hr>
<div class="sub-header">
Containers
</div>
<div class="scrollable identities-list">
<table class="menu" id="identities-list">
<tr class="menu-item hover-highlight">
<td>
<div class="menu-item-name">
<div class="menu-icon">
<div class="usercontext-icon"
data-identity-icon="pet"
data-identity-color="blue"></div>
</div>
<span class="menu-text">Default</span>
</div>
<span class="menu-right-float">
<span class="container-count">22</span>
<span class="menu-arrow">
<img alt="Container Info" src="/img/arrow-icon-right.svg" />
</span>
</span>
</td>
</tr>
</table>
</div>
<div class="bottom-btn keyboard-nav hover-highlight" id="manage-containers-link" tabindex="0">
Manage Containers
</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 class="hide panel menu-panel container-info-panel" id="container-info-panel" tabindex="-1">
<h3 class="title" id="container-info-title">
Personal
</h3>
<button class="btn-return arrow-left keyboard-nav-back" id="close-container-info-panel" tabindex="0"></button>
<hr>
<table class="menu">
<tr class="menu-item hover-highlight keyboard-nav" id="open-new-tab-in-info" tabindex="0">
<td>
<img class="menu-icon" alt="Open in New Tab" src="/img/tab-new-16.svg" />
<span class="menu-text">Open New Tab in this Container</span>
<span class="menu-arrow">
</span>
</td>
</tr>
<tr class="menu-item hover-highlight keyboard-nav" id="hideorshow-container" tabindex="0">
<td>
<img id="container-info-hideorshow-icon" class="menu-icon" alt="Hide This Container" src="img/password-hide.svg" />
<span id="container-info-hideorshow-label" class="menu-text">Hide This Container</span>
<span class="menu-arrow">
</span>
</td>
</tr>
<tr class="menu-item hover-highlight keyboard-nav" id="move-to-new-window" tabindex="0">
<td>
<img class="menu-icon" alt="Move Tabs to a New Window" src="/img/movetowindow-16.svg" />
<span class="menu-text">Move Tabs to a New Window</span>
<span class="menu-arrow">
</span>
</td>
</tr>
<tr class="menu-item hover-highlight keyboard-nav" id="always-open" tabindex="0">
<td>
<img class="menu-icon" alt="Always Open Site in Container" src="/img/container-openin-16.svg" />
<span class="menu-text" id="always-open-in-info-panel">Always Open Site in Container</span>
<span class="menu-arrow">
</span>
</td>
</tr>
</table>
<hr>
<div class="sub-header">
Open Tabs
</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 class="menu" id="container-info-table">
<tr class="menu-item hover-highlight keyboard-nav" tabindex="0">
<td>
<div class="favicon"><img class="menu-icon" src="https://www.mozilla.org/favicon.ico" /></div>
<span class="menu-text truncate-text">www.mozillllllllllllllllllllllllllllllllllllla.org</span>
<img class="trash-button" src="/img/container-close-tab.svg" />
</td>
</tr>
</table>
</div>
<div class="bottom-btn keyboard-nav hover-highlight" id="manage-container-link" tabindex="0">
Manage This Container
</div>
</div>
<div class="panel menu-panel container-picker-panel hide" id="container-picker-panel">
<h3 class="title" id="picker-title">
Multi-Account Containers
</h3>
<button class="btn-return arrow-left keyboard-nav-back" id="close-container-picker-panel" tabindex="0"></button>
<hr>
<div id="new-container-div"></div>
<div class="scrollable identities-list">
<table class="menu" id="picker-identities-list">
<tr class="menu-item hover-highlight keyboard-nav">
<td>
<div class="menu-icon">
<div class="usercontext-icon"
data-identity-icon="pet"
data-identity-color="blue">
</div>
</div>
<span class="menu-text">Default</span>
</td>
</tr>
</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">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">
<div class="panel menu-panel edit-container-panel hide" id="edit-container-panel">
<h3 class="title" id="container-edit-title">
Default
</h3>
<button class="btn-return arrow-left" id="close-container-edit-panel"></button>
<hr>
<div class="scrollable edit-form">
<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"/>
<legend class="form-header">Name</legend>
<input type="text" name="container-name" id="edit-container-panel-name-input" class="edit-container-panel-name-input" maxlength="25"/>
</fieldset>
<fieldset id="edit-container-panel-choose-color" class="radio-choice">
<legend>Choose a color</legend>
<legend class="form-header">Color</legend>
</fieldset>
<fieldset id="edit-container-panel-choose-icon" class="radio-choice">
<legend>Choose an icon</legend>
<legend class="form-header">Icon</legend>
</fieldset>
<fieldset>
<legend>HTTP Proxy (Optional)</legend>
<input type="text" name="container-proxy" id="edit-container-panel-proxy" maxlength="15" placeholder="IP:PORT or USER:PASS@IP:PORT"/>
</fieldset>
</form>
<div id="edit-sites-assigned" class="scrollable" hidden>
<h3>Sites assigned to this container</h3>
<div class="assigned-sites-list">
<div id="edit-container-options">
<div class="options-header">Options</div>
<div class="container-options">
<input type="checkbox" class="site-isolation" id="site-isolation" name="site-isolation">
<label for="site-isolation" class="options-label">Limit to Designated Sites</label>
</div>
<div class="container-options options-label manage-assigned-sites-list" id="manage-assigned-sites-list" tabindex="0">Manage Site List...
</div>
</div>
</div>
<div class="delete-container">
<button class="delete-btn" id="delete-container-button">Delete This Container</button>
</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>
<a href="#" class="button expanded secondary footer-button cancel-button" id="create-container-cancel-link">Cancel</a>
<a href="#" class="button expanded primary footer-button" id="create-container-ok-link">OK</a>
</div>
</div>
<div class="panel menu-panel edit-container-assignments hide" id="edit-container-assignments">
<h3 class="title" id="edit-assignments-title">
Default
</h3>
<button class="btn-return arrow-left" id="close-container-assignment-panel"></button>
<hr>
<div class="scrollable edit-sites-assigned">
<div class="sub-header">Sites assigned to this container</div>
<table class="menu scrollable" id="edit-sites-assigned">
<tr class="menu-item hover-highlight" tabindex="0">
<td>
<div class="favicon"><img class="menu-icon" src="https://www.mozilla.org/favicon.ico" /></div>
<span class="menu-text truncate-text">www.mozillllllllllllllllllllllllllllla.org</span>
<img class="trash-button" src="/img/container-delete.svg" />
</td>
</tr>
</table>
</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>
<h3 class="title" id="container-delete-title">
Default
</h3>
<button class="btn-return arrow-left" id="close-container-delete-panel"></button>
<hr>
<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>
<p class="delete-warning" id="delete-container-tab-warning"></p>
<p class="delete-warning">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>

View file

@ -3,14 +3,10 @@ module.exports = {
"node": true,
"mocha": true
},
globals: {
"sinon": false,
"expect": false,
"nextTick": false,
"buildBackgroundDom": false,
"background": false,
"buildPopupDom": false,
"popup": false,
"helper": false
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"no-restricted-globals": ["error", "browser"]
}
}

View file

@ -1,137 +0,0 @@
module.exports = () => {
const _storage = {};
// could maybe be replaced by https://github.com/acvetkov/sinon-chrome
const browserMock = {
_storage,
runtime: {
onMessage: {
addListener: sinon.stub(),
},
onMessageExternal: {
addListener: sinon.stub(),
},
sendMessage: sinon.stub().resolves(),
},
webRequest: {
onBeforeRequest: {
addListener: sinon.stub()
},
onCompleted: {
addListener: sinon.stub()
},
onErrorOccurred: {
addListener: sinon.stub()
}
},
windows: {
getCurrent: sinon.stub().resolves({}),
onFocusChanged: {
addListener: sinon.stub(),
}
},
tabs: {
onActivated: {
addListener: sinon.stub()
},
onCreated: {
addListener: sinon.stub()
},
onUpdated: {
addListener: sinon.stub()
},
sendMessage: sinon.stub(),
query: sinon.stub().resolves([{}]),
get: sinon.stub(),
create: sinon.stub().resolves({}),
remove: sinon.stub().resolves()
},
history: {
deleteUrl: sinon.stub()
},
storage: {
local: {
get: sinon.stub(),
set: sinon.stub()
}
},
contextualIdentities: {
create: sinon.stub(),
get: sinon.stub(),
query: sinon.stub().resolves([])
},
contextMenus: {
create: sinon.stub(),
remove: sinon.stub(),
onClicked: {
addListener: sinon.stub()
}
},
browserAction: {
setBadgeBackgroundColor: sinon.stub(),
setBadgeText: sinon.stub()
},
management: {
get: sinon.stub(),
onInstalled: {
addListener: sinon.stub()
},
onUninstalled: {
addListener: sinon.stub()
}
},
extension: {
getURL: sinon.stub().returns("moz-extension://multi-account-containers/confirm-page.html")
}
};
// inmemory local storage
browserMock.storage.local = {
get: sinon.spy(async key => {
if (!key) {
return _storage;
}
let result = {};
if (Array.isArray(key)) {
key.map(akey => {
if (typeof _storage[akey] !== "undefined") {
result[akey] = _storage[akey];
}
});
} else if (typeof key === "object") {
// TODO support nested objects
Object.keys(key).map(oKey => {
if (typeof _storage[oKey] !== "undefined") {
result[oKey] = _storage[oKey];
} else {
result[oKey] = key[oKey];
}
});
} else {
result = _storage[key];
}
return result;
}),
set: sinon.spy(async (key, value) => {
if (typeof key === "object") {
// TODO support nested objects
Object.keys(key).map(oKey => {
_storage[oKey] = key[oKey];
});
} else {
_storage[key] = value;
}
}),
remove: sinon.spy(async (key) => {
if (Array.isArray(key)) {
key.map(aKey => {
delete _storage[aKey];
});
} else {
delete _storage[key];
}
}),
};
return browserMock;
};

112
test/common.js Normal file
View file

@ -0,0 +1,112 @@
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,
"replaceTabEnabled": false
});
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,
};

View file

@ -1,39 +1,78 @@
describe("Assignment Feature", () => {
const activeTab = {
id: 1,
cookieStoreId: "firefox-container-1",
url: "http://example.com",
index: 0
};
beforeEach(async () => {
await helper.browser.initializeWithTab(activeTab);
});
const {initializeWithTab} = require("../common");
describe("click the 'Always open in' checkbox in the popup", () => {
beforeEach(async () => {
// popup click to set assignment for activeTab.url
await helper.popup.clickElementById("container-page-assigned");
});
describe("Assignment Reopen Feature", function () {
const url = "http://example.com";
describe("open new Tab with the assigned URL in the default container", () => {
const newTab = {
id: 2,
beforeEach(async function () {
this.webExt = await initializeWithTab({
cookieStoreId: "firefox-default",
url: activeTab.url,
index: 1,
active: true
};
beforeEach(async () => {
// new Tab opening activeTab.url in default container
await helper.browser.openNewTab(newTab);
url
});
});
it("should open the confirm page", async () => {
afterEach(function () {
this.webExt.destroy();
});
describe("set to 'Always open in' firefox-container-4", function () {
beforeEach(async function () {
// popup click to set assignment for activeTab.url
await this.webExt.popup.helper.clickElementById("always-open-in");
await this.webExt.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item");
});
it("should open the page in the assigned container", async function () {
// should have created a new tab with the confirm page
background.browser.tabs.create.should.have.been.calledWith({
url: "moz-extension://multi-account-containers/confirm-page.html?" +
`url=${encodeURIComponent(activeTab.url)}` +
`&cookieStoreId=${activeTab.cookieStoreId}`,
this.webExt.background.browser.tabs.create.should.have.been.calledWithMatch({
active: true,
cookieStoreId: "firefox-container-4",
index: 1,
openerTabId: null,
url: "http://example.com"
});
});
});
});
describe("Assignment Comfirm Page Feature", function () {
const url = "http://example.com";
beforeEach(async function () {
this.webExt = await initializeWithTab({
cookieStoreId: "firefox-container-4",
url
});
});
afterEach(function () {
this.webExt.destroy();
});
describe("open new Tab with the assigned URL in the default container", function () {
let newTab;
beforeEach(async function () {
await this.webExt.popup.helper.clickElementById("always-open-in");
await this.webExt.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item");
// 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,
@ -41,35 +80,8 @@ describe("Assignment Feature", () => {
});
});
it("should remove the new Tab that got opened in the default container", () => {
background.browser.tabs.remove.should.have.been.calledWith(newTab.id);
});
});
describe("click the 'Always open in' checkbox in the popup again", () => {
beforeEach(async () => {
// popup click to remove assignment for activeTab.url
await helper.popup.clickElementById("container-page-assigned");
});
describe("open new Tab with the no longer assigned URL in the default container", () => {
const newTab = {
id: 3,
cookieStoreId: "firefox-default",
url: activeTab.url,
index: 3,
active: true
};
beforeEach(async () => {
// new Tab opening activeTab.url in default container
await helper.browser.openNewTab(newTab);
});
it("should not open the confirm page", async () => {
// should not have created a new tab
background.browser.tabs.create.should.not.have.been.called;
});
});
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);
});
});
});

View file

@ -0,0 +1,36 @@
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("manage-containers-link");
await this.webExt.popup.helper.clickElementById("new-container");
await this.webExt.popup.helper.clickElementById("create-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("manage-containers-link");
await this.webExt.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item", "last");
await this.webExt.popup.helper.clickElementById("delete-container-button");
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;
});
});
});
});

View file

@ -1,67 +1,64 @@
describe("External Webextensions", () => {
const activeTab = {
id: 1,
cookieStoreId: "firefox-container-1",
url: "http://example.com",
index: 0
};
beforeEach(async () => {
await helper.browser.initializeWithTab(activeTab);
await helper.popup.clickElementById("container-page-assigned");
const {expect, initializeWithTab} = require("../common");
describe("External Webextensions", function () {
const url = "http://example.com";
beforeEach(async function () {
this.webExt = await initializeWithTab({
cookieStoreId: "firefox-container-4",
url
});
describe("with contextualIdentities permissions", () => {
it("should be able to get assignments", async () => {
background.browser.management.get.resolves({
await this.webExt.popup.helper.clickElementById("always-open-in");
await this.webExt.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item", "last");
});
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: "http://example.com"
url
};
const sender = {
id: "external-webextension"
};
// currently not possible to get the return value of yielding with sinon
// so we expect that if no error is thrown and the storage was called, everything is ok
// maybe i get around to provide a PR https://github.com/sinonjs/sinon/issues/903
//
// the alternative would be to expose the actual messageHandler and call it directly
// but personally i think that goes against the black-box-ish nature of these feature tests
const rejectionStub = sinon.stub();
process.on("unhandledRejection", rejectionStub);
background.browser.runtime.onMessageExternal.addListener.yield(message, sender);
await nextTick();
process.removeListener("unhandledRejection", rejectionStub);
rejectionStub.should.not.have.been.called;
background.browser.storage.local.get.should.have.been.called;
const [promise] = this.webExt.background.browser.runtime.onMessageExternal.addListener.yield(message, sender);
const answer = await promise;
expect(answer.userContextId === "4").to.be.true;
expect(answer.neverAsk === false).to.be.true;
expect(
Object.prototype.hasOwnProperty.call(
answer, "identityMacAddonUUID")).to.be.true;
});
});
describe("without contextualIdentities permissions", () => {
it("should throw an error", async () => {
background.browser.management.get.resolves({
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: "http://example.com"
url
};
const sender = {
id: "external-webextension"
};
const rejectionStub = sinon.spy();
process.on("unhandledRejection", rejectionStub);
background.browser.runtime.onMessageExternal.addListener.yield(message, sender);
await nextTick();
process.removeListener("unhandledRejection", rejectionStub);
rejectionStub.should.have.been.calledWith(sinon.match({
message: "Missing contextualIdentities permission"
}));
const [promise] = this.webExt.background.browser.runtime.onMessageExternal.addListener.yield(message, sender);
return promise.catch(error => {
expect(error.message).to.equal("Missing contextualIdentities permission");
});
});
});
});

465
test/features/sync.test.js Normal file
View file

@ -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",
}
];

View file

@ -1,47 +0,0 @@
module.exports = {
browser: {
async initializeWithTab(tab) {
await buildBackgroundDom({
beforeParse(window) {
window.browser.tabs.get.resolves(tab);
window.browser.tabs.query.resolves([tab]);
window.browser.contextualIdentities.get.resolves({
cookieStoreId: tab.cookieStoreId
});
}
});
await buildPopupDom({
beforeParse(window) {
window.browser.tabs.get.resolves(tab);
window.browser.tabs.query.resolves([tab]);
}
});
},
async openNewTab(tab, options = {}) {
if (options.resetHistory) {
background.browser.tabs.create.resetHistory();
background.browser.tabs.remove.resetHistory();
}
background.browser.tabs.get.resolves(tab);
background.browser.tabs.onCreated.addListener.yield(tab);
const [promise] = background.browser.webRequest.onBeforeRequest.addListener.yield({
frameId: 0,
tabId: tab.id,
url: tab.url,
requestId: options.requestId
});
return promise;
}
},
popup: {
async clickElementById(id) {
const clickEvent = popup.document.createEvent("HTMLEvents");
clickEvent.initEvent("click");
popup.document.getElementById(id).dispatchEvent(clickEvent);
await nextTick();
}
},
};

38
test/issues/1168.test.js Normal file
View file

@ -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);
});
});
});

View file

@ -1,180 +1,179 @@
describe("#940", () => {
describe("when other onBeforeRequestHandlers are faster and redirect with the same requestId", () => {
it("should not open two confirm pages", async () => {
// init
const activeTab = {
id: 1,
cookieStoreId: "firefox-container-1",
url: "http://example.com",
index: 0
};
await helper.browser.initializeWithTab(activeTab);
// assign the activeTab.url
await helper.popup.clickElementById("container-page-assigned");
const {expect, sinon, initializeWithTab} = require("../common");
// start request and don't await the requests at all
// so the second request below is actually comparable to an actual redirect that also fires immediately
const newTab = {
id: 2,
cookieStoreId: "firefox-default",
url: activeTab.url,
index: 1,
active: true
};
helper.browser.openNewTab(newTab, {
requestId: 1
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-4",
url: "http://example.com"
});
// other addon sees the same request
// and redirects to the https version of activeTab.url
// since it's a redirect the request has the same requestId
background.browser.webRequest.onBeforeRequest.addListener.yield({
frameId: 0,
tabId: newTab.id,
url: "https://example.com",
requestId: 1
});
await nextTick();
await webExtension.popup.helper.clickElementById("always-open-in");
await webExtension.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item");
background.browser.tabs.create.should.have.been.calledOnce;
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", () => {
let promiseResults;
beforeEach(async () => {
// init
const activeTab = {
id: 1,
cookieStoreId: "firefox-container-1",
url: "https://www.youtube.com",
index: 0
};
await helper.browser.initializeWithTab(activeTab);
// assign the activeTab.url
await helper.popup.clickElementById("container-page-assigned");
// http://youtube.com
const newTab = {
id: 2,
cookieStoreId: "firefox-default",
url: "http://youtube.com",
index: 1,
active: true
};
const promise1 = helper.browser.openNewTab(newTab, {
requestId: 1
});
// https://youtube.com
const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({
frameId: 0,
tabId: newTab.id,
url: "https://youtube.com",
requestId: 1
});
// https://www.youtube.com
const [promise3] = background.browser.webRequest.onBeforeRequest.addListener.yield({
frameId: 0,
tabId: newTab.id,
url: "https://www.youtube.com",
requestId: 1
});
// https://www.youtube.com
const [promise4] = background.browser.webRequest.onBeforeRequest.addListener.yield({
frameId: 0,
tabId: newTab.id,
describe("when redirects change requestId midflight", function () {
beforeEach(async function () {
this.webExt = await initializeWithTab({
cookieStoreId: "firefox-container-4",
url: "https://www.youtube.com"
});
await this.webExt.popup.helper.clickElementById("always-open-in");
await this.webExt.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item");
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
});
promiseResults = await Promise.all([promise1, promise2, promise3, promise4]);
return [newTabResponses, newTab];
};
});
it("should not open two confirm pages", async () => {
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(promiseResults[0]).to.deep.equal({});
expect(await newTabResponses.webRequest.onBeforeRequest[0]).to.deep.equal({});
// https://youtube.com is not assigned, no cancel, no reopening
expect(promiseResults[1]).to.deep.equal({});
expect(await newTabResponses.webRequest.onBeforeRequest[1]).to.deep.equal({});
// https://www.youtube.com is assigned, this triggers reopening, cancel
expect(promiseResults[2]).to.deep.equal({
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(promiseResults[3]).to.deep.equal({
expect(await newTabResponses.webRequest.onBeforeRequest[3]).to.deep.equal({
cancel: true
});
background.browser.tabs.create.should.have.been.calledOnce;
this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
});
it("should uncancel after webRequest.onCompleted", async () => {
const [promise1] = background.browser.webRequest.onCompleted.addListener.yield({
tabId: 2
});
await promise1;
const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({
frameId: 0,
tabId: 2,
url: "https://www.youtube.com",
requestId: 123
});
await promise2;
background.browser.tabs.create.should.have.been.calledTwice;
it("should uncancel after webRequest.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
}
}
});
it("should uncancel after webRequest.onErrorOccurred", async () => {
const [promise1] = background.browser.webRequest.onErrorOccurred.addListener.yield({
tabId: 2
});
await promise1;
// request to assigned url in same tab
const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({
frameId: 0,
tabId: 2,
url: "https://www.youtube.com",
requestId: 123
});
await promise2;
background.browser.tabs.create.should.have.been.calledTwice;
this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
});
it("should uncancel after 2 seconds", async () => {
await new Promise(resolve => setTimeout(resolve, 2000));
// request to assigned url in same tab
const [promise2] = background.browser.webRequest.onBeforeRequest.addListener.yield({
frameId: 0,
tabId: 2,
url: "https://www.youtube.com",
requestId: 123
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
}
});
await promise2;
background.browser.tabs.create.should.have.been.calledTwice;
}).timeout(2002);
this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
});
it("should not influence the canceled url in other tabs", async () => {
const newTab = {
id: 123,
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",
index: 10,
active: true
};
await helper.browser.openNewTab(newTab, {
requestId: 321
url: "https://www.youtube.com"
}, {
options: {
webRequestError: true
}
});
background.browser.tabs.create.should.have.been.calledTwice;
this.webExt.background.browser.tabs.create.should.have.been.calledOnce;
});
});
});

View file

@ -1,101 +0,0 @@
if (!process.listenerCount("unhandledRejection")) {
// eslint-disable-next-line no-console
process.on("unhandledRejection", r => console.log(r));
}
const jsdom = require("jsdom");
const path = require("path");
const chai = require("chai");
const sinonChai = require("sinon-chai");
global.sinon = require("sinon");
global.expect = chai.expect;
chai.should();
chai.use(sinonChai);
global.nextTick = () => {
return new Promise(resolve => {
setTimeout(() => {
process.nextTick(resolve);
});
});
};
global.helper = require("./helper");
const browserMock = require("./browser.mock");
const srcBasePath = path.resolve(path.join(__dirname, "..", "src"));
const srcJsBackgroundPath = path.join(srcBasePath, "js", "background");
global.buildBackgroundDom = async (options = {}) => {
const dom = await jsdom.JSDOM.fromFile(path.join(srcJsBackgroundPath, "index.html"), {
runScripts: "dangerously",
resources: "usable",
virtualConsole: (new jsdom.VirtualConsole).sendTo(console),
beforeParse(window) {
window.browser = browserMock();
window.fetch = sinon.stub().resolves({
json: sinon.stub().resolves({})
});
if (options.beforeParse) {
options.beforeParse(window);
}
}
});
await new Promise(resolve => {
dom.window.document.addEventListener("DOMContentLoaded", resolve);
});
await nextTick();
global.background = {
dom,
browser: dom.window.browser
};
};
global.buildPopupDom = async (options = {}) => {
const dom = await jsdom.JSDOM.fromFile(path.join(srcBasePath, "popup.html"), {
runScripts: "dangerously",
resources: "usable",
virtualConsole: (new jsdom.VirtualConsole).sendTo(console),
beforeParse(window) {
window.browser = browserMock();
window.browser.storage.local.set("browserActionBadgesClicked", []);
window.browser.storage.local.set("onboarding-stage", 5);
window.browser.storage.local.set("achievements", []);
window.browser.storage.local.set.resetHistory();
window.fetch = sinon.stub().resolves({
json: sinon.stub().resolves({})
});
if (options.beforeParse) {
options.beforeParse(window);
}
}
});
await new Promise(resolve => {
dom.window.document.addEventListener("DOMContentLoaded", resolve);
});
await nextTick();
dom.window.browser.runtime.sendMessage.resetHistory();
if (global.background) {
dom.window.browser.runtime.sendMessage = sinon.spy(function() {
global.background.browser.runtime.onMessage.addListener.yield(...arguments);
});
}
global.popup = {
dom,
document: dom.window.document,
browser: dom.window.browser
};
};
global.afterEach(() => {
if (global.background) {
global.background.dom.window.close();
delete global.background;
}
if (global.popup) {
global.popup.dom.window.close();
delete global.popup;
}
});