├── .env ├── icon.png ├── src ├── img │ ├── flags │ │ ├── AC.png │ │ ├── AD.png │ │ ├── AE.png │ │ ├── AF.png │ │ ├── AG.png │ │ ├── AI.png │ │ ├── AL.png │ │ ├── AM.png │ │ ├── AO.png │ │ ├── AQ.png │ │ ├── AR.png │ │ ├── AS.png │ │ ├── AT.png │ │ ├── AU.png │ │ ├── AW.png │ │ ├── AX.png │ │ ├── AZ.png │ │ ├── BA.png │ │ ├── BB.png │ │ ├── BD.png │ │ ├── BE.png │ │ ├── BF.png │ │ ├── BG.png │ │ ├── BH.png │ │ ├── BI.png │ │ ├── BJ.png │ │ ├── BL.png │ │ ├── BM.png │ │ ├── BN.png │ │ ├── BO.png │ │ ├── BQ.png │ │ ├── BR.png │ │ ├── BS.png │ │ ├── BT.png │ │ ├── BV.png │ │ ├── BW.png │ │ ├── BY.png │ │ ├── BZ.png │ │ ├── CA.png │ │ ├── CC.png │ │ ├── CD.png │ │ ├── CF.png │ │ ├── CG.png │ │ ├── CH.png │ │ ├── CI.png │ │ ├── CK.png │ │ ├── CL.png │ │ ├── CM.png │ │ ├── CN.png │ │ ├── CO.png │ │ ├── CP.png │ │ ├── CR.png │ │ ├── CU.png │ │ ├── CV.png │ │ ├── CW.png │ │ ├── CX.png │ │ ├── CY.png │ │ ├── CZ.png │ │ ├── DE.png │ │ ├── DG.png │ │ ├── DJ.png │ │ ├── DK.png │ │ ├── DM.png │ │ ├── DO.png │ │ ├── DZ.png │ │ ├── EA.png │ │ ├── EC.png │ │ ├── EE.png │ │ ├── EG.png │ │ ├── EH.png │ │ ├── ER.png │ │ ├── ES.png │ │ ├── ET.png │ │ ├── EU.png │ │ ├── FI.png │ │ ├── FJ.png │ │ ├── FK.png │ │ ├── FM.png │ │ ├── FO.png │ │ ├── FR.png │ │ ├── GA.png │ │ ├── GB.png │ │ ├── GD.png │ │ ├── GE.png │ │ ├── GF.png │ │ ├── GG.png │ │ ├── GH.png │ │ ├── GI.png │ │ ├── GL.png │ │ ├── GM.png │ │ ├── GN.png │ │ ├── GP.png │ │ ├── GQ.png │ │ ├── GR.png │ │ ├── GS.png │ │ ├── GT.png │ │ ├── GU.png │ │ ├── GW.png │ │ ├── GY.png │ │ ├── HK.png │ │ ├── HM.png │ │ ├── HN.png │ │ ├── HR.png │ │ ├── HT.png │ │ ├── HU.png │ │ ├── IC.png │ │ ├── ID.png │ │ ├── IE.png │ │ ├── IL.png │ │ ├── IM.png │ │ ├── IN.png │ │ ├── IO.png │ │ ├── IQ.png │ │ ├── IR.png │ │ ├── IS.png │ │ ├── IT.png │ │ ├── JE.png │ │ ├── JM.png │ │ ├── JO.png │ │ ├── JP.png │ │ ├── KE.png │ │ ├── KG.png │ │ ├── KH.png │ │ ├── KI.png │ │ ├── KM.png │ │ ├── KN.png │ │ ├── KP.png │ │ ├── KR.png │ │ ├── KW.png │ │ ├── KY.png │ │ ├── KZ.png │ │ ├── LA.png │ │ ├── LB.png │ │ ├── LC.png │ │ ├── LI.png │ │ ├── LK.png │ │ ├── LR.png │ │ ├── LS.png │ │ ├── LT.png │ │ ├── LU.png │ │ ├── LV.png │ │ ├── LY.png │ │ ├── MA.png │ │ ├── MC.png │ │ ├── MD.png │ │ ├── ME.png │ │ ├── MF.png │ │ ├── MG.png │ │ ├── MH.png │ │ ├── MK.png │ │ ├── ML.png │ │ ├── MM.png │ │ ├── MN.png │ │ ├── MO.png │ │ ├── MP.png │ │ ├── MQ.png │ │ ├── MR.png │ │ ├── MS.png │ │ ├── MT.png │ │ ├── MU.png │ │ ├── MV.png │ │ ├── MW.png │ │ ├── MX.png │ │ ├── MY.png │ │ ├── MZ.png │ │ ├── NA.png │ │ ├── NC.png │ │ ├── NE.png │ │ ├── NF.png │ │ ├── NG.png │ │ ├── NI.png │ │ ├── NL.png │ │ ├── NO.png │ │ ├── NP.png │ │ ├── NR.png │ │ ├── NU.png │ │ ├── NZ.png │ │ ├── OM.png │ │ ├── PA.png │ │ ├── PE.png │ │ ├── PF.png │ │ ├── PG.png │ │ ├── PH.png │ │ ├── PK.png │ │ ├── PL.png │ │ ├── PM.png │ │ ├── PN.png │ │ ├── PR.png │ │ ├── PS.png │ │ ├── PT.png │ │ ├── PW.png │ │ ├── PY.png │ │ ├── QA.png │ │ ├── RE.png │ │ ├── RO.png │ │ ├── RS.png │ │ ├── RU.png │ │ ├── RW.png │ │ ├── SA.png │ │ ├── SB.png │ │ ├── SC.png │ │ ├── SD.png │ │ ├── SE.png │ │ ├── SG.png │ │ ├── SH.png │ │ ├── SI.png │ │ ├── SJ.png │ │ ├── SK.png │ │ ├── SL.png │ │ ├── SM.png │ │ ├── SN.png │ │ ├── SO.png │ │ ├── SR.png │ │ ├── SS.png │ │ ├── ST.png │ │ ├── SV.png │ │ ├── SX.png │ │ ├── SY.png │ │ ├── SZ.png │ │ ├── TA.png │ │ ├── TC.png │ │ ├── TD.png │ │ ├── TF.png │ │ ├── TG.png │ │ ├── TH.png │ │ ├── TJ.png │ │ ├── TK.png │ │ ├── TL.png │ │ ├── TM.png │ │ ├── TN.png │ │ ├── TO.png │ │ ├── TR.png │ │ ├── TT.png │ │ ├── TV.png │ │ ├── TW.png │ │ ├── TZ.png │ │ ├── UA.png │ │ ├── UG.png │ │ ├── UM.png │ │ ├── UN.png │ │ ├── US.png │ │ ├── UY.png │ │ ├── UZ.png │ │ ├── VA.png │ │ ├── VC.png │ │ ├── VE.png │ │ ├── VG.png │ │ ├── VI.png │ │ ├── VN.png │ │ ├── VU.png │ │ ├── WF.png │ │ ├── WS.png │ │ ├── XK.png │ │ ├── YE.png │ │ ├── YT.png │ │ ├── ZA.png │ │ ├── ZM.png │ │ └── ZW.png │ ├── onboarding-1.png │ ├── onboarding-2.png │ ├── onboarding-3.png │ ├── onboarding-4.png │ ├── onboarding-3-security.png │ ├── arrow-toggle.svg │ ├── container-close-tab.svg │ ├── filters.svg │ ├── new-16.svg │ ├── arrow-icon-left-light.svg │ ├── arrow-icon-left.svg │ ├── refresh-16.svg │ ├── blank-tab.svg │ ├── tab-new-16.svg │ ├── sort-16_1.svg │ ├── container-move.svg │ ├── info-thin-16.svg │ ├── multiaccountcontainer-16.svg │ ├── container-delete.svg │ ├── movetowindow-16.svg │ ├── Account.svg │ ├── info.svg │ ├── no-connection.svg │ ├── container-openin-16.svg │ ├── close.svg │ ├── close-light.svg │ ├── container-newtab.svg │ ├── moz-vpn-logo.svg │ ├── moz-vpn-connected.svg │ ├── moz-vpn-logo-light.svg │ ├── moz-vpn-disconnected.svg │ ├── webicon-facebook.svg │ ├── moz-vpn-status-icons │ │ ├── moz-vpn-connected.svg │ │ └── moz-vpn-disconnected.svg │ ├── gear-icon-light.svg │ ├── gear-icon.svg │ ├── warning.svg │ ├── proxy-warning-light.svg │ ├── proxy-warning.svg │ ├── password-hide.svg │ ├── arrow-icon-right.svg │ ├── arrow-icon-right-light.svg │ ├── Sync.svg │ ├── webicon-twitter.svg │ └── amo-icon.svg ├── fonts │ ├── Inter-Medium.woff2 │ ├── Inter-Regular.woff2 │ ├── Inter-SemiBold.woff2 │ ├── Metropolis-Light.woff2 │ └── Metropolis-Medium.woff2 ├── js │ ├── i18n.js │ ├── background │ │ ├── badge.js │ │ ├── index.html │ │ ├── mozillaVpnBackground.js │ │ └── identityState.js │ ├── pageAction.js │ ├── content-script.js │ ├── proxified-containers.js │ ├── confirm-page.js │ ├── options.js │ └── utils.js ├── css │ ├── content.css │ ├── confirm-page.css │ └── options.css ├── pageActionPopup.html ├── confirm-page.html ├── manifest.json └── options.html ├── .gitmodules ├── .htmllintrc ├── .gitignore ├── .jpmignore ├── .stylelintrc ├── .github ├── workflows │ ├── test.yaml │ └── builds.yaml ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── config.yml │ └── bug.yml ├── CODE_OF_CONDUCT.md ├── bin ├── build-addon.sh ├── addons-linter.sh └── commons.sh ├── test ├── issues │ ├── 1140.test.js │ ├── 1168.test.js │ └── 940.test.js ├── features │ ├── containers.test.js │ ├── external-webextensions.test.js │ └── assignment.test.js └── common.js ├── docs └── release.md ├── README.md ├── CONTRIBUTING.md ├── package.json └── eslint.config.js /.env: -------------------------------------------------------------------------------- 1 | nvm use 7.0 2 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/icon.png -------------------------------------------------------------------------------- /src/img/flags/AC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AC.png -------------------------------------------------------------------------------- /src/img/flags/AD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AD.png -------------------------------------------------------------------------------- /src/img/flags/AE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AE.png -------------------------------------------------------------------------------- /src/img/flags/AF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AF.png -------------------------------------------------------------------------------- /src/img/flags/AG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AG.png -------------------------------------------------------------------------------- /src/img/flags/AI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AI.png -------------------------------------------------------------------------------- /src/img/flags/AL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AL.png -------------------------------------------------------------------------------- /src/img/flags/AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AM.png -------------------------------------------------------------------------------- /src/img/flags/AO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AO.png -------------------------------------------------------------------------------- /src/img/flags/AQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AQ.png -------------------------------------------------------------------------------- /src/img/flags/AR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AR.png -------------------------------------------------------------------------------- /src/img/flags/AS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AS.png -------------------------------------------------------------------------------- /src/img/flags/AT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AT.png -------------------------------------------------------------------------------- /src/img/flags/AU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AU.png -------------------------------------------------------------------------------- /src/img/flags/AW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AW.png -------------------------------------------------------------------------------- /src/img/flags/AX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AX.png -------------------------------------------------------------------------------- /src/img/flags/AZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/AZ.png -------------------------------------------------------------------------------- /src/img/flags/BA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BA.png -------------------------------------------------------------------------------- /src/img/flags/BB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BB.png -------------------------------------------------------------------------------- /src/img/flags/BD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BD.png -------------------------------------------------------------------------------- /src/img/flags/BE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BE.png -------------------------------------------------------------------------------- /src/img/flags/BF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BF.png -------------------------------------------------------------------------------- /src/img/flags/BG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BG.png -------------------------------------------------------------------------------- /src/img/flags/BH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BH.png -------------------------------------------------------------------------------- /src/img/flags/BI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BI.png -------------------------------------------------------------------------------- /src/img/flags/BJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BJ.png -------------------------------------------------------------------------------- /src/img/flags/BL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BL.png -------------------------------------------------------------------------------- /src/img/flags/BM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BM.png -------------------------------------------------------------------------------- /src/img/flags/BN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BN.png -------------------------------------------------------------------------------- /src/img/flags/BO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BO.png -------------------------------------------------------------------------------- /src/img/flags/BQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BQ.png -------------------------------------------------------------------------------- /src/img/flags/BR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BR.png -------------------------------------------------------------------------------- /src/img/flags/BS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BS.png -------------------------------------------------------------------------------- /src/img/flags/BT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BT.png -------------------------------------------------------------------------------- /src/img/flags/BV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BV.png -------------------------------------------------------------------------------- /src/img/flags/BW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BW.png -------------------------------------------------------------------------------- /src/img/flags/BY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BY.png -------------------------------------------------------------------------------- /src/img/flags/BZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/BZ.png -------------------------------------------------------------------------------- /src/img/flags/CA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CA.png -------------------------------------------------------------------------------- /src/img/flags/CC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CC.png -------------------------------------------------------------------------------- /src/img/flags/CD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CD.png -------------------------------------------------------------------------------- /src/img/flags/CF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CF.png -------------------------------------------------------------------------------- /src/img/flags/CG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CG.png -------------------------------------------------------------------------------- /src/img/flags/CH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CH.png -------------------------------------------------------------------------------- /src/img/flags/CI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CI.png -------------------------------------------------------------------------------- /src/img/flags/CK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CK.png -------------------------------------------------------------------------------- /src/img/flags/CL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CL.png -------------------------------------------------------------------------------- /src/img/flags/CM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CM.png -------------------------------------------------------------------------------- /src/img/flags/CN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CN.png -------------------------------------------------------------------------------- /src/img/flags/CO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CO.png -------------------------------------------------------------------------------- /src/img/flags/CP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CP.png -------------------------------------------------------------------------------- /src/img/flags/CR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CR.png -------------------------------------------------------------------------------- /src/img/flags/CU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CU.png -------------------------------------------------------------------------------- /src/img/flags/CV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CV.png -------------------------------------------------------------------------------- /src/img/flags/CW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CW.png -------------------------------------------------------------------------------- /src/img/flags/CX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CX.png -------------------------------------------------------------------------------- /src/img/flags/CY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CY.png -------------------------------------------------------------------------------- /src/img/flags/CZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/CZ.png -------------------------------------------------------------------------------- /src/img/flags/DE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/DE.png -------------------------------------------------------------------------------- /src/img/flags/DG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/DG.png -------------------------------------------------------------------------------- /src/img/flags/DJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/DJ.png -------------------------------------------------------------------------------- /src/img/flags/DK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/DK.png -------------------------------------------------------------------------------- /src/img/flags/DM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/DM.png -------------------------------------------------------------------------------- /src/img/flags/DO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/DO.png -------------------------------------------------------------------------------- /src/img/flags/DZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/DZ.png -------------------------------------------------------------------------------- /src/img/flags/EA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/EA.png -------------------------------------------------------------------------------- /src/img/flags/EC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/EC.png -------------------------------------------------------------------------------- /src/img/flags/EE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/EE.png -------------------------------------------------------------------------------- /src/img/flags/EG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/EG.png -------------------------------------------------------------------------------- /src/img/flags/EH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/EH.png -------------------------------------------------------------------------------- /src/img/flags/ER.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/ER.png -------------------------------------------------------------------------------- /src/img/flags/ES.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/ES.png -------------------------------------------------------------------------------- /src/img/flags/ET.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/ET.png -------------------------------------------------------------------------------- /src/img/flags/EU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/EU.png -------------------------------------------------------------------------------- /src/img/flags/FI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/FI.png -------------------------------------------------------------------------------- /src/img/flags/FJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/FJ.png -------------------------------------------------------------------------------- /src/img/flags/FK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/FK.png -------------------------------------------------------------------------------- /src/img/flags/FM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/FM.png -------------------------------------------------------------------------------- /src/img/flags/FO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/FO.png -------------------------------------------------------------------------------- /src/img/flags/FR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/FR.png -------------------------------------------------------------------------------- /src/img/flags/GA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GA.png -------------------------------------------------------------------------------- /src/img/flags/GB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GB.png -------------------------------------------------------------------------------- /src/img/flags/GD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GD.png -------------------------------------------------------------------------------- /src/img/flags/GE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GE.png -------------------------------------------------------------------------------- /src/img/flags/GF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GF.png -------------------------------------------------------------------------------- /src/img/flags/GG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GG.png -------------------------------------------------------------------------------- /src/img/flags/GH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GH.png -------------------------------------------------------------------------------- /src/img/flags/GI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GI.png -------------------------------------------------------------------------------- /src/img/flags/GL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GL.png -------------------------------------------------------------------------------- /src/img/flags/GM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GM.png -------------------------------------------------------------------------------- /src/img/flags/GN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GN.png -------------------------------------------------------------------------------- /src/img/flags/GP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GP.png -------------------------------------------------------------------------------- /src/img/flags/GQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GQ.png -------------------------------------------------------------------------------- /src/img/flags/GR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GR.png -------------------------------------------------------------------------------- /src/img/flags/GS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GS.png -------------------------------------------------------------------------------- /src/img/flags/GT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GT.png -------------------------------------------------------------------------------- /src/img/flags/GU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GU.png -------------------------------------------------------------------------------- /src/img/flags/GW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GW.png -------------------------------------------------------------------------------- /src/img/flags/GY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/GY.png -------------------------------------------------------------------------------- /src/img/flags/HK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/HK.png -------------------------------------------------------------------------------- /src/img/flags/HM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/HM.png -------------------------------------------------------------------------------- /src/img/flags/HN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/HN.png -------------------------------------------------------------------------------- /src/img/flags/HR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/HR.png -------------------------------------------------------------------------------- /src/img/flags/HT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/HT.png -------------------------------------------------------------------------------- /src/img/flags/HU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/HU.png -------------------------------------------------------------------------------- /src/img/flags/IC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/IC.png -------------------------------------------------------------------------------- /src/img/flags/ID.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/ID.png -------------------------------------------------------------------------------- /src/img/flags/IE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/IE.png -------------------------------------------------------------------------------- /src/img/flags/IL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/IL.png -------------------------------------------------------------------------------- /src/img/flags/IM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/IM.png -------------------------------------------------------------------------------- /src/img/flags/IN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/IN.png -------------------------------------------------------------------------------- /src/img/flags/IO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/IO.png -------------------------------------------------------------------------------- /src/img/flags/IQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/IQ.png -------------------------------------------------------------------------------- /src/img/flags/IR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/IR.png -------------------------------------------------------------------------------- /src/img/flags/IS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/IS.png -------------------------------------------------------------------------------- /src/img/flags/IT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/IT.png -------------------------------------------------------------------------------- /src/img/flags/JE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/JE.png -------------------------------------------------------------------------------- /src/img/flags/JM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/JM.png -------------------------------------------------------------------------------- /src/img/flags/JO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/JO.png -------------------------------------------------------------------------------- /src/img/flags/JP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/JP.png -------------------------------------------------------------------------------- /src/img/flags/KE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/KE.png -------------------------------------------------------------------------------- /src/img/flags/KG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/KG.png -------------------------------------------------------------------------------- /src/img/flags/KH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/KH.png -------------------------------------------------------------------------------- /src/img/flags/KI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/KI.png -------------------------------------------------------------------------------- /src/img/flags/KM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/KM.png -------------------------------------------------------------------------------- /src/img/flags/KN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/KN.png -------------------------------------------------------------------------------- /src/img/flags/KP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/KP.png -------------------------------------------------------------------------------- /src/img/flags/KR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/KR.png -------------------------------------------------------------------------------- /src/img/flags/KW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/KW.png -------------------------------------------------------------------------------- /src/img/flags/KY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/KY.png -------------------------------------------------------------------------------- /src/img/flags/KZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/KZ.png -------------------------------------------------------------------------------- /src/img/flags/LA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/LA.png -------------------------------------------------------------------------------- /src/img/flags/LB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/LB.png -------------------------------------------------------------------------------- /src/img/flags/LC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/LC.png -------------------------------------------------------------------------------- /src/img/flags/LI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/LI.png -------------------------------------------------------------------------------- /src/img/flags/LK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/LK.png -------------------------------------------------------------------------------- /src/img/flags/LR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/LR.png -------------------------------------------------------------------------------- /src/img/flags/LS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/LS.png -------------------------------------------------------------------------------- /src/img/flags/LT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/LT.png -------------------------------------------------------------------------------- /src/img/flags/LU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/LU.png -------------------------------------------------------------------------------- /src/img/flags/LV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/LV.png -------------------------------------------------------------------------------- /src/img/flags/LY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/LY.png -------------------------------------------------------------------------------- /src/img/flags/MA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MA.png -------------------------------------------------------------------------------- /src/img/flags/MC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MC.png -------------------------------------------------------------------------------- /src/img/flags/MD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MD.png -------------------------------------------------------------------------------- /src/img/flags/ME.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/ME.png -------------------------------------------------------------------------------- /src/img/flags/MF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MF.png -------------------------------------------------------------------------------- /src/img/flags/MG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MG.png -------------------------------------------------------------------------------- /src/img/flags/MH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MH.png -------------------------------------------------------------------------------- /src/img/flags/MK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MK.png -------------------------------------------------------------------------------- /src/img/flags/ML.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/ML.png -------------------------------------------------------------------------------- /src/img/flags/MM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MM.png -------------------------------------------------------------------------------- /src/img/flags/MN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MN.png -------------------------------------------------------------------------------- /src/img/flags/MO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MO.png -------------------------------------------------------------------------------- /src/img/flags/MP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MP.png -------------------------------------------------------------------------------- /src/img/flags/MQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MQ.png -------------------------------------------------------------------------------- /src/img/flags/MR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MR.png -------------------------------------------------------------------------------- /src/img/flags/MS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MS.png -------------------------------------------------------------------------------- /src/img/flags/MT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MT.png -------------------------------------------------------------------------------- /src/img/flags/MU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MU.png -------------------------------------------------------------------------------- /src/img/flags/MV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MV.png -------------------------------------------------------------------------------- /src/img/flags/MW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MW.png -------------------------------------------------------------------------------- /src/img/flags/MX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MX.png -------------------------------------------------------------------------------- /src/img/flags/MY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MY.png -------------------------------------------------------------------------------- /src/img/flags/MZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/MZ.png -------------------------------------------------------------------------------- /src/img/flags/NA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/NA.png -------------------------------------------------------------------------------- /src/img/flags/NC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/NC.png -------------------------------------------------------------------------------- /src/img/flags/NE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/NE.png -------------------------------------------------------------------------------- /src/img/flags/NF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/NF.png -------------------------------------------------------------------------------- /src/img/flags/NG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/NG.png -------------------------------------------------------------------------------- /src/img/flags/NI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/NI.png -------------------------------------------------------------------------------- /src/img/flags/NL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/NL.png -------------------------------------------------------------------------------- /src/img/flags/NO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/NO.png -------------------------------------------------------------------------------- /src/img/flags/NP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/NP.png -------------------------------------------------------------------------------- /src/img/flags/NR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/NR.png -------------------------------------------------------------------------------- /src/img/flags/NU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/NU.png -------------------------------------------------------------------------------- /src/img/flags/NZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/NZ.png -------------------------------------------------------------------------------- /src/img/flags/OM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/OM.png -------------------------------------------------------------------------------- /src/img/flags/PA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PA.png -------------------------------------------------------------------------------- /src/img/flags/PE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PE.png -------------------------------------------------------------------------------- /src/img/flags/PF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PF.png -------------------------------------------------------------------------------- /src/img/flags/PG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PG.png -------------------------------------------------------------------------------- /src/img/flags/PH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PH.png -------------------------------------------------------------------------------- /src/img/flags/PK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PK.png -------------------------------------------------------------------------------- /src/img/flags/PL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PL.png -------------------------------------------------------------------------------- /src/img/flags/PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PM.png -------------------------------------------------------------------------------- /src/img/flags/PN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PN.png -------------------------------------------------------------------------------- /src/img/flags/PR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PR.png -------------------------------------------------------------------------------- /src/img/flags/PS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PS.png -------------------------------------------------------------------------------- /src/img/flags/PT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PT.png -------------------------------------------------------------------------------- /src/img/flags/PW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PW.png -------------------------------------------------------------------------------- /src/img/flags/PY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/PY.png -------------------------------------------------------------------------------- /src/img/flags/QA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/QA.png -------------------------------------------------------------------------------- /src/img/flags/RE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/RE.png -------------------------------------------------------------------------------- /src/img/flags/RO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/RO.png -------------------------------------------------------------------------------- /src/img/flags/RS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/RS.png -------------------------------------------------------------------------------- /src/img/flags/RU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/RU.png -------------------------------------------------------------------------------- /src/img/flags/RW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/RW.png -------------------------------------------------------------------------------- /src/img/flags/SA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SA.png -------------------------------------------------------------------------------- /src/img/flags/SB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SB.png -------------------------------------------------------------------------------- /src/img/flags/SC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SC.png -------------------------------------------------------------------------------- /src/img/flags/SD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SD.png -------------------------------------------------------------------------------- /src/img/flags/SE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SE.png -------------------------------------------------------------------------------- /src/img/flags/SG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SG.png -------------------------------------------------------------------------------- /src/img/flags/SH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SH.png -------------------------------------------------------------------------------- /src/img/flags/SI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SI.png -------------------------------------------------------------------------------- /src/img/flags/SJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SJ.png -------------------------------------------------------------------------------- /src/img/flags/SK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SK.png -------------------------------------------------------------------------------- /src/img/flags/SL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SL.png -------------------------------------------------------------------------------- /src/img/flags/SM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SM.png -------------------------------------------------------------------------------- /src/img/flags/SN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SN.png -------------------------------------------------------------------------------- /src/img/flags/SO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SO.png -------------------------------------------------------------------------------- /src/img/flags/SR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SR.png -------------------------------------------------------------------------------- /src/img/flags/SS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SS.png -------------------------------------------------------------------------------- /src/img/flags/ST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/ST.png -------------------------------------------------------------------------------- /src/img/flags/SV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SV.png -------------------------------------------------------------------------------- /src/img/flags/SX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SX.png -------------------------------------------------------------------------------- /src/img/flags/SY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SY.png -------------------------------------------------------------------------------- /src/img/flags/SZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/SZ.png -------------------------------------------------------------------------------- /src/img/flags/TA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TA.png -------------------------------------------------------------------------------- /src/img/flags/TC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TC.png -------------------------------------------------------------------------------- /src/img/flags/TD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TD.png -------------------------------------------------------------------------------- /src/img/flags/TF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TF.png -------------------------------------------------------------------------------- /src/img/flags/TG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TG.png -------------------------------------------------------------------------------- /src/img/flags/TH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TH.png -------------------------------------------------------------------------------- /src/img/flags/TJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TJ.png -------------------------------------------------------------------------------- /src/img/flags/TK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TK.png -------------------------------------------------------------------------------- /src/img/flags/TL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TL.png -------------------------------------------------------------------------------- /src/img/flags/TM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TM.png -------------------------------------------------------------------------------- /src/img/flags/TN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TN.png -------------------------------------------------------------------------------- /src/img/flags/TO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TO.png -------------------------------------------------------------------------------- /src/img/flags/TR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TR.png -------------------------------------------------------------------------------- /src/img/flags/TT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TT.png -------------------------------------------------------------------------------- /src/img/flags/TV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TV.png -------------------------------------------------------------------------------- /src/img/flags/TW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TW.png -------------------------------------------------------------------------------- /src/img/flags/TZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/TZ.png -------------------------------------------------------------------------------- /src/img/flags/UA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/UA.png -------------------------------------------------------------------------------- /src/img/flags/UG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/UG.png -------------------------------------------------------------------------------- /src/img/flags/UM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/UM.png -------------------------------------------------------------------------------- /src/img/flags/UN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/UN.png -------------------------------------------------------------------------------- /src/img/flags/US.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/US.png -------------------------------------------------------------------------------- /src/img/flags/UY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/UY.png -------------------------------------------------------------------------------- /src/img/flags/UZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/UZ.png -------------------------------------------------------------------------------- /src/img/flags/VA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/VA.png -------------------------------------------------------------------------------- /src/img/flags/VC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/VC.png -------------------------------------------------------------------------------- /src/img/flags/VE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/VE.png -------------------------------------------------------------------------------- /src/img/flags/VG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/VG.png -------------------------------------------------------------------------------- /src/img/flags/VI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/VI.png -------------------------------------------------------------------------------- /src/img/flags/VN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/VN.png -------------------------------------------------------------------------------- /src/img/flags/VU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/VU.png -------------------------------------------------------------------------------- /src/img/flags/WF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/WF.png -------------------------------------------------------------------------------- /src/img/flags/WS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/WS.png -------------------------------------------------------------------------------- /src/img/flags/XK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/XK.png -------------------------------------------------------------------------------- /src/img/flags/YE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/YE.png -------------------------------------------------------------------------------- /src/img/flags/YT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/YT.png -------------------------------------------------------------------------------- /src/img/flags/ZA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/ZA.png -------------------------------------------------------------------------------- /src/img/flags/ZM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/ZM.png -------------------------------------------------------------------------------- /src/img/flags/ZW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/flags/ZW.png -------------------------------------------------------------------------------- /src/img/onboarding-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/onboarding-1.png -------------------------------------------------------------------------------- /src/img/onboarding-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/onboarding-2.png -------------------------------------------------------------------------------- /src/img/onboarding-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/onboarding-3.png -------------------------------------------------------------------------------- /src/img/onboarding-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/onboarding-4.png -------------------------------------------------------------------------------- /src/fonts/Inter-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/fonts/Inter-Medium.woff2 -------------------------------------------------------------------------------- /src/fonts/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/fonts/Inter-Regular.woff2 -------------------------------------------------------------------------------- /src/fonts/Inter-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/fonts/Inter-SemiBold.woff2 -------------------------------------------------------------------------------- /src/fonts/Metropolis-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/fonts/Metropolis-Light.woff2 -------------------------------------------------------------------------------- /src/fonts/Metropolis-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/fonts/Metropolis-Medium.woff2 -------------------------------------------------------------------------------- /src/img/onboarding-3-security.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/multi-account-containers/HEAD/src/img/onboarding-3-security.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/_locales"] 2 | branch = main 3 | path = src/_locales 4 | url = https://github.com/mozilla-l10n/multi-account-containers-l10n.git 5 | ignore=all 6 | -------------------------------------------------------------------------------- /.htmllintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [], 3 | 4 | "id-class-style": "dash", 5 | "indent-style": "spaces", 6 | "indent-width": 2, 7 | "attr-name-ignore-regex": "http-equiv|data-*" 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | README.html 4 | *.xpi 5 | *.sw* 6 | .vimrc 7 | .env 8 | addon.env 9 | 10 | src/web-ext-artifacts/* 11 | web-ext-artifacts 12 | 13 | # JetBrains IDE files 14 | .idea 15 | 16 | # IstanbulJS 17 | .nyc_output 18 | coverage -------------------------------------------------------------------------------- /.jpmignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | docs/ 3 | test/ 4 | .npm/ 5 | node_modules/ 6 | bin/ 7 | 8 | .env 9 | .eslintrc.js 10 | .eslintignore 11 | .gitignore 12 | .htmllintrc 13 | .jpmignore 14 | .npm 15 | .stylelintrc 16 | .travis.yml 17 | *.xpi 18 | *.md 19 | .vimrc 20 | .DS_Store 21 | .gdb_history 22 | *.sw* 23 | -------------------------------------------------------------------------------- /src/img/arrow-toggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/img/container-close-tab.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/img/filters.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/img/new-16.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/img/arrow-icon-left-light.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/img/arrow-icon-left.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/img/refresh-16.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/img/blank-tab.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/js/i18n.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", async () => { 2 | document.querySelectorAll("[data-i18n-message-id]").forEach(el => { 3 | const messageArgs = el.dataset.i18nPlaceholder ? el.dataset.i18nPlaceholder : null; 4 | el.textContent = browser.i18n.getMessage(el.dataset.i18nMessageId, [messageArgs]); 5 | }); 6 | document.querySelectorAll("[data-i18n-attribute]").forEach(el => { 7 | el.setAttribute(el.dataset.i18nAttribute, browser.i18n.getMessage(el.dataset.i18nAttributeMessageId)); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "stylelint-order" 4 | ], 5 | 6 | "extends": "stylelint-config-standard", 7 | 8 | "ignoreFiles": ["src/css/*.min.css"], 9 | 10 | "rules": { 11 | "declaration-block-no-duplicate-properties": true, 12 | "property-no-unknown": [ 13 | true, { 14 | ignoreProperties: 15 | ["inset-block-end", "inset-block-start"] 16 | }], 17 | "property-disallowed-list": [ 18 | "/(min[-]|max[-])height/", 19 | "/width/", 20 | "/top/", 21 | "/bottom/", 22 | "padding", 23 | "margin" 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/img/tab-new-16.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - production 7 | pull_request: 8 | branches: 9 | 10 | jobs: 11 | test: 12 | name: Run tests 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Clone repository 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: lts/* 23 | 24 | - name: Install dependencies 25 | run: npm install --legacy-peer-deps 26 | 27 | - name: Run tests 28 | run: npm run test 29 | -------------------------------------------------------------------------------- /src/img/sort-16_1.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Before submitting your pull request** 2 | 3 | - [ ] I agree to license my code under the [MPL 2.0 license](https://www.mozilla.org/en-US/MPL/2.0/). 4 | - [ ] I rebased my work on top of the main branch. 5 | - [ ] I ran `npm test` and all tests passed. 6 | - [ ] I added test coverages if relevant. 7 | 8 | # Description 9 | 10 | *Please include a summary of the changes including relevant motivation and context.* 11 | 12 | ## Type of change 13 | 14 | *Select all that apply.* 15 | 16 | - [ ] Bug fix 17 | - [ ] New feature 18 | - [ ] Major change (fix or feature that would cause existing functionality to work differently than in the current version) 19 | 20 | Tag issues related to this pull request: 21 | 22 | * 23 | * 24 | * 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /src/css/content.css: -------------------------------------------------------------------------------- 1 | .container-notification { 2 | align-items: center; 3 | background: #efefef; 4 | color: #003f07; 5 | display: flex; 6 | font: 12px sans-serif; 7 | inline-size: 100vw; 8 | justify-content: start; 9 | offset-block-start: 0; 10 | offset-inline-start: 0; 11 | padding-block-end: 8px; 12 | padding-block-start: 8px; 13 | padding-inline-end: 8px; 14 | padding-inline-start: 8px; 15 | position: fixed; 16 | text-align: start; 17 | transform: translateY(-100%); 18 | transition: transform 0.3s cubic-bezier(0.07, 0.95, 0, 1) 0.3s; 19 | z-index: 999999999999; 20 | } 21 | 22 | .container-notification img { 23 | block-size: 16px; 24 | display: inline-block; 25 | inline-size: 16px; 26 | margin-inline-end: 3px; 27 | } 28 | -------------------------------------------------------------------------------- /src/img/container-move.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/builds.yaml: -------------------------------------------------------------------------------- 1 | name: Builds 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - production 7 | pull_request: 8 | branches: 9 | - main 10 | - production 11 | schedule: 12 | - cron: '0 2 * * *' # Daily at 2AM UTC 13 | 14 | jobs: 15 | builds: 16 | name: Builds 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Clone repository 21 | uses: actions/checkout@v3 22 | 23 | - name: Create the package 24 | shell: bash 25 | run: | 26 | ./bin/build-addon.sh nightly.xpi 27 | 28 | - name: Uploading 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: ${{matrix.config.name}} Build 32 | path: src/web-ext-artifacts 33 | -------------------------------------------------------------------------------- /bin/build-addon.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | 7 | . $(dirname $0)/commons.sh 8 | 9 | print Y "Update the submodules..." 10 | git submodule init || die 11 | git submodule update --remote --depth 1 src/_locales || die 12 | 13 | print Y "Installing dependencies..." 14 | npm install --legacy-peer-deps || die 15 | 16 | print Y "Running tests..." 17 | npm test 18 | 19 | print Y "Creating the final package..." 20 | cd src || die 21 | 22 | if [[ $# -gt 0 ]]; then 23 | EXTRA_PARAMS="--filename $1" 24 | fi 25 | 26 | npx web-ext build --overwrite-dest $EXTRA_PARAMS || die 27 | -------------------------------------------------------------------------------- /src/img/info-thin-16.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/img/multiaccountcontainer-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/img/container-delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /src/img/movetowindow-16.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/img/Account.svg: -------------------------------------------------------------------------------- 1 | account -------------------------------------------------------------------------------- /src/img/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/js/background/badge.js: -------------------------------------------------------------------------------- 1 | const MAJOR_VERSIONS = ["2.3.0", "2.4.0", "6.2.0", "8.0.2"]; 2 | const badge = { 3 | async init() { 4 | const currentWindow = await browser.windows.getCurrent(); 5 | this.displayBrowserActionBadge(currentWindow); 6 | }, 7 | 8 | async displayBrowserActionBadge() { 9 | const extensionInfo = await backgroundLogic.getExtensionInfo(); 10 | const storage = await browser.storage.local.get({ browserActionBadgesClicked: [] }); 11 | 12 | if (MAJOR_VERSIONS.indexOf(extensionInfo.version) > -1 && 13 | storage.browserActionBadgesClicked.indexOf(extensionInfo.version) < 0) { 14 | browser.browserAction.setBadgeBackgroundColor({ color: "rgb(255, 79, 94)" }); 15 | browser.browserAction.setBadgeText({ text: "!" }); 16 | browser.browserAction.setBadgeTextColor({ color: "rgb(255, 255, 255)" }); 17 | } 18 | } 19 | }; 20 | 21 | badge.init(); 22 | -------------------------------------------------------------------------------- /test/issues/1140.test.js: -------------------------------------------------------------------------------- 1 | const { sinon, nextTick, buildBackgroundDom } = require("../common"); 2 | 3 | describe("#1140", () => { 4 | beforeEach(async () => { 5 | this.background = await buildBackgroundDom(); 6 | }); 7 | 8 | describe("removing containers", () => { 9 | beforeEach(async () => { 10 | this.background.browser.contextualIdentities.onRemoved.addListener = sinon.stub(); 11 | const [promise] = this.background.browser.runtime.onMessage.addListener.yield({ 12 | method: "deleteContainer", 13 | message: { 14 | userContextId: "1" 15 | } 16 | }); 17 | await promise; 18 | await nextTick(); 19 | }); 20 | 21 | it("should remove the identitystate from storage as well", async () => { 22 | this.background.browser.storage.local.remove.should.have.been.calledWith([ 23 | "identitiesState@@_firefox-container-1" 24 | ]); 25 | }); 26 | }); 27 | }); -------------------------------------------------------------------------------- /src/img/no-connection.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | image/svg+xml 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /src/img/container-openin-16.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: "Explore our help articles" 4 | url: "https://support.mozilla.org/kb/containers" 5 | about: "Dig into the knowledge base, tips and tricks, troubleshooting, and so much more." 6 | - name: "Ask a support question" 7 | url: "https://support.mozilla.org/questions/new/desktop/form" 8 | about: "Get support from our contributors or staff members." 9 | - name: "Submit new ideas" 10 | url: "https://connect.mozilla.org/t5/discussions/how-to-submit-a-great-idea-in-five-easy-steps/td-p/24" 11 | about: "Have an idea for a new product feature? Share it with our community and staff members!" 12 | - name: "Discussions" 13 | url: "https://connect.mozilla.org/t5/discussions/bd-p/discussions" 14 | about: "Give feedback and participate in meaningful conversations with the community and Mozilla employees" 15 | - name: "Discover more awesome tools" 16 | url: "https://www.mozilla.org/firefox/products/" 17 | about: "Learn more about other products from Mozilla" 18 | -------------------------------------------------------------------------------- /bin/addons-linter.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | 7 | # addons-linter is not happy to see a `.github` folder in src/_locales. 8 | # We need to do an horrible hack to run the test. 9 | 10 | . $(dirname $0)/commons.sh 11 | 12 | TMPDIR=/tmp/MAC_addonsLinter 13 | 14 | print Y "Update the submodules..." 15 | git submodule init || die 16 | git submodule update --remote --depth 1 src/_locales || die 17 | 18 | printn Y "Removing previous execution data... " 19 | rm -rf $TMPDIR || die 20 | print G "done." 21 | 22 | printn Y "Creating a tmp folder ($TMPDIR)... " 23 | mkdir $TMPDIR || die 24 | print G "done." 25 | 26 | printn Y "Copying data... " 27 | cp -r src $TMPDIR || die 28 | print G "done." 29 | 30 | printn Y "Removing the github folder... " 31 | rm -rf $TMPDIR/src/_locales/.github || die 32 | print G "done." 33 | 34 | print Y "Running the test..." 35 | npx addons-linter $TMPDIR/src || die 36 | -------------------------------------------------------------------------------- /src/js/background/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/img/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/img/close-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/img/container-newtab.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 11 | icon-newtab 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/release.md: -------------------------------------------------------------------------------- 1 | # Release a new version 2 | 3 | ## Make the new version 4 | 5 | 1. Bump the version number in `package.json` and `manifest.json` 6 | 2. Commit the version number bump 7 | 3. Create a git tag for the version: `git tag ` 8 | 4. Push the tag up to GitHub: `git push --tags` 9 | 10 | ## Publish to AMO 11 | 12 | 1. Run `./bin/build-addon.sh` 13 | 2. [Upload the zip file to AMO][amo-upload] 14 | 15 | ## Publish to GitHub 16 | 17 | Finally, we also publish the release to GitHub. 18 | 19 | 1. Download the signed `.xpi` from [the addon versions page][addon-page] 20 | 2. [Create a new release on GitHub][gh-release] 21 | * For *Tag version* and *Release title*, use the version number 22 | * For *Release notes*, copy the output of: 23 | ``` 24 | git log --no-merges \ 25 | --pretty=format:"%h %s" .. 26 | ``` 27 | * For the *Attach binaries*, select the signed `.xpi` file 28 | 29 | [addon-page]: https://addons.mozilla.org/developers/addon/multi-account-containers/versions 30 | [amo-upload]: https://addons.mozilla.org/developers/addon/multi-account-containers/versions/submit/ 31 | [gh-release]: https://github.com/mozilla/multi-account-containers/releases/new 32 | -------------------------------------------------------------------------------- /src/img/moz-vpn-logo.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 14 | 15 | -------------------------------------------------------------------------------- /bin/commons.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | 7 | printv() { 8 | if [ -t 1 ]; then 9 | NCOLORS=$(tput colors) 10 | 11 | if test -n "$NCOLORS" && test "$NCOLORS" -ge 8; then 12 | NORMAL="$(tput sgr0)" 13 | RED="$(tput setaf 1)" 14 | GREEN="$(tput setaf 2)" 15 | YELLOW="$(tput setaf 3)" 16 | fi 17 | fi 18 | 19 | if [[ $2 = 'G' ]]; then 20 | # shellcheck disable=SC2086 21 | echo $1 -e "${GREEN}$3${NORMAL}" 22 | elif [[ $2 = 'Y' ]]; then 23 | # shellcheck disable=SC2086 24 | echo $1 -e "${YELLOW}$3${NORMAL}" 25 | elif [[ $2 = 'N' ]]; then 26 | # shellcheck disable=SC2086 27 | echo $1 -e "$3" 28 | else 29 | # shellcheck disable=SC2086 30 | echo $1 -e "${RED}$3${NORMAL}" 31 | fi 32 | } 33 | 34 | print() { 35 | printv '' "$1" "$2" 36 | } 37 | 38 | printn() { 39 | printv "-n" "$1" "$2" 40 | } 41 | 42 | error() { 43 | printv '' R "$1" 44 | } 45 | 46 | die() { 47 | if [[ "$1" ]]; then 48 | error "$1" 49 | else 50 | error Failed 51 | fi 52 | 53 | exit 1 54 | } 55 | -------------------------------------------------------------------------------- /src/img/moz-vpn-connected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/img/moz-vpn-logo-light.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 14 | 15 | -------------------------------------------------------------------------------- /src/pageActionPopup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Firefox Multi-Account Containers 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

13 |
14 |
15 | 16 | 17 | 26 | 27 | 28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/js/pageAction.js: -------------------------------------------------------------------------------- 1 | async function init() { 2 | const fragment = document.createDocumentFragment(); 3 | const identities = await browser.contextualIdentities.query({}); 4 | 5 | for (const identity of identities) { 6 | const tr = document.createElement("tr"); 7 | tr.classList.add("menu-item", "hover-highlight"); 8 | tr.setAttribute("data-cookie-store-id", identity.cookieStoreId); 9 | const td = document.createElement("td"); 10 | td.innerHTML = Utils.escaped` 11 | 17 | ${identity.name} 18 | 19 | `; 20 | 21 | tr.appendChild(td); 22 | fragment.appendChild(tr); 23 | 24 | Utils.addEnterHandler(tr, async () => { 25 | Utils.alwaysOpenInContainer(identity); 26 | window.close(); 27 | }); 28 | } 29 | 30 | const list = document.querySelector("#picker-identities-list"); 31 | list.innerHTML = ""; 32 | list.appendChild(fragment); 33 | 34 | MozillaVPN.handleContainerList(identities); 35 | 36 | // Set the theme 37 | Utils.applyTheme(); 38 | } 39 | 40 | init(); 41 | -------------------------------------------------------------------------------- /src/img/moz-vpn-disconnected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/img/webicon-facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /test/issues/1168.test.js: -------------------------------------------------------------------------------- 1 | const {expect, sinon, initializeWithTab} = require("../common"); 2 | 3 | describe("#1168", function () { 4 | describe("when navigation happens too slow after opening new tab to a page which then redirects", function () { 5 | let clock, tab, background; 6 | 7 | beforeEach(async function () { 8 | this.webExt = await initializeWithTab({ 9 | cookieStoreId: "firefox-container-1", 10 | url: "https://bugzilla.mozilla.org" 11 | }); 12 | 13 | await this.webExt.popup.helper.clickElementById("container-page-assigned"); 14 | 15 | clock = sinon.useFakeTimers(); 16 | tab = await this.webExt.browser.tabs._create({}); 17 | 18 | clock.tick(2000); 19 | 20 | await background.browser.tabs._navigate(tab.id, "https://duckduckgo.com/?q=%21bugzilla+thing&t=ffab"); 21 | await background.browser.tabs._redirect(tab.id, [ 22 | "https://bugzilla.mozilla.org" 23 | ]); 24 | }); 25 | 26 | afterEach(function () { 27 | this.webExt.destroy(); 28 | clock.restore(); 29 | }); 30 | 31 | // Not solved yet 32 | // See: https://github.com/mozilla/multi-account-containers/issues/1168#issuecomment-378394091 33 | it.skip("should remove the old tab", async function () { 34 | expect(background.browser.tabs.create).to.have.been.calledOnce; 35 | expect(background.browser.tabs.remove).to.have.been.calledWith(tab.id); 36 | }); 37 | }); 38 | }); -------------------------------------------------------------------------------- /src/img/moz-vpn-status-icons/moz-vpn-connected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/img/moz-vpn-status-icons/moz-vpn-disconnected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/img/gear-icon-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/img/gear-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/features/containers.test.js: -------------------------------------------------------------------------------- 1 | const {initializeWithTab} = require("../common"); 2 | 3 | describe("Containers Management", function () { 4 | beforeEach(async function () { 5 | this.webExt = await initializeWithTab(); 6 | }); 7 | 8 | afterEach(function () { 9 | this.webExt.destroy(); 10 | }); 11 | 12 | describe("creating a new container", function () { 13 | beforeEach(async function () { 14 | await this.webExt.popup.helper.clickElementById("manage-containers-link"); 15 | await this.webExt.popup.helper.clickElementById("new-container"); 16 | await this.webExt.popup.helper.clickElementById("create-container-ok-link"); 17 | }); 18 | 19 | it("should create it in the browser as well", function () { 20 | this.webExt.background.browser.contextualIdentities.create.should.have.been.calledOnce; 21 | }); 22 | 23 | describe("removing it afterwards", function () { 24 | beforeEach(async function () { 25 | await this.webExt.popup.helper.clickElementById("manage-containers-link"); 26 | await this.webExt.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item", "last"); 27 | await this.webExt.popup.helper.clickElementById("delete-container-button"); 28 | await this.webExt.popup.helper.clickElementById("delete-container-ok-link"); 29 | }); 30 | 31 | it("should remove it in the browser as well", function () { 32 | this.webExt.background.browser.contextualIdentities.remove.should.have.been.calledOnce; 33 | }); 34 | }); 35 | }); 36 | }); -------------------------------------------------------------------------------- /src/img/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/img/proxy-warning-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/img/proxy-warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi-Account Containers 2 | 3 | [![Test](https://github.com/mozilla/multi-account-containers/actions/workflows/test.yaml/badge.svg)](https://github.com/mozilla/multi-account-containers/actions/workflows/test.yaml) 4 | 5 | The Firefox Multi-Account Containers extension lets you carve out a separate box for each of your online lives – no more opening a different browser just to check your work email! 6 | 7 | Learn more about Multi-Account Containers in 8 | [our end-user documentation][enduser]. 9 | 10 | ## Contributing 11 | 12 | Everyone is welcome to contribute to Multi-Account Containers. To learn how 13 | to contribute a patch to Multi-Account Container, please 14 | [read our contributing guide][contributing]. 15 | 16 | You can also chat with us on [our Matrix room][matrix] or ask in [our discussions board][discussions]. 17 | 18 | This repository is governed by Mozilla's code of conduct and etiquette 19 | guidelines. For more details, [please read the Mozilla Community Participation Guidelines][cpg]. 20 | 21 | ### License 22 | 23 | This Source Code Form is subject to the terms of the Mozilla Public 24 | License, v. 2.0. If a copy of the MPL was not distributed with this 25 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 26 | 27 | 28 | [contributing]: CONTRIBUTING.md 29 | [cpg]: https://www.mozilla.org/about/governance/policies/participation/ 30 | [enduser]: https://support.mozilla.org/en-US/kb/containers 31 | [forum]: https://discourse.mozilla.org/c/containers/223 32 | [discussions]: https://github.com/mozilla/multi-account-containers/discussions 33 | [matrix]: https://matrix.to/#/#containers:mozilla.org 34 | -------------------------------------------------------------------------------- /src/js/content-script.js: -------------------------------------------------------------------------------- 1 | async function delayAnimation(delay = 350) { 2 | return new Promise((resolve) => { 3 | setTimeout(resolve, delay); 4 | }); 5 | } 6 | 7 | async function doAnimation(element, property, value) { 8 | return new Promise((resolve) => { 9 | const handler = () => { 10 | resolve(); 11 | element.removeEventListener("transitionend", handler); 12 | }; 13 | element.addEventListener("transitionend", handler); 14 | window.requestAnimationFrame(() => { 15 | element.style[property] = value; 16 | }); 17 | }); 18 | } 19 | 20 | async function addMessage(message) { 21 | const divElement = document.createElement("div"); 22 | divElement.classList.add("container-notification"); 23 | // Ideally we would use https://bugzilla.mozilla.org/show_bug.cgi?id=1340930 when this is available 24 | divElement.innerText = message.text; 25 | 26 | const imageElement = document.createElement("img"); 27 | const imagePath = browser.runtime.getURL("/img/multiaccountcontainer-16.svg"); 28 | const response = await fetch(imagePath); 29 | const blob = await response.blob(); 30 | const objectUrl = URL.createObjectURL(blob); 31 | imageElement.src = objectUrl; 32 | imageElement.width = imageElement.height = 24; 33 | divElement.prepend(imageElement); 34 | 35 | document.body.appendChild(divElement); 36 | 37 | await delayAnimation(100); 38 | await doAnimation(divElement, "transform", "translateY(0)"); 39 | await delayAnimation(3000); 40 | await doAnimation(divElement, "transform", "translateY(-100%)"); 41 | 42 | divElement.remove(); 43 | } 44 | 45 | browser.runtime.onMessage.addListener((message) => { 46 | addMessage(message); 47 | }); 48 | -------------------------------------------------------------------------------- /src/img/password-hide.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/img/arrow-icon-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Arrow 8 | Created with Sketch. 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/img/arrow-icon-right-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Arrow 8 | Created with Sketch. 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a problem in Multi-Account Containers 3 | labels: [bug] 4 | body: 5 | - type: checkboxes 6 | id: before-bug-report 7 | attributes: 8 | label: Before submitting a bug report 9 | options: 10 | - label: "I updated to the latest version of Multi-Account Container and tested if I can reproduce the issue" 11 | required: true 12 | - label: "I searched for existing reports to see if it hasn't already been reported" 13 | required: true 14 | - type: textarea 15 | id: step_to_reproduce 16 | attributes: 17 | label: "Step to reproduce" 18 | description: "Provide a list of steps you did to trigger this bug" 19 | placeholder: | 20 | 1. I opened ... 21 | 2. I clicked on ... 22 | 3. ... 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: actual_behavior 27 | attributes: 28 | label: "Actual behavior" 29 | description: "Provide a description of what is currently happening" 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: expected_behavior 34 | attributes: 35 | label: "Expected behavior" 36 | description: "Provide a description of what should happen" 37 | validations: 38 | required: true 39 | - type: textarea 40 | id: additional_informations 41 | attributes: 42 | label: "Additional informations" 43 | description: "Provide any other information revelant to this issue" 44 | validations: 45 | required: false 46 | - type: textarea 47 | id: about_support 48 | attributes: 49 | label: "Provide a copy of Troubleshooting Information page (optional)" 50 | description: "To get a copy of the Troubleshooting Information page, type *about:support* in the address bar and click on the *Copy text to clipboard* button." 51 | render: "plain text" 52 | validations: 53 | required: false 54 | -------------------------------------------------------------------------------- /src/confirm-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 |
15 |
16 |

17 |
18 |

19 |
20 |
21 | 25 |
26 |
27 | 32 | 36 | 42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/css/confirm-page.css: -------------------------------------------------------------------------------- 1 | /* General Rules and Resets */ 2 | .title { 3 | background-image: none; 4 | } 5 | 6 | main { 7 | background: url(/img/onboarding-4.png) no-repeat; 8 | background-position: 200px 0; 9 | background-size: 120px; 10 | margin-inline-start: -350px; 11 | padding-inline-start: 350px; 12 | } 13 | 14 | .container-name { 15 | font-weight: bold; 16 | } 17 | 18 | button .container-name, 19 | #current-container-name { 20 | font-weight: bold; 21 | } 22 | 23 | @media only screen and (max-width: 900px) { 24 | main { 25 | background: none; 26 | } 27 | 28 | /* for a mid sized window we have enough for this but not our image */ 29 | .title { 30 | background-image: url('chrome://global/skin/icons/info.svg'); 31 | } 32 | } 33 | 34 | html { 35 | box-sizing: border-box; 36 | font: message-box; 37 | } 38 | 39 | #redirect-url, 40 | #redirect-origin { 41 | font-weight: bold; 42 | 43 | /* max-inline-size is needed to force this text smaller than the layout at a mid-sized window */ 44 | max-inline-size: 40rem; 45 | word-break: break-all; 46 | } 47 | 48 | #redirect-url { 49 | background: #efedf0; /* Grey 20 */ 50 | border-radius: 2px; 51 | line-height: 1.5; 52 | padding-block-end: 0.5rem; 53 | padding-block-start: 0.5rem; 54 | padding-inline-end: 0.5rem; 55 | padding-inline-start: 0.5rem; 56 | } 57 | 58 | /* stylelint-disable media-feature-name-no-unknown */ 59 | @media (prefers-color-scheme: dark) { 60 | #redirect-url { 61 | background: #38383d; /* Grey 70 */ 62 | color: #eee; /* White 20 */ 63 | } 64 | } 65 | /* stylelint-enable */ 66 | 67 | #redirect-url img { 68 | block-size: 16px; 69 | inline-size: 16px; 70 | margin-inline-end: 6px; 71 | offset-block-start: 3px; 72 | position: relative; 73 | } 74 | 75 | dfn { 76 | font-style: normal; 77 | } 78 | 79 | #deny, 80 | #confirm { 81 | flex-grow: 1; 82 | } 83 | 84 | .button-container > button { 85 | min-inline-size: 240px; 86 | } 87 | 88 | .check-label { 89 | align-items: center; 90 | display: flex; 91 | } 92 | -------------------------------------------------------------------------------- /test/features/external-webextensions.test.js: -------------------------------------------------------------------------------- 1 | const {expect, initializeWithTab} = require("../common"); 2 | 3 | describe("External Webextensions", function () { 4 | const url = "http://example.com"; 5 | 6 | beforeEach(async function () { 7 | this.webExt = await initializeWithTab({ 8 | cookieStoreId: "firefox-container-4", 9 | url 10 | }); 11 | 12 | await this.webExt.popup.helper.clickElementById("always-open-in"); 13 | await this.webExt.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item", "last"); 14 | }); 15 | 16 | afterEach(function () { 17 | this.webExt.destroy(); 18 | }); 19 | 20 | describe("with contextualIdentities permissions", function () { 21 | it("should be able to get assignments", async function () { 22 | this.webExt.background.browser.management.get.resolves({ 23 | permissions: ["contextualIdentities"] 24 | }); 25 | 26 | const message = { 27 | method: "getAssignment", 28 | url 29 | }; 30 | const sender = { 31 | id: "external-webextension" 32 | }; 33 | 34 | const [promise] = this.webExt.background.browser.runtime.onMessageExternal.addListener.yield(message, sender); 35 | const answer = await promise; 36 | expect(answer.userContextId === "4").to.be.true; 37 | expect(answer.neverAsk === false).to.be.true; 38 | expect( 39 | Object.prototype.hasOwnProperty.call( 40 | answer, "identityMacAddonUUID")).to.be.true; 41 | }); 42 | }); 43 | 44 | describe("without contextualIdentities permissions", function () { 45 | it("should throw an error", async function () { 46 | this.webExt.background.browser.management.get.resolves({ 47 | permissions: [] 48 | }); 49 | 50 | const message = { 51 | method: "getAssignment", 52 | url 53 | }; 54 | const sender = { 55 | id: "external-webextension" 56 | }; 57 | 58 | const [promise] = this.webExt.background.browser.runtime.onMessageExternal.addListener.yield(message, sender); 59 | return promise.catch(error => { 60 | expect(error.message).to.equal("Missing contextualIdentities permission"); 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Requirements 4 | 5 | * Firefox 91.1.0+ 6 | * Git 2.13+ 7 | * Node 7+ 8 | 9 | ## Getting Started 10 | 11 | 1. Follow the instructions on [How to fork a repository][fork] 12 | 2. Fetch the locales: 13 | 14 | ``` 15 | cd multi-account-containers 16 | git submodule update --init 17 | ``` 18 | 3. Install the project dependencies 19 | ``` 20 | npm install --legacy-peer-deps 21 | ``` 22 | 4. Run `npm run dev`. 23 | 24 | ## Translations 25 | 26 | The translations are located in `src/_locales`. This directory is a git 27 | repository like any other. Before editing files in this folder, you need to: 28 | 29 | 1. `cd src/_locales/` 30 | 2. `git checkout -b message-updates-yyyymmdd` 31 | 3. `git push -u origin message-updates-yyyymmdd` 32 | 33 | You can then [open a pull request][pr] on [the l10n repository][l10n]. 34 | 35 | ## Tips for contributing 36 | 37 | 1. Choose [an issue][issues] that you would like to work on. 38 | 2. Fork the repository and follow the instructions for setting it up locally. 39 | 3. Run the add-on locally and try reproducing the issue. 40 | 4. Debug add-ons by clicking the “Settings” icon in about:addons, and then clicking “Debug Add-ons” 41 | 5. Click “Inspect” on the MAC add-on to open developer tools for the popup extension (see [this documentation][extension-doc] for more information) 42 | 6. Once you have a fix ready, commit your changes with the following commit message template: “Fix #: ” 43 | 7. Push your changes and open a pull request for review. 44 | 45 | If you run into an issue, you can always ask the other community members in the [discussions board][discussions]. 46 | 47 | 48 | [discussions]: https://github.com/mozilla/multi-account-containers/discussions 49 | [extension-doc]: https://extensionworkshop.com/documentation/develop/debugging/ 50 | [fork]: https://docs.github.com/en/get-started/quickstart/fork-a-repo 51 | [issues]: https://github.com/mozilla/multi-account-containers/issues 52 | [l10n]: https://github.com/mozilla-l10n/multi-account-containers-l10n/ 53 | [pr]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests 54 | [web-ext]: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Getting_started_with_web-ext -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testpilot-containers", 3 | "title": "Multi-Account Containers", 4 | "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.", 5 | "version": "8.3.5", 6 | "author": "Andrea Marchesini, Luke Crouch, Lesley Norton, Kendall Werts, Maxx Crawford, Jonathan Kingston", 7 | "bugs": { 8 | "url": "https://github.com/mozilla/multi-account-containers/issues" 9 | }, 10 | "devDependencies": { 11 | "@eslint/js": "^9.36.0", 12 | "addons-linter": "^5.28.0", 13 | "ajv": "^6.6.3", 14 | "chai": "^4.2.0", 15 | "eslint": "^9.36.0", 16 | "eslint-plugin-no-unsanitized": "^4.1.4", 17 | "eslint-plugin-promise": "^7.2.1", 18 | "globals": "^16.4.0", 19 | "htmllint-cli": "0.0.7", 20 | "json": ">=10.0.0", 21 | "mocha": "^10.1.0", 22 | "npm-run-all": "^4.0.0", 23 | "nyc": "^15.0.0", 24 | "sinon": "^7.5.0", 25 | "sinon-chai": "^3.3.0", 26 | "stylelint": "^13.5.0", 27 | "stylelint-config-standard": "^20.0.0", 28 | "stylelint-order": "^4.0.0", 29 | "web-ext": "^8.10.0", 30 | "webextensions-jsdom": "^1.2.1" 31 | }, 32 | "homepage": "https://github.com/mozilla/multi-account-containers#readme", 33 | "license": "MPL-2.0", 34 | "main": "index.js", 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/mozilla/multi-account-containers.git" 38 | }, 39 | "scripts": { 40 | "build": "web-ext build -s src/", 41 | "dev": "npm run remove-locales-github && web-ext run -s src/", 42 | "lint": "npm-run-all lint:*", 43 | "lint:addon": "./bin/addons-linter.sh", 44 | "lint:css": "stylelint src/css/*.css", 45 | "lint:html": "htmllint *.html", 46 | "lint:js": "eslint .", 47 | "package": "rm -rf src/web-ext-artifacts && npm run build && mv src/web-ext-artifacts/firefox_multi-account_containers-*.zip addon.xpi", 48 | "restore-locales-github": "cd src/_locales && git restore .github/", 49 | "remove-locales-github": "rm -rf src/_locales/.github", 50 | "test": "npm run lint && npm run coverage", 51 | "test:once": "mocha test/**/*.test.js", 52 | "test:watch": "npm run test:once -- --watch", 53 | "coverage": "nyc --reporter=html --reporter=text mocha test/**/*.test.js --timeout 60000" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/img/Sync.svg: -------------------------------------------------------------------------------- 1 | Sync -------------------------------------------------------------------------------- /src/img/webicon-twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 20 | 27 | 28 | -------------------------------------------------------------------------------- /src/css/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | --grey10: #e7e7e7; 3 | 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 5 | background: #fff; 6 | color: rgb(74, 74, 79); 7 | font-size: 13px; 8 | overflow: hidden; 9 | } 10 | 11 | h3:first-of-type { 12 | margin-block-start: 2.5rem; 13 | } 14 | 15 | label { 16 | display: flex; 17 | align-items: center; 18 | font-size: 14px; 19 | } 20 | 21 | label > span { 22 | padding-inline-end: 4px; 23 | } 24 | 25 | .settings-group { 26 | margin-block-end: 16px; 27 | } 28 | 29 | form { 30 | display: flex; 31 | flex-direction: column; 32 | padding-block-end: 1rem; 33 | } 34 | 35 | .settings-group p { 36 | margin-inline-start: 24px; 37 | margin-block: 4px 8px; 38 | } 39 | 40 | input[type="checkbox"] { 41 | margin-inline: 0 8px; 42 | margin-block: 1px auto; 43 | inline-size: 16px; 44 | block-size: 16px; 45 | } 46 | 47 | button { 48 | margin-inline: 0 auto; 49 | } 50 | 51 | .keyboard-shortcut { 52 | display: flex; 53 | flex-direction: row; 54 | justify-content: space-between; 55 | max-inline-size: 70%; 56 | align-items: center; 57 | } 58 | 59 | .bold { 60 | font-weight: 600; 61 | } 62 | 63 | .moz-vpn-proxy-permissions { 64 | margin-block: 0 2rem; 65 | padding-block-end: 1rem; 66 | border-block-end: 1px solid var(--grey10); 67 | display: flex; 68 | flex-direction: column; 69 | } 70 | 71 | h3.moz-vpn-proxy-permissions-title { 72 | margin-block-start: 0; 73 | position: relative; 74 | display: flex; 75 | align-items: center; 76 | } 77 | 78 | .warning-icon { 79 | display: flex; 80 | align-items: center; 81 | } 82 | 83 | .warning-icon.show-warning::before { 84 | background-image: url("/img/warning.svg"); 85 | background-size: 24px; 86 | background-repeat: no-repeat; 87 | background-position: center; 88 | content: ""; 89 | display: block; 90 | block-size: 24px; 91 | inline-size: 24px; 92 | margin-inline-end: 0.5rem; 93 | } 94 | 95 | .moz-vpn-proxy-permissions-title::before, 96 | .moz-vpn-proxy-permissions-title::after { 97 | background-color: var(--grey10); 98 | content: ""; 99 | height: 1px; 100 | flex: 1 1 0%; 101 | } 102 | 103 | h3.moz-vpn-proxy-permissions-title::before { 104 | margin-inline-end: 2rem; 105 | margin-inline-start: -50%; 106 | } 107 | 108 | h3.moz-vpn-proxy-permissions-title::after { 109 | margin-inline-start: 2rem; 110 | margin-inline-end: -50%; 111 | } 112 | 113 | @media (prefers-color-scheme: dark) { 114 | body { 115 | background: #23212a; 116 | color: #fff; 117 | } 118 | 119 | p { 120 | color: rgb(177, 177, 179); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/js/proxified-containers.js: -------------------------------------------------------------------------------- 1 | // This object allows other scripts to access the list mapping containers to their proxies 2 | proxifiedContainers = { 3 | 4 | async retrieveAll() { 5 | const result = await browser.storage.local.get("proxifiedContainersKey"); 6 | if(!result || !result["proxifiedContainersKey"]) { 7 | return null; 8 | } 9 | 10 | return result["proxifiedContainersKey"]; 11 | }, 12 | 13 | async retrieve(cookieStoreId) { 14 | const result = await this.retrieveAll(); 15 | if(!result) { 16 | return null; 17 | } 18 | 19 | return result.find(o => o.cookieStoreId === cookieStoreId); 20 | }, 21 | 22 | async set(cookieStoreId, proxy) { 23 | // Assumes proxy is a properly formatted object 24 | let proxifiedContainersStore = await proxifiedContainers.retrieveAll(); 25 | if (!proxifiedContainersStore) proxifiedContainersStore = []; 26 | 27 | const index = proxifiedContainersStore.findIndex(i => i.cookieStoreId === cookieStoreId); 28 | if (index === -1) { 29 | proxifiedContainersStore.push({ 30 | cookieStoreId: cookieStoreId, 31 | proxy: proxy 32 | }); 33 | } else { 34 | proxifiedContainersStore[index] = { 35 | cookieStoreId: cookieStoreId, 36 | proxy: proxy 37 | }; 38 | } 39 | 40 | await browser.storage.local.set({ 41 | proxifiedContainersKey: proxifiedContainersStore 42 | }); 43 | }, 44 | 45 | // Parses a proxy description string of the format type://host[:port] or type://username:password@host[:port] (port is optional) 46 | parseProxy(proxy_str, mozillaVpnData = null) { 47 | const proxyRegexp = /(?(https?)|(socks4?)):\/\/(\b(?[\w-]+):(?[\w-]+)@)?(?((?:\d{1,3}\.){3}\d{1,3}\b)|(\b([\w.-]+)+))(:(?\d+))?/; 48 | const matches = proxyRegexp.exec(proxy_str); 49 | if (!matches) { 50 | return false; 51 | } 52 | 53 | if (mozillaVpnData && mozillaVpnData.mozProxyEnabled === undefined) { 54 | matches.groups.type = null; 55 | } 56 | 57 | if (!mozillaVpnData) { 58 | mozillaVpnData = MozillaVPN.getMozillaProxyInfoObj(); 59 | } 60 | 61 | return {...matches.groups,...mozillaVpnData}; 62 | }, 63 | 64 | // Deletes the proxy information object for a specified cookieStoreId [useful for cleaning] 65 | async delete(cookieStoreId) { 66 | // Assumes proxy is a properly formatted object 67 | const proxifiedContainersStore = await proxifiedContainers.retrieveAll(); 68 | const index = proxifiedContainersStore.findIndex(i => i.cookieStoreId === cookieStoreId); 69 | if (index !== -1) { 70 | proxifiedContainersStore.splice(index, 1); 71 | } 72 | await browser.storage.local.set({ 73 | proxifiedContainersKey: proxifiedContainersStore 74 | }); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const { 2 | defineConfig, 3 | globalIgnores, 4 | } = require("eslint/config"); 5 | 6 | const globals = require("globals"); 7 | const promise = require("eslint-plugin-promise"); 8 | const noUnsanitized = require("eslint-plugin-no-unsanitized"); 9 | const js = require("@eslint/js"); 10 | 11 | module.exports = defineConfig([{ 12 | languageOptions: { 13 | "ecmaVersion": 2021, 14 | parserOptions: {}, 15 | 16 | globals: { 17 | ...globals.browser, 18 | ...globals.node, 19 | ...globals.webextensions, 20 | "Utils": true, 21 | "CustomizableUI": true, 22 | "CustomizableWidgets": true, 23 | "SessionStore": true, 24 | "Services": true, 25 | "Components": true, 26 | "XPCOMUtils": true, 27 | "OS": true, 28 | "ADDON_UNINSTALL": true, 29 | "ADDON_DISABLE": true, 30 | "CONTAINER_ORDER_STORAGE_KEY": true, 31 | "proxifiedContainers": true, 32 | "MozillaVPN": true, 33 | "MozillaVPN_Background": true, 34 | }, 35 | }, 36 | 37 | plugins: { 38 | js, 39 | promise, 40 | "no-unsanitized": noUnsanitized, 41 | }, 42 | 43 | extends: ["js/recommended"], 44 | 45 | "rules": { 46 | "promise/always-return": "off", 47 | "promise/avoid-new": "off", 48 | "promise/catch-or-return": "error", 49 | "promise/no-callback-in-promise": "warn", 50 | "promise/no-native": "off", 51 | "promise/no-nesting": "warn", 52 | "promise/no-promise-in-callback": "warn", 53 | "promise/no-return-wrap": "error", 54 | "promise/param-names": "error", 55 | "no-unsanitized/method": ["error"], 56 | 57 | "no-unsanitized/property": ["error", { 58 | "escape": { 59 | "taggedTemplates": ["Utils.escaped"], 60 | }, 61 | }], 62 | 63 | "eqeqeq": "error", 64 | "indent": ["error", 2], 65 | "linebreak-style": ["error", "unix"], 66 | "no-throw-literal": "error", 67 | "no-warning-comments": "warn", 68 | "no-var": "error", 69 | "prefer-const": "error", 70 | "quotes": ["error", "double"], 71 | "radix": "error", 72 | "semi": ["error", "always"], 73 | }, 74 | }, 75 | 76 | { 77 | files: ["test/**/*.js"], 78 | languageOptions: { 79 | globals: { 80 | ...globals.mocha, 81 | }, 82 | }, 83 | "rules": { 84 | "no-restricted-globals": ["error", "browser"], 85 | }, 86 | }, 87 | 88 | { 89 | files: ["src/js/**/*.js"], 90 | languageOptions: { 91 | globals: { 92 | "assignManager": true, 93 | "badge": true, 94 | "backgroundLogic": true, 95 | "identityState": true, 96 | "messageHandler": true, 97 | "sync": true, 98 | }, 99 | }, 100 | }, 101 | 102 | globalIgnores(["lib/testpilot/*.js", "**/coverage"])]); 103 | -------------------------------------------------------------------------------- /src/img/amo-icon.svg: -------------------------------------------------------------------------------- 1 | Created with Sketch. -------------------------------------------------------------------------------- /test/features/assignment.test.js: -------------------------------------------------------------------------------- 1 | const {initializeWithTab} = require("../common"); 2 | 3 | describe("Assignment Reopen Feature", function () { 4 | const url = "http://example.com"; 5 | 6 | beforeEach(async function () { 7 | this.webExt = await initializeWithTab({ 8 | cookieStoreId: "firefox-default", 9 | url 10 | }); 11 | }); 12 | 13 | afterEach(function () { 14 | this.webExt.destroy(); 15 | }); 16 | 17 | describe("set to 'Always open in' firefox-container-4", function () { 18 | beforeEach(async function () { 19 | // popup click to set assignment for activeTab.url 20 | await this.webExt.popup.helper.clickElementById("always-open-in"); 21 | await this.webExt.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item"); 22 | }); 23 | 24 | it("should open the page in the assigned container", async function () { 25 | // should have created a new tab with the confirm page 26 | this.webExt.background.browser.tabs.create.should.have.been.calledWithMatch({ 27 | active: true, 28 | cookieStoreId: "firefox-container-4", 29 | index: 1, 30 | openerTabId: null, 31 | url: "http://example.com" 32 | }); 33 | }); 34 | 35 | }); 36 | 37 | }); 38 | 39 | describe("Assignment Comfirm Page Feature", function () { 40 | const url = "http://example.com"; 41 | 42 | beforeEach(async function () { 43 | this.webExt = await initializeWithTab({ 44 | cookieStoreId: "firefox-container-4", 45 | url 46 | }); 47 | }); 48 | 49 | afterEach(function () { 50 | this.webExt.destroy(); 51 | }); 52 | 53 | describe("open new Tab with the assigned URL in the default container", function () { 54 | let newTab; 55 | beforeEach(async function () { 56 | await this.webExt.popup.helper.clickElementById("always-open-in"); 57 | await this.webExt.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item"); 58 | 59 | // new Tab opening activeTab.url in default container 60 | newTab = await this.webExt.background.browser.tabs._create({ 61 | cookieStoreId: "firefox-default", 62 | url 63 | }, { 64 | options: { 65 | webRequestError: true // because request is canceled due to reopening 66 | } 67 | }); 68 | }); 69 | 70 | it("should open the confirm page", async function () { 71 | // should have created a new tab with the confirm page 72 | this.webExt.background.browser.tabs.create.should.have.been.calledWithMatch({ 73 | url: "moz-extension://fake/confirm-page.html?" + 74 | `url=${encodeURIComponent(url)}` + 75 | `&cookieStoreId=${this.webExt.tab.cookieStoreId}`, 76 | cookieStoreId: undefined, 77 | openerTabId: null, 78 | index: 2, 79 | active: true 80 | }); 81 | }); 82 | 83 | it("should remove the new Tab that got opened in the default container", function () { 84 | this.webExt.background.browser.tabs.remove.should.have.been.calledWith(newTab.id); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | if (!process.listenerCount("unhandledRejection")) { 2 | process.on("unhandledRejection", r => console.log(r)); 3 | } 4 | 5 | const path = require("path"); 6 | const chai = require("chai"); 7 | const sinonChai = require("sinon-chai"); 8 | const crypto = require("crypto"); 9 | const sinon = require("sinon"); 10 | const expect = chai.expect; 11 | chai.should(); 12 | chai.use(sinonChai); 13 | const nextTick = () => { 14 | return new Promise(resolve => { 15 | setTimeout(() => { 16 | process.nextTick(resolve); 17 | }); 18 | }); 19 | }; 20 | 21 | const webExtensionsJSDOM = require("webextensions-jsdom"); 22 | const manifestPath = path.resolve(path.join(__dirname, "../src/manifest.json")); 23 | 24 | const buildDom = async ({background = {}, popup = {}}) => { 25 | background = { 26 | ...background, 27 | jsdom: { 28 | ...background.jsom, 29 | beforeParse(window) { 30 | window.browser.permissions.getAll.resolves({permissions: ["bookmarks"]}); 31 | window.crypto = { 32 | getRandomValues: arr => crypto.randomBytes(arr.length), 33 | }; 34 | // By default, the mock contextMenus.remove() returns undefined; 35 | // Let it return a Promise instead, so that .then() calls chained to 36 | // it (in src/js/background/assignManager.js) do not fail. 37 | window.browser.contextMenus.remove.resolves(); 38 | } 39 | } 40 | }; 41 | 42 | popup = { 43 | ...popup, 44 | jsdom: { 45 | ...popup.jsdom, 46 | pretendToBeVisual: true 47 | } 48 | }; 49 | 50 | const webExtension = await webExtensionsJSDOM.fromManifest(manifestPath, { 51 | apiFake: true, 52 | wiring: true, 53 | sinon: global.sinon, 54 | background, 55 | popup 56 | }); 57 | 58 | webExtension.browser = webExtension.background.browser; 59 | return webExtension; 60 | }; 61 | 62 | const buildBackgroundDom = background => { 63 | return buildDom({ 64 | background, 65 | popup: false 66 | }); 67 | }; 68 | 69 | const buildPopupDom = popup => { 70 | return buildDom({ 71 | popup, 72 | background: false 73 | }); 74 | }; 75 | 76 | const initializeWithTab = async (details = { 77 | cookieStoreId: "firefox-default" 78 | }) => { 79 | let tab; 80 | const webExtension = await buildDom({ 81 | background: { 82 | async afterBuild(background) { 83 | tab = await background.browser.tabs._create(details); 84 | } 85 | }, 86 | popup: { 87 | jsdom: { 88 | beforeParse(window) { 89 | window.browser.storage.local.set({ 90 | "browserActionBadgesClicked": [], 91 | "onboarding-stage": 7, 92 | "achievements": [], 93 | "syncEnabled": true, 94 | "replaceTabEnabled": false, 95 | "mozillaVpnInstalled": false, 96 | }); 97 | window.browser.storage.local.set.resetHistory(); 98 | window.browser.storage.sync.clear(); 99 | } 100 | } 101 | } 102 | }); 103 | webExtension.tab = tab; 104 | 105 | return webExtension; 106 | }; 107 | 108 | module.exports = { 109 | buildDom, 110 | buildBackgroundDom, 111 | buildPopupDom, 112 | initializeWithTab, 113 | sinon, 114 | expect, 115 | nextTick, 116 | }; 117 | -------------------------------------------------------------------------------- /src/js/background/mozillaVpnBackground.js: -------------------------------------------------------------------------------- 1 | const MozillaVPN_Background = { 2 | MOZILLA_VPN_SERVERS_KEY: "mozillaVpnServers", 3 | MOZILLA_VPN_HIDDEN_TOUTS_LIST_KEY: "mozillaVpnHiddenToutsList", 4 | 5 | _isolationKey: 0, 6 | 7 | async maybeInitPort() { 8 | if (this.port && this.port.error === null) { 9 | return; 10 | } 11 | try { 12 | /* 13 | Find a way to not spam the console when MozillaVPN client is not installed 14 | File at path ".../../MozillaVPN/..." is not executable.` thrown by resource://gre/modules/Subprocess.jsm:152` 15 | Which does is not caught by this try/catch 16 | */ 17 | this.port = await browser.runtime.connectNative("mozillavpn"); 18 | this.port.onMessage.addListener(response => this.handleResponse(response)); 19 | 20 | this.port.onMessage.addListener(this.handleResponse); 21 | this.postToApp("status"); 22 | this.postToApp("servers"); 23 | 24 | // When the mozillavpn dies or the VPN disconnects, we need to increase 25 | // the isolation key in order to create new proxy connections. Otherwise 26 | // we could see random timeout when the browser tries to connect to an 27 | // invalid proxy connection. 28 | this.port.onDisconnect.addListener(() => this.increaseIsolationKey()); 29 | 30 | } catch { 31 | this._installed = false; 32 | this._connected = false; 33 | } 34 | }, 35 | 36 | async init() { 37 | const { mozillaVpnServers } = await browser.storage.local.get(this.MOZILLA_VPN_SERVERS_KEY); 38 | if (typeof(mozillaVpnServers) === "undefined") { 39 | await browser.storage.local.set({ [this.MOZILLA_VPN_SERVERS_KEY]:[] }); 40 | await browser.storage.local.set({ [this.MOZILLA_VPN_HIDDEN_TOUTS_LIST_KEY]:[] }); 41 | this._installed = false; 42 | this._connected = false; 43 | } 44 | this.maybeInitPort(); 45 | }, 46 | 47 | getConnectionStatus() { 48 | return this._connected; 49 | }, 50 | 51 | getInstallationStatus() { 52 | return this._installed; 53 | }, 54 | 55 | // Post messages to MozillaVPN client 56 | postToApp(message) { 57 | try { 58 | this.port.postMessage({t: message}); 59 | } catch(e) { 60 | if (e.message === "Attempt to postMessage on disconnected port") { 61 | this._installed = false; 62 | this._connected = false; 63 | } 64 | } 65 | }, 66 | 67 | // Handle responses from MozillaVPN client 68 | async handleResponse(response) { 69 | MozillaVPN_Background._installed = true; 70 | if (response.error && response.error === "vpn-client-down") { 71 | MozillaVPN_Background._connected = false; 72 | return; 73 | } 74 | if (response.servers) { 75 | const servers = response.servers.countries; 76 | browser.storage.local.set({ [MozillaVPN_Background.MOZILLA_VPN_SERVERS_KEY]: servers}); 77 | return; 78 | } 79 | 80 | if ((response.status && response.status.vpn) || response.t === "status") { 81 | const status = response.status ? response.status.vpn : response.vpn; 82 | 83 | if (status === "StateOn") { 84 | MozillaVPN_Background._connected = true; 85 | } 86 | 87 | if (status === "StateOff" || status === "StateDisconnecting") { 88 | MozillaVPN_Background._connected = false; 89 | } 90 | 91 | // Let's increase the network key isolation at any vpn status change. 92 | MozillaVPN_Background.increaseIsolationKey(); 93 | } 94 | }, 95 | 96 | increaseIsolationKey() { 97 | ++this._isolationKey; 98 | }, 99 | 100 | get isolationKey() { 101 | return this._isolationKey; 102 | }, 103 | 104 | async removeMozillaVpnProxies() { 105 | const proxies = await proxifiedContainers.retrieveAll(); 106 | if (!proxies) { 107 | return; 108 | } 109 | for (const proxyObj of proxies) { 110 | const { proxy } = proxyObj; 111 | if (proxy.countryCode !== undefined) { 112 | await proxifiedContainers.delete(proxyObj.cookieStoreId); 113 | } 114 | } 115 | }, 116 | }; 117 | 118 | MozillaVPN_Background.init(); 119 | -------------------------------------------------------------------------------- /src/js/confirm-page.js: -------------------------------------------------------------------------------- 1 | async function load() { 2 | const searchParams = new URL(window.location).searchParams; 3 | const redirectUrl = searchParams.get("url"); 4 | const cookieStoreId = searchParams.get("cookieStoreId"); 5 | const currentCookieStoreId = searchParams.get("currentCookieStoreId"); 6 | const redirectUrlElement = document.getElementById("redirect-url"); 7 | redirectUrlElement.textContent = redirectUrl; 8 | appendFavicon(redirectUrl, redirectUrlElement); 9 | 10 | // Option for staying on the previous container 11 | document.getElementById("deny").addEventListener("click", (e) => { 12 | e.preventDefault(); 13 | denySubmit(redirectUrl, currentCookieStoreId); 14 | }); 15 | 16 | // Option for going to the default container (no container) 17 | document.getElementById("deny-no-container").addEventListener("click", (e) => { 18 | e.preventDefault(); 19 | denySubmit(redirectUrl, currentCookieStoreId); 20 | }); 21 | 22 | const container = await browser.contextualIdentities.get(cookieStoreId); 23 | const currentContainer = currentCookieStoreId ? await browser.contextualIdentities.get(currentCookieStoreId) : null; 24 | const currentContainerName = currentContainer ? setDenyButton(currentContainer.name) : setDenyButton(""); 25 | 26 | document.querySelectorAll("[data-message-id]").forEach(el => { 27 | const elementData = el.dataset; 28 | const containerName = elementData.messageArg === "container-name" ? container.name : currentContainerName; 29 | el.textContent = browser.i18n.getMessage(elementData.messageId, containerName); 30 | }); 31 | 32 | // Option for going to newly selected container 33 | document.getElementById("confirm").addEventListener("click", (e) => { 34 | e.preventDefault(); 35 | confirmSubmit(redirectUrl, cookieStoreId); 36 | }); 37 | } 38 | 39 | function setDenyButton(currentContainerName) { 40 | const buttonDeny = document.getElementById("deny"); 41 | const buttonDenyNoContainer = document.getElementById("deny-no-container"); 42 | 43 | if (currentContainerName) { 44 | buttonDenyNoContainer.style.display = "none"; 45 | return currentContainerName; 46 | } 47 | buttonDeny.style.display = "none"; 48 | return; 49 | } 50 | 51 | function appendFavicon(pageUrl, redirectUrlElement) { 52 | const origin = new URL(pageUrl).origin; 53 | const favIconElement = Utils.createFavIconElement(`${origin}/favicon.ico`); 54 | 55 | redirectUrlElement.prepend(favIconElement); 56 | } 57 | 58 | function confirmSubmit(redirectUrl, cookieStoreId) { 59 | const neverAsk = document.getElementById("never-ask").checked; 60 | // Sending neverAsk message to background to store for next time we see this process 61 | if (neverAsk) { 62 | browser.runtime.sendMessage({ 63 | method: "neverAsk", 64 | neverAsk: true, 65 | cookieStoreId: cookieStoreId, 66 | pageUrl: redirectUrl 67 | }); 68 | } 69 | openInContainer(redirectUrl, cookieStoreId); 70 | } 71 | 72 | /** 73 | * @returns {Promise} 74 | */ 75 | async function getCurrentTab() { 76 | const tabs = await browser.tabs.query({ 77 | active: true, 78 | windowId: browser.windows.WINDOW_ID_CURRENT 79 | }); 80 | return tabs[0]; 81 | } 82 | 83 | async function denySubmit(redirectUrl, currentCookieStoreId) { 84 | const tab = await getCurrentTab(); 85 | const currentContainer = currentCookieStoreId ? await browser.contextualIdentities.get(currentCookieStoreId) : null; 86 | const neverAsk = document.getElementById("never-ask").checked; 87 | 88 | if (neverAsk) { 89 | await browser.runtime.sendMessage({ 90 | method: "neverAsk", 91 | neverAsk: true, 92 | cookieStoreId: currentCookieStoreId, 93 | pageUrl: redirectUrl, 94 | defaultContainer: !currentContainer 95 | }); 96 | } 97 | 98 | await browser.runtime.sendMessage({ 99 | method: "exemptContainerAssignment", 100 | tabId: tab.id, 101 | pageUrl: redirectUrl 102 | }); 103 | document.location.replace(redirectUrl); 104 | } 105 | 106 | load(); 107 | 108 | async function openInContainer(redirectUrl, cookieStoreId) { 109 | const tab = await getCurrentTab(); 110 | const reopenedTab = await browser.tabs.create({ 111 | index: tab.index + 1, 112 | cookieStoreId, 113 | url: redirectUrl 114 | }); 115 | if (tab.groupId >= 0) { 116 | // If the original tab was in a tab group, make sure that the reopened tab 117 | // stays in the same tab group. 118 | await browser.tabs.group({ groupId: tab.groupId, tabIds: reopenedTab.id }); 119 | } 120 | await browser.tabs.remove(tab.id); 121 | } 122 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Firefox Multi-Account Containers", 4 | "version": "8.3.5", 5 | "incognito": "not_allowed", 6 | "description": "__MSG_extensionDescription__", 7 | "icons": { 8 | "48": "img/multiaccountcontainer-16.svg", 9 | "96": "img/multiaccountcontainer-16.svg" 10 | }, 11 | "homepage_url": "https://github.com/mozilla/multi-account-containers#readme", 12 | "permissions": [ 13 | "", 14 | "activeTab", 15 | "cookies", 16 | "contextMenus", 17 | "contextualIdentities", 18 | "history", 19 | "idle", 20 | "management", 21 | "storage", 22 | "unlimitedStorage", 23 | "tabs", 24 | "webRequestBlocking", 25 | "webRequest" 26 | ], 27 | "optional_permissions": [ 28 | "bookmarks", 29 | "browsingData", 30 | "nativeMessaging", 31 | "proxy" 32 | ], 33 | "browser_specific_settings": { 34 | "gecko": { 35 | "id": "@testpilot-containers", 36 | "strict_min_version": "91.1.0", 37 | "data_collection_permissions": { 38 | "required": ["none"] 39 | } 40 | } 41 | }, 42 | "commands": { 43 | "_execute_browser_action": { 44 | "suggested_key": { 45 | "default": "Ctrl+Period", 46 | "mac": "MacCtrl+Period" 47 | }, 48 | "description": "__MSG_openContainerPanel__" 49 | }, 50 | "sort_tabs": { 51 | "description": "__MSG_sortTabsByContainer__" 52 | }, 53 | "open_container_0": { 54 | "suggested_key": { 55 | "default": "Ctrl+Shift+1" 56 | }, 57 | "description": "__MSG_containerShortcut__" 58 | }, 59 | "open_container_1": { 60 | "suggested_key": { 61 | "default": "Ctrl+Shift+2" 62 | }, 63 | "description": "__MSG_containerShortcut__" 64 | }, 65 | "open_container_2": { 66 | "suggested_key": { 67 | "default": "Ctrl+Shift+3" 68 | }, 69 | "description": "__MSG_containerShortcut__" 70 | }, 71 | "open_container_3": { 72 | "suggested_key": { 73 | "default": "Ctrl+Shift+4" 74 | }, 75 | "description": "__MSG_containerShortcut__" 76 | }, 77 | "open_container_4": { 78 | "suggested_key": { 79 | "default": "Ctrl+Shift+5" 80 | }, 81 | "description": "__MSG_containerShortcut__" 82 | }, 83 | "open_container_5": { 84 | "suggested_key": { 85 | "default": "Ctrl+Shift+6" 86 | }, 87 | "description": "__MSG_containerShortcut__" 88 | }, 89 | "open_container_6": { 90 | "suggested_key": { 91 | "default": "Ctrl+Shift+7" 92 | }, 93 | "description": "__MSG_containerShortcut__" 94 | }, 95 | "open_container_7": { 96 | "suggested_key": { 97 | "default": "Ctrl+Shift+8" 98 | }, 99 | "description": "__MSG_containerShortcut__" 100 | }, 101 | "open_container_8": { 102 | "suggested_key": { 103 | "default": "Ctrl+Shift+9" 104 | }, 105 | "description": "__MSG_containerShortcut__" 106 | }, 107 | "open_container_9": { 108 | "suggested_key": { 109 | "default": "Ctrl+Shift+0" 110 | }, 111 | "description": "__MSG_containerShortcut__" 112 | } 113 | }, 114 | "browser_action": { 115 | "browser_style": true, 116 | "default_icon": "img/multiaccountcontainer-16.svg", 117 | "default_title": "Firefox Multi-Account Containers", 118 | "default_popup": "popup.html", 119 | "default_area": "navbar", 120 | "theme_icons": [ 121 | { 122 | "light": "img/multiaccountcontainer-16.svg", 123 | "dark": "img/multiaccountcontainer-16.svg", 124 | "size": 32 125 | } 126 | ] 127 | }, 128 | "page_action": { 129 | "browser_style": true, 130 | "default_icon": "img/container-openin-16.svg", 131 | "default_title": "__MSG_alwaysOpenSiteInContainer__", 132 | "default_popup": "pageActionPopup.html", 133 | "pinned": false, 134 | "show_matches": ["*://*/*"] 135 | }, 136 | "background": { 137 | "page": "js/background/index.html" 138 | }, 139 | "content_scripts": [ 140 | { 141 | "matches": [ 142 | "" 143 | ], 144 | "js": [ 145 | "js/content-script.js" 146 | ], 147 | "css": [ 148 | "css/content.css" 149 | ], 150 | "run_at": "document_start" 151 | } 152 | ], 153 | "default_locale": "en", 154 | "web_accessible_resources": [ 155 | "/img/multiaccountcontainer-16.svg" 156 | ], 157 | "options_ui": { 158 | "page": "options.html", 159 | "browser_style": true 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/js/options.js: -------------------------------------------------------------------------------- 1 | const NUMBER_OF_KEYBOARD_SHORTCUTS = 10; 2 | 3 | async function setUpCheckBoxes() { 4 | document.querySelectorAll("[data-permission-id]").forEach(async(el) => { 5 | const permissionId = el.dataset.permissionId; 6 | const permissionEnabled = await browser.permissions.contains({ permissions: [permissionId] }); 7 | el.checked = !!permissionEnabled; 8 | }); 9 | } 10 | 11 | function disablePermissionsInputs() { 12 | document.querySelectorAll("[data-permission-id").forEach(el => { 13 | el.disabled = true; 14 | }); 15 | } 16 | 17 | function enablePermissionsInputs() { 18 | document.querySelectorAll("[data-permission-id").forEach(el => { 19 | el.disabled = false; 20 | }); 21 | } 22 | 23 | document.querySelectorAll("[data-permission-id").forEach(async(el) => { 24 | const permissionId = el.dataset.permissionId; 25 | el.addEventListener("change", async() => { 26 | if (el.checked) { 27 | disablePermissionsInputs(); 28 | const granted = await browser.permissions.request({ permissions: [permissionId] }); 29 | if (!granted) { 30 | el.checked = false; 31 | enablePermissionsInputs(); 32 | } 33 | return; 34 | } 35 | await browser.permissions.remove({ permissions: [permissionId] }); 36 | }); 37 | }); 38 | 39 | async function maybeShowPermissionsWarningIcon() { 40 | const bothMozillaVpnPermissionsEnabled = await MozillaVPN.bothPermissionsEnabled(); 41 | const permissionsWarningEl = document.querySelector(".warning-icon"); 42 | permissionsWarningEl.classList.toggle("show-warning", !bothMozillaVpnPermissionsEnabled); 43 | } 44 | 45 | async function enableDisableSync() { 46 | const checkbox = document.querySelector("#syncCheck"); 47 | await browser.storage.local.set({syncEnabled: !!checkbox.checked}); 48 | browser.runtime.sendMessage({ method: "resetSync" }); 49 | } 50 | 51 | async function enableDisableReplaceTab() { 52 | const checkbox = document.querySelector("#replaceTabCheck"); 53 | await browser.storage.local.set({replaceTabEnabled: !!checkbox.checked}); 54 | } 55 | 56 | async function changeTheme(event) { 57 | const theme = event.currentTarget; 58 | await browser.storage.local.set({currentTheme: theme.value}); 59 | await browser.storage.local.set({currentThemeId: theme.selectedIndex}); 60 | } 61 | 62 | async function setupOptions() { 63 | const { syncEnabled } = await browser.storage.local.get("syncEnabled"); 64 | const { replaceTabEnabled } = await browser.storage.local.get("replaceTabEnabled"); 65 | const { currentThemeId } = await browser.storage.local.get("currentThemeId"); 66 | 67 | document.querySelector("#syncCheck").checked = !!syncEnabled; 68 | document.querySelector("#replaceTabCheck").checked = !!replaceTabEnabled; 69 | document.querySelector("#changeTheme").selectedIndex = currentThemeId; 70 | setupContainerShortcutSelects(); 71 | } 72 | 73 | async function setupContainerShortcutSelects () { 74 | const keyboardShortcut = await browser.runtime.sendMessage({method: "getShortcuts"}); 75 | const identities = await browser.contextualIdentities.query({}); 76 | const fragment = document.createDocumentFragment(); 77 | const noneOption = document.createElement("option"); 78 | noneOption.value = "none"; 79 | noneOption.id = "none"; 80 | noneOption.textContent = "None"; 81 | fragment.append(noneOption); 82 | 83 | for (const identity of identities) { 84 | const option = document.createElement("option"); 85 | option.value = identity.cookieStoreId; 86 | option.id = identity.cookieStoreId; 87 | option.textContent = identity.name; 88 | fragment.append(option); 89 | } 90 | 91 | for (let i=0; i < NUMBER_OF_KEYBOARD_SHORTCUTS; i++) { 92 | const shortcutKey = "open_container_"+i; 93 | const shortcutSelect = document.getElementById(shortcutKey); 94 | shortcutSelect.appendChild(fragment.cloneNode(true)); 95 | if (keyboardShortcut && keyboardShortcut[shortcutKey]) { 96 | const cookieStoreId = keyboardShortcut[shortcutKey]; 97 | shortcutSelect.querySelector("#" + cookieStoreId).selected = true; 98 | } 99 | } 100 | } 101 | 102 | function storeShortcutChoice (event) { 103 | browser.runtime.sendMessage({ 104 | method: "setShortcut", 105 | shortcut: event.target.id, 106 | cookieStoreId: event.target.value 107 | }); 108 | } 109 | 110 | function resetOnboarding() { 111 | browser.storage.local.set({"onboarding-stage": 0}); 112 | } 113 | 114 | async function resetPermissionsUi() { 115 | await maybeShowPermissionsWarningIcon(); 116 | await setUpCheckBoxes(); 117 | enablePermissionsInputs(); 118 | } 119 | 120 | browser.permissions.onAdded.addListener(resetPermissionsUi); 121 | browser.permissions.onRemoved.addListener(resetPermissionsUi); 122 | 123 | document.addEventListener("DOMContentLoaded", setupOptions); 124 | document.querySelector("#syncCheck").addEventListener( "change", enableDisableSync); 125 | document.querySelector("#replaceTabCheck").addEventListener( "change", enableDisableReplaceTab); 126 | document.querySelector("#changeTheme").addEventListener( "change", changeTheme); 127 | 128 | maybeShowPermissionsWarningIcon(); 129 | for (let i=0; i < NUMBER_OF_KEYBOARD_SHORTCUTS; i++) { 130 | document.querySelector("#open_container_"+i) 131 | .addEventListener("change", storeShortcutChoice); 132 | } 133 | 134 | document.querySelectorAll("[data-btn-id]").forEach(btn => { 135 | btn.addEventListener("click", () => { 136 | switch (btn.dataset.btnId) { 137 | case "reset-onboarding": 138 | resetOnboarding(); 139 | break; 140 | case "moz-vpn-learn-more": 141 | browser.tabs.create({ 142 | url: MozillaVPN.attachUtmParameters("https://support.mozilla.org/kb/protect-your-container-tabs-mozilla-vpn", "options-learn-more") 143 | }); 144 | break; 145 | } 146 | }); 147 | }); 148 | resetPermissionsUi(); 149 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

15 |
16 | 20 |

21 |
22 |
23 |

24 | 25 |

26 |
27 |
28 | 32 |

33 |
34 |
35 | 39 |

40 |
41 |
42 |
43 |

44 |
45 | 49 |

50 |
51 | 52 |

53 | 54 |
55 | 59 |

60 |
61 | 62 | 66 |

67 | 68 |

79 | 80 |

81 |

82 | 83 |

88 |

93 |

98 |

103 |

108 |

113 |

118 |

123 |

128 |

133 |

134 | 135 |

136 |

Mozilla VPN

137 | 138 |
139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /test/issues/940.test.js: -------------------------------------------------------------------------------- 1 | const {expect, sinon, initializeWithTab} = require("../common"); 2 | 3 | describe("#940", function () { 4 | describe("when other onBeforeRequestHandlers are faster and redirect with the same requestId", function () { 5 | it("should not open two confirm pages", async function () { 6 | const webExtension = await initializeWithTab({ 7 | cookieStoreId: "firefox-container-4", 8 | url: "http://example.com" 9 | }); 10 | 11 | await webExtension.popup.helper.clickElementById("always-open-in"); 12 | await webExtension.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item"); 13 | 14 | const responses = {}; 15 | await webExtension.background.browser.tabs._create({ 16 | url: "https://example.com" 17 | }, { 18 | options: { 19 | webRequestRedirects: ["https://example.com"], 20 | webRequestError: true, 21 | instantRedirects: true 22 | }, 23 | responses 24 | }); 25 | 26 | const result = await responses.webRequest.onBeforeRequest[1]; 27 | expect(result).to.deep.equal({ 28 | cancel: true 29 | }); 30 | webExtension.browser.tabs.create.should.have.been.calledOnce; 31 | 32 | webExtension.destroy(); 33 | }); 34 | }); 35 | 36 | describe("when redirects change requestId midflight", function () { 37 | beforeEach(async function () { 38 | 39 | this.webExt = await initializeWithTab({ 40 | cookieStoreId: "firefox-container-4", 41 | url: "https://www.youtube.com" 42 | }); 43 | 44 | await this.webExt.popup.helper.clickElementById("always-open-in"); 45 | await this.webExt.popup.helper.clickElementByQuerySelectorAll("#picker-identities-list > .menu-item"); 46 | 47 | global.clock = sinon.useFakeTimers(); 48 | this.redirectedRequest = async (options = {}) => { 49 | const newTabResponses = {}; 50 | const newTab = await this.webExt.browser.tabs._create({ 51 | url: "http://youtube.com" 52 | }, { 53 | options: Object.assign({ 54 | webRequestRedirects: [ 55 | "https://youtube.com", 56 | "https://www.youtube.com", 57 | { 58 | url: "https://www.youtube.com", 59 | webRequest: { 60 | requestId: 2 61 | } 62 | } 63 | ], 64 | webRequestError: true, 65 | instantRedirects: true 66 | }, options), 67 | responses: newTabResponses 68 | }); 69 | 70 | return [newTabResponses, newTab]; 71 | }; 72 | }); 73 | 74 | afterEach(function () { 75 | this.webExt.destroy(); 76 | global.clock.restore(); 77 | }); 78 | 79 | it("should not open two confirm pages", async function () { 80 | const [newTabResponses] = await this.redirectedRequest(); 81 | 82 | // http://youtube.com is not assigned, no cancel, no reopening 83 | expect(await newTabResponses.webRequest.onBeforeRequest[0]).to.deep.equal({}); 84 | 85 | // https://youtube.com is not assigned, no cancel, no reopening 86 | expect(await newTabResponses.webRequest.onBeforeRequest[1]).to.deep.equal({}); 87 | 88 | // https://www.youtube.com is assigned, this triggers reopening, cancel 89 | expect(await newTabResponses.webRequest.onBeforeRequest[2]).to.deep.equal({ 90 | cancel: true 91 | }); 92 | 93 | // https://www.youtube.com is assigned, this was a redirect, cancel early, no reopening 94 | expect(await newTabResponses.webRequest.onBeforeRequest[3]).to.deep.equal({ 95 | cancel: true 96 | }); 97 | 98 | this.webExt.background.browser.tabs.create.should.have.been.calledOnce; 99 | }); 100 | 101 | it("should uncancel after webRequest.onCompleted", async function () { 102 | const [newTabResponses, newTab] = await this.redirectedRequest(); 103 | // remove onCompleted listeners because in the real world this request would never complete 104 | // and thus might trigger unexpected behavior because the tab gets removed when reopening 105 | this.webExt.background.browser.webRequest.onCompleted.addListener = sinon.stub(); 106 | this.webExt.background.browser.tabs.create.resetHistory(); 107 | // we create a tab with the same id and use the same request id to see if uncanceled 108 | await this.webExt.browser.tabs._create({ 109 | id: newTab.id, 110 | url: "https://www.youtube.com" 111 | }, { 112 | options: { 113 | webRequest: { 114 | requestId: newTabResponses.webRequest.request.requestId 115 | } 116 | } 117 | }); 118 | 119 | this.webExt.background.browser.tabs.create.should.have.been.calledOnce; 120 | }); 121 | 122 | it("should uncancel after webRequest.onErrorOccurred", async function () { 123 | const [newTabResponses, newTab] = await this.redirectedRequest(); 124 | this.webExt.background.browser.tabs.create.resetHistory(); 125 | // we create a tab with the same id and use the same request id to see if uncanceled 126 | await this.webExt.browser.tabs._create({ 127 | id: newTab.id, 128 | url: "https://www.youtube.com" 129 | }, { 130 | options: { 131 | webRequest: { 132 | requestId: newTabResponses.webRequest.request.requestId 133 | }, 134 | webRequestError: true 135 | } 136 | }); 137 | 138 | this.webExt.background.browser.tabs.create.should.have.been.calledOnce; 139 | }); 140 | 141 | it("should uncancel after 2 seconds", async function () { 142 | const [newTabResponses, newTab] = await this.redirectedRequest({ 143 | webRequestDontYield: ["onCompleted", "onErrorOccurred"] 144 | }); 145 | global.clock.tick(2000); 146 | 147 | this.webExt.background.browser.tabs.create.resetHistory(); 148 | // we create a tab with the same id and use the same request id to see if uncanceled 149 | await this.webExt.browser.tabs._create({ 150 | id: newTab.id, 151 | url: "https://www.youtube.com" 152 | }, { 153 | options: { 154 | webRequest: { 155 | requestId: newTabResponses.webRequest.request.requestId 156 | }, 157 | webRequestError: true 158 | } 159 | }); 160 | 161 | this.webExt.background.browser.tabs.create.should.have.been.calledOnce; 162 | }); 163 | 164 | it("should not influence the canceled url in other tabs", async function () { 165 | await this.redirectedRequest(); 166 | this.webExt.background.browser.tabs.create.resetHistory(); 167 | await this.webExt.browser.tabs._create({ 168 | cookieStoreId: "firefox-default", 169 | url: "https://www.youtube.com" 170 | }, { 171 | options: { 172 | webRequestError: true 173 | } 174 | }); 175 | 176 | this.webExt.background.browser.tabs.create.should.have.been.calledOnce; 177 | }); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /src/js/background/identityState.js: -------------------------------------------------------------------------------- 1 | window.identityState = { 2 | keyboardShortcut: {}, 3 | storageArea: { 4 | area: browser.storage.local, 5 | 6 | getContainerStoreKey(cookieStoreId) { 7 | const storagePrefix = "identitiesState@@_"; 8 | return `${storagePrefix}${cookieStoreId}`; 9 | }, 10 | 11 | async get(cookieStoreId) { 12 | const storeKey = this.getContainerStoreKey(cookieStoreId); 13 | const storageResponse = await this.area.get([storeKey]); 14 | if (storageResponse && storeKey in storageResponse) { 15 | if (!storageResponse[storeKey].macAddonUUID){ 16 | storageResponse[storeKey].macAddonUUID = uuidv4(); 17 | await this.set(cookieStoreId, storageResponse[storeKey]); 18 | } 19 | return storageResponse[storeKey]; 20 | } 21 | // If local storage doesn't have an entry, look it up to make sure it's 22 | // an in-use identity. 23 | const identities = await browser.contextualIdentities.query({}); 24 | const match = identities.find( 25 | (identity) => identity.cookieStoreId === cookieStoreId); 26 | if (match) { 27 | const defaultContainerState = identityState._createIdentityState(); 28 | await this.set(cookieStoreId, defaultContainerState); 29 | return defaultContainerState; 30 | } 31 | return false; 32 | }, 33 | 34 | set(cookieStoreId, data) { 35 | const storeKey = this.getContainerStoreKey(cookieStoreId); 36 | return this.area.set({ 37 | [storeKey]: data 38 | }); 39 | }, 40 | 41 | async remove(cookieStoreId) { 42 | const storeKey = this.getContainerStoreKey(cookieStoreId); 43 | return this.area.remove([storeKey]); 44 | }, 45 | 46 | async setKeyboardShortcut(shortcutId, cookieStoreId) { 47 | identityState.keyboardShortcut[shortcutId] = cookieStoreId; 48 | return this.area.set({[shortcutId]: cookieStoreId}); 49 | }, 50 | 51 | async loadKeyboardShortcuts () { 52 | const identities = await browser.contextualIdentities.query({}); 53 | for (let i=0; i < backgroundLogic.NUMBER_OF_KEYBOARD_SHORTCUTS; i++) { 54 | const key = "open_container_" + i; 55 | const storageObject = await this.area.get(key); 56 | if (storageObject[key]){ 57 | identityState.keyboardShortcut[key] = storageObject[key]; 58 | continue; 59 | } 60 | if (identities[i]) { 61 | identityState.keyboardShortcut[key] = identities[i].cookieStoreId; 62 | continue; 63 | } 64 | identityState.keyboardShortcut[key] = "none"; 65 | } 66 | return identityState.keyboardShortcut; 67 | }, 68 | 69 | /* 70 | * Looks for abandoned identity keys in local storage, and makes sure all 71 | * identities registered in the browser are also in local storage. (this 72 | * appears to not always be the case based on how this.get() is written) 73 | */ 74 | async upgradeData() { 75 | const identitiesList = await browser.contextualIdentities.query({}); 76 | 77 | for (const identity of identitiesList) { 78 | // ensure all identities have an entry in local storage 79 | await identityState.addUUID(identity.cookieStoreId); 80 | } 81 | 82 | const macConfigs = await this.area.get(); 83 | for(const configKey of Object.keys(macConfigs)) { 84 | if (configKey.includes("identitiesState@@_")) { 85 | const cookieStoreId = String(configKey).replace(/^identitiesState@@_/, ""); 86 | const match = identitiesList.find( 87 | localIdentity => localIdentity.cookieStoreId === cookieStoreId 88 | ); 89 | if (cookieStoreId === "firefox-default") continue; 90 | if (!match) { 91 | await this.remove(cookieStoreId); 92 | continue; 93 | } 94 | if (!macConfigs[configKey].macAddonUUID) { 95 | await identityState.storageArea.get(cookieStoreId); 96 | } 97 | } 98 | } 99 | }, 100 | 101 | }, 102 | 103 | _createTabObject(tab) { 104 | return Object.assign({}, tab); 105 | }, 106 | 107 | async getCookieStoreIDuuidMap() { 108 | const containers = {}; 109 | const identities = await browser.contextualIdentities.query({}); 110 | for(const identity of identities) { 111 | const containerInfo = await this.storageArea.get(identity.cookieStoreId); 112 | containers[identity.cookieStoreId] = containerInfo.macAddonUUID; 113 | } 114 | return containers; 115 | }, 116 | 117 | async storeHidden(cookieStoreId, windowId) { 118 | const containerState = await this.storageArea.get(cookieStoreId); 119 | const tabsByContainer = await browser.tabs.query({cookieStoreId, windowId}); 120 | tabsByContainer.forEach((tab) => { 121 | const tabObject = this._createTabObject(tab); 122 | if (!backgroundLogic.isPermissibleURL(tab.url)) { 123 | return; 124 | } 125 | // This tab is going to be closed. Let's mark this tabObject as 126 | // non-active. 127 | tabObject.active = false; 128 | tabObject.hiddenState = true; 129 | containerState.hiddenTabs.push(tabObject); 130 | }); 131 | 132 | return this.storageArea.set(cookieStoreId, containerState); 133 | }, 134 | 135 | async updateUUID(cookieStoreId, uuid) { 136 | if (!cookieStoreId || !uuid) { 137 | throw new Error ("cookieStoreId or uuid missing"); 138 | } 139 | const containerState = await this.storageArea.get(cookieStoreId); 140 | containerState.macAddonUUID = uuid; 141 | await this.storageArea.set(cookieStoreId, containerState); 142 | return uuid; 143 | }, 144 | 145 | async addUUID(cookieStoreId) { 146 | await this.storageArea.get(cookieStoreId); 147 | }, 148 | 149 | async lookupMACaddonUUID(cookieStoreId) { 150 | // This stays a lookup, because if the cookieStoreId doesn't 151 | // exist, this.get() will create it, which is not what we want. 152 | const cookieStoreIdKey = cookieStoreId.includes("firefox-container-") ? 153 | cookieStoreId : "firefox-container-" + cookieStoreId; 154 | const macConfigs = await this.storageArea.area.get(); 155 | for(const configKey of Object.keys(macConfigs)) { 156 | if (configKey === this.storageArea.getContainerStoreKey(cookieStoreIdKey)) { 157 | return macConfigs[configKey].macAddonUUID; 158 | } 159 | } 160 | return false; 161 | }, 162 | 163 | async lookupCookieStoreId(macAddonUUID) { 164 | const macConfigs = await this.storageArea.area.get(); 165 | for(const configKey of Object.keys(macConfigs)) { 166 | if (configKey.includes("identitiesState@@_")) { 167 | if(macConfigs[configKey].macAddonUUID === macAddonUUID) { 168 | return String(configKey).replace(/^identitiesState@@_/, ""); 169 | } 170 | } 171 | } 172 | return false; 173 | }, 174 | 175 | _createIdentityState() { 176 | return { 177 | hiddenTabs: [], 178 | macAddonUUID: uuidv4() 179 | }; 180 | }, 181 | 182 | init() { 183 | this.storageArea.loadKeyboardShortcuts(); 184 | } 185 | }; 186 | 187 | identityState.init(); 188 | 189 | function uuidv4() { 190 | // https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript 191 | return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => 192 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 193 | ); 194 | } 195 | -------------------------------------------------------------------------------- /src/js/utils.js: -------------------------------------------------------------------------------- 1 | /*global getBogusProxy */ 2 | 3 | const DEFAULT_FAVICON = "/img/blank-favicon.svg"; 4 | 5 | // eslint-disable-next-line 6 | const CONTAINER_ORDER_STORAGE_KEY = "container-order"; 7 | 8 | // TODO use export here instead of globals 9 | const Utils = { 10 | 11 | createFavIconElement(url) { 12 | const imageElement = document.createElement("img"); 13 | imageElement.classList.add("icon", "offpage", "menu-icon"); 14 | imageElement.src = url; 15 | const loadListener = (e) => { 16 | e.target.classList.remove("offpage"); 17 | e.target.removeEventListener("load", loadListener); 18 | e.target.removeEventListener("error", errorListener); 19 | }; 20 | const errorListener = (e) => { 21 | e.target.src = DEFAULT_FAVICON; 22 | }; 23 | imageElement.addEventListener("error", errorListener); 24 | imageElement.addEventListener("load", loadListener); 25 | return imageElement; 26 | }, 27 | 28 | // See comment in PR #313 - so far the (hacky) method being used to block proxies is to produce a sufficiently long random address 29 | getBogusProxy() { 30 | const bogusFailover = 1; 31 | const bogusType = "socks4"; 32 | const bogusPort = 9999; 33 | const bogusUsername = "foo"; 34 | if(typeof window.Utils.pregeneratedString !== "undefined") 35 | { 36 | return {type:bogusType, host:`w.${window.Utils.pregeneratedString}.coo`, port:bogusPort, username:bogusUsername, failoverTimeout:bogusFailover}; 37 | } 38 | else 39 | { 40 | // Initialize Utils.pregeneratedString 41 | window.Utils.pregeneratedString = ""; 42 | 43 | // We generate a cryptographically random string (of length specified in bogusLength), but we only do so once - thus negating any time delay caused 44 | const bogusLength = 8; 45 | const array = new Uint8Array(bogusLength); 46 | window.crypto.getRandomValues(array); 47 | for(let i = 0; i < bogusLength; i++) 48 | { 49 | const s = array[i].toString(16); 50 | if(s.length === 1) 51 | window.Utils.pregeneratedString += `0${s}`; 52 | else 53 | window.Utils.pregeneratedString += s; 54 | } 55 | 56 | // The only issue with this approach is that if (for some unknown reason) pregeneratedString is not saved, it will result in an infinite loop - but better than a privacy leak! 57 | return getBogusProxy(); 58 | } 59 | }, 60 | 61 | /** 62 | * Escapes any occurances of &, ", <, > or / with XML entities. 63 | * 64 | * @param {string} str 65 | * The string to escape. 66 | * @return {string} The escaped string. 67 | */ 68 | escapeXML(str) { 69 | const replacements = { "&": "&", "\"": """, "'": "'", "<": "<", ">": ">", "/": "/" }; 70 | return String(str).replace(/[&"'<>/]/g, m => replacements[m]); 71 | }, 72 | 73 | /** 74 | * A tagged template function which escapes any XML metacharacters in 75 | * interpolated values. 76 | * 77 | * @param {Array} strings 78 | * An array of literal strings extracted from the templates. 79 | * @param {Array} values 80 | * An array of interpolated values extracted from the template. 81 | * @returns {string} 82 | * The result of the escaped values interpolated with the literal 83 | * strings. 84 | */ 85 | escaped(strings, ...values) { 86 | const result = []; 87 | 88 | for (const [i, string] of strings.entries()) { 89 | result.push(string); 90 | if (i < values.length) 91 | result.push(this.escapeXML(values[i])); 92 | } 93 | 94 | return result.join(""); 95 | }, 96 | 97 | /** 98 | * @returns {Promise} 99 | */ 100 | async currentTab() { 101 | const activeTabs = await browser.tabs.query({ active: true, windowId: browser.windows.WINDOW_ID_CURRENT }); 102 | if (activeTabs.length > 0) { 103 | return activeTabs[0]; 104 | } 105 | return false; 106 | }, 107 | 108 | addEnterHandler(element, handler) { 109 | element.addEventListener("click", (e) => { 110 | handler(e); 111 | }); 112 | element.addEventListener("keydown", (e) => { 113 | if (e.keyCode === 13) { 114 | e.preventDefault(); 115 | handler(e); 116 | } 117 | }); 118 | }, 119 | 120 | addEnterOnlyHandler(element, handler) { 121 | element.addEventListener("keydown", (e) => { 122 | if (e.keyCode === 13) { 123 | e.preventDefault(); 124 | handler(e); 125 | } 126 | }); 127 | }, 128 | 129 | userContextId(cookieStoreId = "") { 130 | const userContextId = cookieStoreId.replace("firefox-container-", ""); 131 | return (userContextId !== cookieStoreId) ? Number(userContextId) : false; 132 | }, 133 | 134 | setOrRemoveAssignment(tabId, url, userContextId, value) { 135 | return browser.runtime.sendMessage({ 136 | method: "setOrRemoveAssignment", 137 | tabId, 138 | url, 139 | userContextId, 140 | value 141 | }); 142 | }, 143 | 144 | resetCookiesForSite(pageUrl, cookieStoreId) { 145 | return browser.runtime.sendMessage({ 146 | method: "resetCookiesForSite", 147 | pageUrl, 148 | cookieStoreId, 149 | }); 150 | }, 151 | 152 | /** 153 | * @param {string} url 154 | * @param {string} currentUserContextId 155 | * @param {string} newUserContextId 156 | * @param {number} tabIndex 157 | * @param {boolean} active 158 | * @param {number} [groupId] 159 | * @returns {Promise} 160 | */ 161 | async reloadInContainer(url, currentUserContextId, newUserContextId, tabIndex, active, groupId = undefined) { 162 | return await browser.runtime.sendMessage({ 163 | method: "reloadInContainer", 164 | url, 165 | currentUserContextId, 166 | newUserContextId, 167 | tabIndex, 168 | active, 169 | groupId 170 | }); 171 | }, 172 | 173 | async alwaysOpenInContainer(identity) { 174 | const currentTab = await this.currentTab(); 175 | const assignedUserContextId = this.userContextId(identity.cookieStoreId); 176 | if (currentTab.cookieStoreId !== identity.cookieStoreId) { 177 | return await browser.runtime.sendMessage({ 178 | method: "assignAndReloadInContainer", 179 | url: currentTab.url, 180 | currentUserContextId: false, 181 | newUserContextId: assignedUserContextId, 182 | tabIndex: currentTab.index +1, 183 | active: currentTab.active, 184 | groupId: currentTab.groupId 185 | }); 186 | } 187 | await Utils.setOrRemoveAssignment( 188 | currentTab.id, 189 | currentTab.url, 190 | assignedUserContextId, 191 | false 192 | ); 193 | }, 194 | /* Theme helper 195 | * 196 | * First, we look if there's a theme already set in the local storage. If 197 | * there isn't one, we set the theme based on `prefers-color-scheme`. 198 | * */ 199 | getTheme(currentTheme, window) { 200 | if (typeof currentTheme !== "undefined" && currentTheme !== "auto") { 201 | return currentTheme; 202 | } 203 | if (window.matchMedia("(prefers-color-scheme: dark)").matches) { 204 | return "dark"; 205 | } 206 | return "light"; 207 | }, 208 | async applyTheme() { 209 | const { currentTheme } = await browser.storage.local.get("currentTheme"); 210 | const popup = document.getElementsByTagName("html")[0]; 211 | const theme = Utils.getTheme(currentTheme, window); 212 | popup.setAttribute("data-theme", theme); 213 | } 214 | }; 215 | 216 | window.Utils = Utils; 217 | --------------------------------------------------------------------------------