├── .eslintignore ├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .prettierignore ├── .travis.yml ├── LICENSE ├── README.md ├── commitlint.config.js ├── fontello.json ├── manifest-update-url.ts ├── package-lock.json ├── package.json ├── semantic.json ├── src ├── .eslintrc.json ├── _locales │ ├── en │ │ └── messages.json │ ├── ru │ │ └── messages.json │ └── tr │ │ └── messages.json ├── background.ts ├── background │ ├── browseraction.ts │ ├── cleanup.ts │ ├── commands.ts │ ├── container.ts │ ├── contextmenu.ts │ ├── convert.ts │ ├── cookies.ts │ ├── event-listeners.ts │ ├── external-addons.ts │ ├── history.ts │ ├── isolation.ts │ ├── lib.ts │ ├── log.ts │ ├── mac.ts │ ├── management.ts │ ├── migration-legacy.ts │ ├── migration.ts │ ├── mouseclick.ts │ ├── pageaction.ts │ ├── preferences.ts │ ├── request.ts │ ├── runtime.ts │ ├── scripts.ts │ ├── statistics.ts │ ├── storage.ts │ ├── tabs.ts │ ├── tmp.ts │ └── utils.ts ├── contentscript.ts ├── global.d.ts ├── icons │ ├── LICENSE │ ├── deleteshistory │ │ ├── LICENSE │ │ └── deletes-history-icon-16.svg │ ├── page-black-simple-16.svg │ ├── page-black-simple-32.svg │ ├── page-blue-simple-16.svg │ ├── page-blue-simple-32.svg │ ├── page-d-16.svg │ ├── page-d-32.svg │ ├── page-red-simple-16.svg │ ├── page-red-simple-32.svg │ ├── page-w-16.svg │ ├── page-w-32.svg │ ├── page-white-simple-16.svg │ ├── page-white-simple-32.svg │ ├── pageaction-blue-19.svg │ ├── pageaction-blue-38.svg │ ├── pageaction-gray-19.svg │ ├── pageaction-gray-38.svg │ ├── pageaction-green-19.svg │ ├── pageaction-green-38.svg │ ├── pageaction-orange-19.svg │ ├── pageaction-orange-38.svg │ ├── pageaction-pink-19.svg │ ├── pageaction-pink-38.svg │ ├── pageaction-purple-19.svg │ ├── pageaction-purple-38.svg │ ├── pageaction-red-19.svg │ ├── pageaction-red-38.svg │ ├── pageaction-toolbar-19.svg │ ├── pageaction-toolbar-38.svg │ ├── pageaction-turquoise-19.svg │ ├── pageaction-turquoise-38.svg │ ├── pageaction-warning-red-19.svg │ ├── pageaction-warning-red-38.svg │ ├── pageaction-yellow-19.svg │ ├── pageaction-yellow-38.svg │ └── tmpcontainer-48.png ├── manifest.json ├── shared.ts ├── types.ts └── ui │ ├── .eslintrc.json │ ├── components │ ├── actions.vue │ ├── advanced │ │ ├── cookies.vue │ │ ├── deletehistory.vue │ │ ├── general.vue │ │ ├── index.vue │ │ └── scripts.vue │ ├── breadcrumb.vue │ ├── domainpattern.vue │ ├── export-import.vue │ ├── general.vue │ ├── glossary │ │ ├── index.vue │ │ └── link.vue │ ├── isolation │ │ ├── global.vue │ │ ├── index.vue │ │ ├── perdomain.vue │ │ └── settings.vue │ ├── message.vue │ ├── options.vue │ ├── popup.vue │ └── statistics.vue │ ├── mixin.ts │ ├── options.ts │ ├── popup.ts │ ├── root.ts │ ├── ui.html │ ├── vendor │ ├── fontello │ │ ├── LICENSE.txt │ │ └── fontello-embedded.css │ └── semantic │ │ ├── LICENSE.md │ │ ├── semantic.min.css │ │ └── themes │ │ └── default │ │ └── assets │ │ └── fonts │ │ └── icons.woff2 │ ├── vue-shims.d.ts │ └── vuedraggable.d.ts ├── test ├── .eslintrc.json ├── background.alwaysopenin.test.ts ├── background.browseractions.test.ts ├── background.cleanup.test.ts ├── background.cookies.test.ts ├── background.isolation.test.ts ├── background.mac.test.ts ├── background.mouseclicks.test.ts ├── background.redirects.test.ts ├── background.storage.test.ts ├── background.test.ts ├── chai-deep-match.d.ts ├── functional │ └── index.test.ts ├── global.d.ts ├── helper.ts ├── setup.ts └── webextensions-geckodriver.d.ts ├── tsconfig.json ├── tsconfig.lint.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | /src/ui/vendor 2 | *.html 3 | /dist -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "parser": "@typescript-eslint/parser", 4 | "ecmaVersion": 2018 5 | }, 6 | "env": { 7 | "browser": true, 8 | "webextensions": true, 9 | "node": true, 10 | "es6": true 11 | }, 12 | "extends": [ 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:vue/recommended", 15 | "plugin:prettier/recommended", 16 | "prettier/vue", 17 | "prettier/@typescript-eslint" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | - Temporary Containers Version: 15 | - Firefox Version: 16 | 17 | ### Actual behavior 18 | .. 19 | 20 | ### Expected behavior 21 | .. 22 | 23 | ### Steps to reproduce 24 | 25 | 1. .. 26 | 2. .. 27 | 3. .. 28 | 29 | ### Notes 30 | .. 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /web-ext-artifacts 3 | /.nyc_output 4 | /coverage 5 | /semantic 6 | /src/core/README.md 7 | /src/core/LICENSE 8 | /src/.web-extension-id 9 | /.web-ext-artifacts 10 | /dist 11 | /*.todo 12 | /.idea 13 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | vendor -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | services: 3 | - xvfb 4 | 5 | language: node_js 6 | node_js: 'lts/*' 7 | 8 | cache: 9 | npm: false 10 | 11 | addons: 12 | firefox: latest 13 | 14 | script: 15 | - | 16 | npm run lint 17 | npm run test 18 | if [[ $TRAVIS_TAG == *"beta"* ]]; then 19 | npm run beta 20 | else 21 | unset WEB_EXT_API_KEY 22 | unset WEB_EXT_API_SECRET 23 | npm run build 24 | fi 25 | npm run test:functional 26 | npm run webext:lint 27 | npm run check:dependencies 28 | 29 | after_success: npm run coverage 30 | 31 | deploy: 32 | provider: releases 33 | prerelease: true 34 | api_key: $GITHUB_TOKEN 35 | file_glob: true 36 | file: web-ext-artifacts/* 37 | skip_cleanup: true 38 | draft: true 39 | name: $TRAVIS_TAG 40 | on: 41 | tags: true 42 | condition: $TRAVIS_TAG =~ beta 43 | 44 | after_deploy: 45 | - | 46 | if [[ $TRAVIS_TAG == *"beta"* ]]; then 47 | git clone --depth 1 --branch beta-updates https://github.com/stoically/temporary-containers.git 48 | cd temporary-containers 49 | export BETA_VERSION=$(echo $TRAVIS_TAG | sed "s/^v//") 50 | sed -i -e "s/[0-9]\+\.[0-9]\+\(beta[0-9]\+\)\?/$BETA_VERSION/g" updates.json 51 | sed -i -e "s/-fx\\.xpi/-an\\+fx\\.xpi/" updates.json 52 | git commit -am "$TRAVIS_TAG" 53 | git push https://$GITHUB_TOKEN:x-oauth-basic@github.com/stoically/temporary-containers.git/ beta-updates 54 | fi 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT - Copyright since 2017 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Temporary Containers Firefox Add-on 2 | 3 | [![Build Status](https://travis-ci.com/stoically/temporary-containers.svg?branch=master)](https://travis-ci.com/stoically/temporary-containers) 4 | [![Temporary Containers Matrix room #tmp:mozilla.org](https://img.shields.io/badge/matrix-%23tmp%3Amozilla.org-blue)](https://matrix.to/#/#tmp:mozilla.org) 5 | 6 | Detailed information about the Add-on [can be found in the wiki](https://github.com/stoically/temporary-containers/wiki). There's also [this long-form article](https://medium.com/@stoically/enhance-your-privacy-in-firefox-with-temporary-containers-33925cd6cd21). 7 | 8 | ## Development 9 | 10 | ### Requirements 11 | 12 | - Clone the repository 13 | - `npm install` 14 | - `npm run dev` 15 | 16 | ### Run in Firefox 17 | 18 | - `npx web-ext run -s dist` 19 | - starts the default system Firefox with a temporary profile, loads the Add-on and watches for changes 20 | - append `-p profilename` to start Firefox with a specific profile 21 | 22 | or 23 | 24 | - Open `about:debugging` and `Load Temporary Add-on` which is located in the `dist` directory 25 | 26 | Check `about:debugging` and click `Inspect` to the right of Temporary Containers to see the console. 27 | 28 | ### Run the tests 29 | 30 | - Once: `npm test` 31 | - Shows a coverage summary and generates a detailed report in the `coverage` directory 32 | - Watcher: `npm run watch:test` 33 | 34 | ### Release 35 | 36 | #### AMO and GitHub 37 | 38 | - Bump manifest version 39 | - Commit, tag and push 40 | - Upload zip web-ext-artifact to AMO 41 | - Download published AMO xpi 42 | - Create and publish GitHub release with AMO xpi 43 | 44 | #### Pre-Release on GitHub 45 | 46 | - Bump manifest version 47 | - Commit and push 48 | - git tag v1.0beta1 49 | - git push origin v1.0beta1 50 | - git log \$(git tag --sort=-version:refname | sed -n 2p)..HEAD --pretty=format:%s 51 | - Add release notes and publish 52 | 53 | ## Libraries 54 | 55 | [Vue.js](https://vuejs.org) and [SemanticUI](https://semantic-ui.com/) are used for the preferences & popup UI. 56 | 57 | ## License 58 | 59 | MIT 60 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /fontello.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "css_prefix_text": "icon-", 4 | "css_use_suffix": false, 5 | "hinting": true, 6 | "units_per_em": 1000, 7 | "ascent": 850, 8 | "glyphs": [ 9 | { 10 | "uid": "e82cedfa1d5f15b00c5a81c9bd731ea2", 11 | "css": "info-circled", 12 | "code": 59392, 13 | "src": "fontawesome" 14 | }, 15 | { 16 | "uid": "f48ae54adfb27d8ada53d0fd9e34ee10", 17 | "css": "trash-empty", 18 | "code": 59393, 19 | "src": "fontawesome" 20 | }, 21 | { 22 | "uid": "f9c8ea86275ca16128235c6452b67b8e", 23 | "css": "user-secret", 24 | "code": 61979, 25 | "src": "fontawesome" 26 | }, 27 | { 28 | "uid": "559647a6f430b3aeadbecd67194451dd", 29 | "css": "menu", 30 | "code": 61641, 31 | "src": "fontawesome" 32 | }, 33 | { 34 | "uid": "d35a1d35efeb784d1dc9ac18b9b6c2b6", 35 | "css": "pencil", 36 | "code": 59394, 37 | "src": "fontawesome" 38 | }, 39 | { 40 | "uid": "98687378abd1faf8f6af97c254eb6cd6", 41 | "css": "cog-alt", 42 | "code": 59395, 43 | "src": "fontawesome" 44 | }, 45 | { 46 | "uid": "422e07e5afb80258a9c4ed1706498f8a", 47 | "css": "circle-empty", 48 | "code": 61708, 49 | "src": "fontawesome" 50 | }, 51 | { 52 | "uid": "266d5d9adf15a61800477a5acf9a4462", 53 | "css": "chart-bar", 54 | "code": 59396, 55 | "src": "fontawesome" 56 | }, 57 | { 58 | "uid": "6020aff067fc3c119cdd75daa5249220", 59 | "css": "exchange", 60 | "code": 61676, 61 | "src": "fontawesome" 62 | }, 63 | { 64 | "uid": "17ebadd1e3f274ff0205601eef7b9cc4", 65 | "css": "help-circled", 66 | "code": 59397, 67 | "src": "fontawesome" 68 | } 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /manifest-update-url.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | const manifestJson = require('./dist/manifest.json'); 4 | const updateUrl = 5 | 'https://raw.githubusercontent.com/stoically/temporary-containers/beta-updates/updates.json'; 6 | // eslint-disable-next-line @typescript-eslint/camelcase 7 | manifestJson.applications.gecko.update_url = updateUrl; 8 | 9 | // eslint-disable-next-line quotes 10 | fs.writeFileSync( 11 | './dist/manifest.json', 12 | JSON.stringify(manifestJson, null, 2) + '\n' 13 | ); 14 | -------------------------------------------------------------------------------- /semantic.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "semantic/", 3 | "paths": { 4 | "source": { 5 | "config": "src/theme.config", 6 | "definitions": "src/definitions/", 7 | "site": "src/site/", 8 | "themes": "src/themes/" 9 | }, 10 | "output": { 11 | "packaged": "dist/", 12 | "uncompressed": "dist/components/", 13 | "compressed": "dist/components/", 14 | "themes": "dist/themes/" 15 | }, 16 | "clean": "dist/" 17 | }, 18 | "components": [], 19 | "permission": false, 20 | "autoInstall": true, 21 | "rtl": false, 22 | "version": "2.8.5" 23 | } 24 | -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": false 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryContainers } from './background/tmp'; 2 | 3 | window.tmp = new TemporaryContainers(); 4 | window.tmp 5 | .initialize() 6 | .then((tmp) => { 7 | if (tmp.storage.installed) { 8 | tmp.debug('[bg] fresh install, showing options'); 9 | browser.tabs.create({ 10 | url: browser.runtime.getURL('options.html?installed'), 11 | }); 12 | } 13 | }) 14 | .catch((error) => { 15 | browser.browserAction.onClicked.addListener(() => { 16 | browser.tabs.create({ 17 | url: browser.runtime.getURL(` 18 | options.html?error=${encodeURIComponent(error.toString())} 19 | `), 20 | }); 21 | }); 22 | browser.browserAction.setPopup({ 23 | popup: null, 24 | }); 25 | browser.browserAction.setTitle({ title: 'Temporary Containers Error' }); 26 | browser.browserAction.setBadgeBackgroundColor({ 27 | color: 'red', 28 | }); 29 | browser.browserAction.setBadgeText({ 30 | text: 'E', 31 | }); 32 | browser.browserAction.enable(); 33 | 34 | window.tmp?.eventlisteners?.remove(); 35 | throw error; 36 | }); 37 | -------------------------------------------------------------------------------- /src/background/browseraction.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryContainers } from './tmp'; 2 | import { Container } from './container'; 3 | import { PreferencesSchema, ToolbarIconColor, Tab, TabId } from '~/types'; 4 | 5 | export class BrowserAction { 6 | private background: TemporaryContainers; 7 | private pref!: PreferencesSchema; 8 | private container!: Container; 9 | 10 | constructor(background: TemporaryContainers) { 11 | this.background = background; 12 | } 13 | 14 | initialize(): void { 15 | this.pref = this.background.pref; 16 | this.container = this.background.container; 17 | 18 | if (this.pref.browserActionPopup) { 19 | this.setPopup(); 20 | } 21 | if (this.pref.iconColor !== 'default') { 22 | this.setIcon(this.pref.iconColor); 23 | } 24 | if (!this.background.isolation.getActiveState()) { 25 | this.addIsolationInactiveBadge(); 26 | } 27 | } 28 | 29 | onClicked(): Promise { 30 | return this.container.createTabInTempContainer({ 31 | deletesHistory: this.pref.deletesHistory.automaticMode === 'automatic', 32 | }); 33 | } 34 | 35 | setPopup(): void { 36 | browser.browserAction.setPopup({ 37 | popup: 'popup.html', 38 | }); 39 | browser.browserAction.setTitle({ title: 'Temporary Containers' }); 40 | } 41 | 42 | unsetPopup(): void { 43 | browser.browserAction.setPopup({ 44 | popup: null, 45 | }); 46 | browser.browserAction.setTitle({ title: null }); 47 | } 48 | 49 | setIcon(iconColor: ToolbarIconColor): void { 50 | const iconPath = '../../icons'; 51 | let iconColorFileName: string = iconColor; 52 | if (iconColor === 'default') { 53 | iconColorFileName = 'd'; 54 | } 55 | const icon = { 56 | path: { 57 | 16: `${iconPath}/page-${iconColorFileName}-16.svg`, 58 | 32: `${iconPath}/page-${iconColorFileName}-32.svg`, 59 | }, 60 | }; 61 | browser.browserAction.setIcon(icon); 62 | } 63 | 64 | addBadge(tabId: TabId): void { 65 | if (!this.background.isolation.getActiveState()) { 66 | return; 67 | } 68 | 69 | browser.browserAction.setTitle({ 70 | title: 'Automatic Mode on navigation active', 71 | tabId, 72 | }); 73 | browser.browserAction.setBadgeBackgroundColor({ 74 | color: '#f9f9fa', 75 | tabId, 76 | }); 77 | browser.browserAction.setBadgeText({ 78 | text: 'A', 79 | tabId, 80 | }); 81 | } 82 | 83 | removeBadge(tabId: TabId): void { 84 | if (!this.background.isolation.getActiveState()) { 85 | return; 86 | } 87 | 88 | browser.browserAction.setTitle({ 89 | title: !this.pref.browserActionPopup 90 | ? 'Open a new tab in a new Temporary Container (Alt+C)' 91 | : 'Temporary Containers', 92 | tabId, 93 | }); 94 | browser.browserAction.setBadgeText({ 95 | text: null, 96 | tabId, 97 | }); 98 | } 99 | 100 | async addIsolationInactiveBadge(num?: number): Promise { 101 | browser.browserAction.setBadgeBackgroundColor({ 102 | color: 'red', 103 | }); 104 | browser.browserAction.setBadgeText({ 105 | text: num ? num.toString() : '!', 106 | }); 107 | 108 | const tabs = await browser.tabs.query({ 109 | currentWindow: true, 110 | active: true, 111 | }); 112 | if (tabs[0]) { 113 | browser.browserAction.setBadgeBackgroundColor({ 114 | color: 'red', 115 | tabId: tabs[0].id, 116 | }); 117 | browser.browserAction.setBadgeText({ 118 | text: null, 119 | tabId: tabs[0].id, 120 | }); 121 | } 122 | } 123 | 124 | removeIsolationInactiveBadge(): void { 125 | browser.browserAction.setBadgeText({ 126 | text: '', 127 | }); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/background/commands.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryContainers } from './tmp'; 2 | import { Container } from './container'; 3 | import { Storage } from './storage'; 4 | import { Tabs } from './tabs'; 5 | import { PreferencesSchema, Tab, Permissions, Debug } from '~/types'; 6 | 7 | export class Commands { 8 | private background: TemporaryContainers; 9 | private debug: Debug; 10 | private pref!: PreferencesSchema; 11 | private storage!: Storage; 12 | private container!: Container; 13 | private permissions!: Permissions; 14 | private tabs!: Tabs; 15 | 16 | constructor(background: TemporaryContainers) { 17 | this.background = background; 18 | this.debug = background.debug; 19 | } 20 | 21 | initialize(): void { 22 | this.pref = this.background.pref; 23 | this.storage = this.background.storage; 24 | this.container = this.background.container; 25 | this.permissions = this.background.permissions; 26 | this.tabs = this.background.tabs; 27 | } 28 | 29 | async onCommand(name: string): Promise { 30 | switch (name) { 31 | case 'new_temporary_container_tab': 32 | if (!this.pref.keyboardShortcuts.AltC) { 33 | return; 34 | } 35 | this.container.createTabInTempContainer({ 36 | deletesHistory: 37 | this.pref.deletesHistory.automaticMode === 'automatic', 38 | }); 39 | break; 40 | 41 | case 'new_no_container_tab': 42 | if (!this.pref.keyboardShortcuts.AltN) { 43 | return; 44 | } 45 | try { 46 | const tab = (await browser.tabs.create({ 47 | url: 'about:blank', 48 | })) as Tab; 49 | this.container.noContainerTabs[tab.id] = true; 50 | this.debug( 51 | '[onCommand] new no container tab created', 52 | this.container.noContainerTabs 53 | ); 54 | } catch (error) { 55 | this.debug('[onCommand] couldnt create tab', error); 56 | } 57 | break; 58 | 59 | case 'new_no_container_window_tab': 60 | if (!this.pref.keyboardShortcuts.AltShiftC) { 61 | return; 62 | } 63 | try { 64 | const browserWindow = await browser.windows.create({ 65 | url: 'about:blank', 66 | }); 67 | if (!browserWindow.tabs) { 68 | return; 69 | } 70 | const [tab] = browserWindow.tabs as Tab[]; 71 | this.container.noContainerTabs[tab.id] = true; 72 | this.debug( 73 | '[onCommand] new no container tab created in window', 74 | browserWindow, 75 | this.container.noContainerTabs 76 | ); 77 | } catch (error) { 78 | this.debug('[onCommand] couldnt create tab in window', error); 79 | } 80 | break; 81 | 82 | case 'new_no_history_tab': 83 | if (!this.pref.keyboardShortcuts.AltP) { 84 | return; 85 | } 86 | if (this.permissions.history) { 87 | this.container.createTabInTempContainer({ deletesHistory: true }); 88 | } 89 | break; 90 | 91 | case 'new_same_container_tab': 92 | if (!this.pref.keyboardShortcuts.AltX) { 93 | return; 94 | } 95 | this.tabs.createInSameContainer(); 96 | break; 97 | 98 | case 'new_temporary_container_tab_current_url': { 99 | if (!this.pref.keyboardShortcuts.AltO) { 100 | return; 101 | } 102 | const [activeTab] = (await browser.tabs.query({ 103 | currentWindow: true, 104 | active: true, 105 | })) as Tab[]; 106 | if (!activeTab || !activeTab.url.startsWith('http')) { 107 | return; 108 | } 109 | this.container.createTabInTempContainer({ 110 | url: activeTab.url, 111 | deletesHistory: 112 | this.pref.deletesHistory.automaticMode === 'automatic', 113 | }); 114 | break; 115 | } 116 | 117 | case 'toggle_isolation': 118 | if (!this.pref.keyboardShortcuts.AltI) { 119 | return; 120 | } 121 | this.background.isolation.toggleActiveState(); 122 | break; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/background/convert.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryContainers } from './tmp'; 2 | import { Container } from './container'; 3 | import { Storage } from './storage'; 4 | import { CookieStoreId, TabId } from '~/types'; 5 | 6 | export class Convert { 7 | private background: TemporaryContainers; 8 | private storage!: Storage; 9 | private container!: Container; 10 | 11 | constructor(background: TemporaryContainers) { 12 | this.background = background; 13 | } 14 | 15 | initialize(): void { 16 | this.storage = this.background.storage; 17 | this.container = this.background.container; 18 | } 19 | 20 | async convertTempContainerToPermanent({ 21 | cookieStoreId, 22 | tabId, 23 | name, 24 | }: { 25 | cookieStoreId: CookieStoreId; 26 | tabId: TabId; 27 | name: string; 28 | }): Promise { 29 | delete this.storage.local.tempContainers[cookieStoreId]; 30 | await this.storage.persist(); 31 | await browser.contextualIdentities.update(cookieStoreId, { 32 | name, 33 | color: 'blue', 34 | }); 35 | await browser.tabs.reload(tabId); 36 | } 37 | 38 | async convertTempContainerToRegular({ 39 | cookieStoreId, 40 | tabId, 41 | }: { 42 | cookieStoreId: CookieStoreId; 43 | tabId: TabId; 44 | }): Promise { 45 | this.storage.local.tempContainers[cookieStoreId].deletesHistory = false; 46 | delete this.storage.local.tempContainers[cookieStoreId].history; 47 | await this.storage.persist(); 48 | const name = this.storage.local.tempContainers[cookieStoreId].name.replace( 49 | '-deletes-history', 50 | '' 51 | ); 52 | await browser.contextualIdentities.update(cookieStoreId, { name }); 53 | await browser.tabs.reload(tabId); 54 | } 55 | 56 | async convertPermanentToTempContainer({ 57 | cookieStoreId, 58 | tabId, 59 | }: { 60 | cookieStoreId: CookieStoreId; 61 | tabId: TabId; 62 | }): Promise { 63 | const containerOptions = this.container.generateContainerNameIconColor(); 64 | await browser.contextualIdentities.update(cookieStoreId, { 65 | name: containerOptions.name, 66 | icon: containerOptions.icon, 67 | color: containerOptions.color, 68 | }); 69 | this.storage.local.tempContainers[cookieStoreId] = containerOptions; 70 | await this.storage.persist(); 71 | await browser.tabs.reload(tabId); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/background/history.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryContainers } from './tmp'; 2 | import { Storage } from './storage'; 3 | import { Tab, CookieStoreId, Debug } from '~/types'; 4 | 5 | export class History { 6 | private background: TemporaryContainers; 7 | private debug: Debug; 8 | private storage!: Storage; 9 | 10 | constructor(background: TemporaryContainers) { 11 | this.background = background; 12 | this.debug = background.debug; 13 | } 14 | 15 | initialize(): void { 16 | this.storage = this.background.storage; 17 | } 18 | 19 | async maybeAddHistory(tab: Tab | undefined, url: string): Promise { 20 | if (!tab || url === 'about:blank' || url === 'about:newtab') { 21 | return; 22 | } 23 | const cookieStoreId = tab.cookieStoreId; 24 | const container = this.storage.local.tempContainers[cookieStoreId]; 25 | if ( 26 | cookieStoreId !== `${this.background.containerPrefix}-default` && 27 | container && 28 | container.deletesHistory 29 | ) { 30 | if (!container.history) { 31 | container.history = {}; 32 | } 33 | container.history[url] = { 34 | tabId: tab.id, 35 | }; 36 | await this.storage.persist(); 37 | } 38 | } 39 | 40 | maybeClearHistory(cookieStoreId: CookieStoreId): number { 41 | let count = 0; 42 | const container = this.storage.local.tempContainers[cookieStoreId]; 43 | if (container && container.deletesHistory && container.history) { 44 | const urls = Object.keys(container.history); 45 | count = urls.length; 46 | urls.map((url) => { 47 | if (!url) { 48 | return; 49 | } 50 | this.debug('[maybeClearHistory] removing url from history', url); 51 | browser.history.deleteUrl({ url }); 52 | }); 53 | } 54 | return count; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/background/lib.ts: -------------------------------------------------------------------------------- 1 | import delay from 'delay'; 2 | import PQueue from 'p-queue'; 3 | import psl from 'psl'; 4 | 5 | export { delay, PQueue, psl }; 6 | -------------------------------------------------------------------------------- /src/background/log.ts: -------------------------------------------------------------------------------- 1 | import { Debug } from '~/types'; 2 | 3 | /* eslint-disable @typescript-eslint/no-explicit-any */ 4 | 5 | export class Log { 6 | public DEBUG = false; 7 | public stringify = true; 8 | private checkedLocalStorage = false; 9 | private checkLocalStoragePromise = this.checkLocalStorage(); 10 | 11 | constructor() { 12 | this.debug = this.debug.bind(this); 13 | browser.runtime.onInstalled.addListener( 14 | this.onInstalledListener.bind(this) 15 | ); 16 | } 17 | 18 | public debug: Debug = async (...args: any[]): Promise => { 19 | let date; 20 | if (!this.checkedLocalStorage && !window._mochaTest) { 21 | date = new Date().toUTCString(); 22 | await this.checkLocalStoragePromise; 23 | } 24 | 25 | if (!this.DEBUG) { 26 | return; 27 | } 28 | 29 | if (!date) { 30 | date = new Date().toUTCString(); 31 | } 32 | 33 | args = args.map((arg) => { 34 | if (typeof arg === 'object' && arg.favIconUrl) { 35 | arg = JSON.parse(JSON.stringify(arg)); 36 | delete arg.favIconUrl; 37 | return arg; 38 | } 39 | return arg; 40 | }); 41 | 42 | if (this.stringify && !window._mochaTest) { 43 | console.log(date, ...args.map((value) => JSON.stringify(value))); 44 | console.log('------------------------------------------'); 45 | } else { 46 | console.log(date, ...args.slice(0)); 47 | } 48 | }; 49 | 50 | checkLocalStorage(): void | Promise { 51 | if (this.DEBUG) { 52 | return; 53 | } 54 | 55 | // let's put this in the js event queue, just to make sure 56 | // that localstorage doesn't block registering event-listeners at all 57 | return new Promise((resolve) => 58 | setTimeout(() => { 59 | if (window.localStorage.getItem('debug-dev') === 'true') { 60 | this.DEBUG = true; 61 | this.stringify = false; 62 | this.checkedLocalStorage = true; 63 | this.debug('[log] enabled debug-dev because of localstorage item'); 64 | } else if (window.localStorage.getItem('debug') === 'true') { 65 | this.DEBUG = true; 66 | this.stringify = true; 67 | this.checkedLocalStorage = true; 68 | this.debug('[log] enabled debug because of localstorage item'); 69 | } 70 | resolve(); 71 | }) 72 | ); 73 | } 74 | 75 | onInstalledListener(details: any): void { 76 | browser.runtime.onInstalled.removeListener(this.onInstalledListener); 77 | 78 | if (!this.DEBUG && details.temporary) { 79 | this.DEBUG = true; 80 | this.stringify = false; 81 | 82 | if (details.reason === 'update') { 83 | browser.tabs.create({ 84 | url: browser.runtime.getURL('options.html'), 85 | }); 86 | } 87 | 88 | this.debug( 89 | '[log] enabled debug-dev because of temporary install', 90 | details 91 | ); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/background/management.ts: -------------------------------------------------------------------------------- 1 | import { addons } from './external-addons'; 2 | import { TemporaryContainers } from './tmp'; 3 | import { Debug } from '~/types'; 4 | 5 | export class Management { 6 | public addons = addons; 7 | private debug: Debug; 8 | 9 | constructor(background: TemporaryContainers) { 10 | this.debug = background.debug; 11 | } 12 | 13 | async initialize(): Promise { 14 | try { 15 | const extensions = await browser.management.getAll(); 16 | extensions.map((extension) => { 17 | const addon = this.addons.get(extension.id); 18 | if (addon) { 19 | addon.enabled = extension.enabled; 20 | addon.version = extension.version; 21 | } 22 | }); 23 | } catch (error) { 24 | this.debug('[management:initialize] couldnt getAll extensions', error); 25 | return; 26 | } 27 | } 28 | 29 | disable(extension: browser.management.ExtensionInfo): void { 30 | const addon = this.addons.get(extension.id); 31 | if (addon) { 32 | addon.enabled = false; 33 | addon.version = extension.version; 34 | } 35 | } 36 | 37 | enable(extension: browser.management.ExtensionInfo): void { 38 | const addon = this.addons.get(extension.id); 39 | if (addon && extension.enabled) { 40 | addon.enabled = true; 41 | addon.version = extension.version; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/background/migration-legacy.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | // this is only needed once for upgrades from <1.0 and should be removed in the next major version 3 | // we now store the addon version in storage instead of waiting for onInstalled 4 | import { TemporaryContainers } from './tmp'; 5 | 6 | export class MigrationLegacy { 7 | constructor(background: TemporaryContainers) { 8 | const debug = background.debug; 9 | 10 | const migrationReadyAbortController = new AbortController(); 11 | let migrationReady: () => void; 12 | let migrationReadyTimeout: number; 13 | const migrationReadyPromise = new Promise((resolve, reject) => { 14 | migrationReady = resolve; 15 | 16 | migrationReadyAbortController.signal.addEventListener('abort', () => { 17 | reject('[migration-legacy] waiting for migration ready timed out'); 18 | }); 19 | }).catch(debug); 20 | 21 | const migrationOnInstalledListener = async ( 22 | ...args: any[] 23 | ): Promise => { 24 | browser.runtime.onInstalled.removeListener(migrationOnInstalledListener); 25 | const { version } = await browser.storage.local.get('version'); 26 | if (version) { 27 | clearTimeout(migrationReadyTimeout); 28 | debug('[migration-legacy] version found, skip', version); 29 | return; 30 | } 31 | 32 | await migrationReadyPromise; 33 | return background.migration.onInstalled.call( 34 | background.migration, 35 | ...args 36 | ); 37 | }; 38 | browser.runtime.onInstalled.addListener(migrationOnInstalledListener); 39 | 40 | window.migrationLegacy = async (migration: any): Promise => { 41 | try { 42 | debug( 43 | '[migration-legacy] no previousVersion found, waiting for onInstalled' 44 | ); 45 | const updateDetails: any = await new Promise((resolve, reject) => { 46 | migration.onInstalled = resolve; 47 | window.setTimeout(() => { 48 | migrationReadyAbortController.abort(); 49 | reject(); 50 | }, 10000); 51 | debug('[migration-legacy] ready'); 52 | migrationReady(); 53 | }); 54 | migration.previousVersion = updateDetails.previousVersion; 55 | } catch (error) { 56 | debug( 57 | '[migration-legacy] waiting for onInstalled failed, assuming 0.103' 58 | ); 59 | migration.previousVersion = '0.103'; 60 | } 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/background/pageaction.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryContainers } from './tmp'; 2 | import { Storage } from './storage'; 3 | import { PreferencesSchema, Tab } from '~/types'; 4 | 5 | export class PageAction { 6 | private background: TemporaryContainers; 7 | private pref!: PreferencesSchema; 8 | private storage!: Storage; 9 | 10 | constructor(background: TemporaryContainers) { 11 | this.background = background; 12 | } 13 | 14 | initialize(): void { 15 | this.pref = this.background.pref; 16 | this.storage = this.background.storage; 17 | } 18 | 19 | async showOrHide(activatedTab?: Tab): Promise { 20 | if (!activatedTab) { 21 | const [activeTab] = (await browser.tabs.query({ 22 | currentWindow: true, 23 | active: true, 24 | })) as Tab[]; 25 | activatedTab = activeTab; 26 | } 27 | 28 | let color; 29 | if (!this.background.isolation.getActiveState()) { 30 | color = 'warning-red'; 31 | } else if ( 32 | activatedTab.cookieStoreId === 33 | `${this.background.containerPrefix}-default` 34 | ) { 35 | color = 'gray'; 36 | } else if ( 37 | this.storage.local.tempContainers[activatedTab.cookieStoreId] && 38 | this.storage.local.tempContainers[activatedTab.cookieStoreId].color 39 | ) { 40 | color = this.storage.local.tempContainers[activatedTab.cookieStoreId] 41 | .color; 42 | } else { 43 | const container = await browser.contextualIdentities.get( 44 | activatedTab.cookieStoreId 45 | ); 46 | color = container.color; 47 | } 48 | if (activatedTab?.id) { 49 | browser.pageAction.setIcon({ 50 | path: { 51 | '19': `icons/pageaction-${color}-19.svg`, 52 | '38': `icons/pageaction-${color}-38.svg`, 53 | }, 54 | tabId: activatedTab.id, 55 | }); 56 | if (!this.pref.pageAction) { 57 | browser.pageAction.hide(activatedTab.id); 58 | } else { 59 | browser.pageAction.show(activatedTab.id); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/background/scripts.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryContainers } from './tmp'; 2 | import { Debug, PreferencesSchema, Tab } from '~/types'; 3 | import { Utils } from './utils'; 4 | import { Container } from './container'; 5 | 6 | export class Scripts { 7 | private background: TemporaryContainers; 8 | private debug: Debug; 9 | private pref!: PreferencesSchema; 10 | private container: Container; 11 | private utils!: Utils; 12 | 13 | constructor(background: TemporaryContainers) { 14 | this.background = background; 15 | this.debug = background.debug; 16 | this.container = background.container; 17 | } 18 | 19 | initialize(): void { 20 | this.pref = this.background.pref; 21 | this.utils = this.background.utils; 22 | } 23 | 24 | async maybeExecute( 25 | request: browser.webNavigation._OnCommittedDetails 26 | ): Promise { 27 | if (!Object.keys(this.pref.scripts.domain).length) { 28 | return; 29 | } 30 | 31 | const tab = (await browser.tabs.get(request.tabId)) as Tab; 32 | if (!this.container.isTemporary(tab.cookieStoreId)) { 33 | return; 34 | } 35 | 36 | for (const domainPattern in this.pref.scripts.domain) { 37 | if (!this.utils.matchDomainPattern(request.url, domainPattern)) { 38 | continue; 39 | } 40 | for (const script of this.pref.scripts.domain[domainPattern]) { 41 | try { 42 | this.debug('[maybeExecute] executing script', request); 43 | await browser.tabs.executeScript(request.tabId, script); 44 | } catch (error) { 45 | this.debug( 46 | '[maybeExecute] executing script failed', 47 | error.toString() 48 | ); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/background/statistics.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryContainers } from './tmp'; 2 | import { Cleanup } from './cleanup'; 3 | import { Container } from './container'; 4 | import { Storage } from './storage'; 5 | import { formatBytes } from '../shared'; 6 | import { PreferencesSchema, Tab, CookieStoreId, Debug } from '~/types'; 7 | 8 | export class Statistics { 9 | private removedContainerCount = 0; 10 | private removedContainerCookiesCount = 0; 11 | private removedContainerHistoryCount = 0; 12 | private removedContentLength = 0; 13 | private requests: { 14 | [key: string]: { contentLength: number }; 15 | } = {}; 16 | 17 | private background: TemporaryContainers; 18 | private debug: Debug; 19 | private pref!: PreferencesSchema; 20 | private storage!: Storage; 21 | private container!: Container; 22 | private cleanup!: Cleanup; 23 | 24 | constructor(background: TemporaryContainers) { 25 | this.background = background; 26 | this.debug = background.debug; 27 | } 28 | 29 | initialize(): void { 30 | this.pref = this.background.pref; 31 | this.storage = this.background.storage; 32 | this.container = this.background.container; 33 | this.cleanup = this.background.cleanup; 34 | } 35 | 36 | async collect( 37 | request: browser.webRequest._OnCompletedDetails 38 | ): Promise { 39 | if (!this.pref.statistics && !this.pref.deletesHistory.statistics) { 40 | return; 41 | } 42 | 43 | if (request.tabId === -1) { 44 | return; 45 | } 46 | 47 | let tab; 48 | try { 49 | tab = (await browser.tabs.get(request.tabId)) as Tab; 50 | } catch (error) { 51 | return; 52 | } 53 | 54 | if (!this.container.isTemporary(tab.cookieStoreId)) { 55 | return; 56 | } 57 | 58 | if (!this.requests[tab.cookieStoreId]) { 59 | this.requests[tab.cookieStoreId] = { 60 | contentLength: 0, 61 | }; 62 | } 63 | if (!request.fromCache && request.responseHeaders) { 64 | const contentLength = request.responseHeaders.find( 65 | (header) => header.name === 'content-length' 66 | ); 67 | if (contentLength && contentLength.value) { 68 | this.requests[tab.cookieStoreId].contentLength += parseInt( 69 | contentLength.value, 70 | 10 71 | ); 72 | } 73 | } 74 | } 75 | 76 | async update( 77 | historyClearedCount: number, 78 | cookieStoreId: CookieStoreId 79 | ): Promise { 80 | this.removedContainerCount++; 81 | 82 | let cookieCount = 0; 83 | try { 84 | const cookies = await browser.cookies.getAll({ storeId: cookieStoreId }); 85 | cookieCount = cookies.length; 86 | } catch (error) { 87 | this.debug('[tryToRemove] couldnt get cookies', cookieStoreId, error); 88 | } 89 | 90 | if (historyClearedCount) { 91 | this.removedContainerHistoryCount += historyClearedCount; 92 | } 93 | if (this.pref.statistics) { 94 | this.storage.local.statistics.containersDeleted++; 95 | } 96 | 97 | if ( 98 | this.pref.deletesHistory.statistics && 99 | this.container.isTemporary(cookieStoreId, 'deletesHistory') 100 | ) { 101 | this.storage.local.statistics.deletesHistory.containersDeleted++; 102 | if (historyClearedCount) { 103 | this.storage.local.statistics.deletesHistory.urlsDeleted += historyClearedCount; 104 | } 105 | if (cookieCount) { 106 | this.storage.local.statistics.deletesHistory.cookiesDeleted += cookieCount; 107 | } 108 | } 109 | if (cookieCount) { 110 | if (this.pref.statistics) { 111 | this.storage.local.statistics.cookiesDeleted += cookieCount; 112 | } 113 | this.removedContainerCookiesCount += cookieCount; 114 | } 115 | 116 | if ( 117 | this.requests[cookieStoreId] && 118 | this.requests[cookieStoreId].contentLength 119 | ) { 120 | if (this.pref.statistics) { 121 | this.storage.local.statistics.cacheDeleted += this.requests[ 122 | cookieStoreId 123 | ].contentLength; 124 | } 125 | this.removedContentLength += this.requests[cookieStoreId].contentLength; 126 | } 127 | 128 | delete this.requests[cookieStoreId]; 129 | } 130 | 131 | finish(): void { 132 | if (this.removedContainerCount) { 133 | let notificationMessage = `Deleted Temporary Containers: ${this.removedContainerCount}`; 134 | if (this.removedContainerCookiesCount) { 135 | notificationMessage += `\nand ${this.removedContainerCookiesCount} Cookies`; 136 | } 137 | if (this.removedContentLength) { 138 | notificationMessage += `\nand ~${formatBytes( 139 | this.removedContentLength 140 | )} Cache`; 141 | } 142 | if (this.removedContainerHistoryCount) { 143 | notificationMessage += `\nand ${this.removedContainerHistoryCount} URLs from History`; 144 | } 145 | this.cleanup.maybeShowNotification(notificationMessage); 146 | } 147 | 148 | this.removedContainerCount = 0; 149 | this.removedContainerCookiesCount = 0; 150 | this.removedContainerHistoryCount = 0; 151 | this.removedContentLength = 0; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/background/storage.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryContainers } from './tmp'; 2 | import { StorageLocal, Debug } from '~/types'; 3 | 4 | export class Storage { 5 | public local!: StorageLocal; 6 | public installed: boolean; 7 | public defaults: StorageLocal; 8 | 9 | private background: TemporaryContainers; 10 | private debug: Debug; 11 | 12 | constructor(background: TemporaryContainers) { 13 | this.background = background; 14 | this.debug = background.debug; 15 | this.installed = false; 16 | 17 | this.defaults = { 18 | containerPrefix: false, 19 | tempContainerCounter: 0, 20 | tempContainers: {}, 21 | tempContainersNumbers: [], 22 | statistics: { 23 | startTime: new Date(), 24 | containersDeleted: 0, 25 | cookiesDeleted: 0, 26 | cacheDeleted: 0, 27 | deletesHistory: { 28 | containersDeleted: 0, 29 | cookiesDeleted: 0, 30 | urlsDeleted: 0, 31 | }, 32 | }, 33 | isolation: { 34 | active: true, 35 | reactivateTargetTime: 0, 36 | }, 37 | preferences: background.preferences.defaults, 38 | lastFileExport: false, 39 | version: false, 40 | }; 41 | } 42 | 43 | async initialize(): Promise { 44 | this.local = (await browser.storage.local.get()) as StorageLocal; 45 | 46 | // empty storage *should* mean new install 47 | if (!this.local || !Object.keys(this.local).length) { 48 | return this.install(); 49 | } 50 | 51 | // check for managed preferences 52 | try { 53 | const managed = await browser.storage.managed.get(); 54 | if (managed && Object.keys(managed).length) { 55 | this.local.version = managed.version; 56 | this.local.preferences = managed.preferences; 57 | await this.persist(); 58 | } 59 | } catch (error) { 60 | this.debug( 61 | '[initialize] accessing managed storage failed:', 62 | error.toString() 63 | ); 64 | } 65 | 66 | this.debug('[initialize] storage initialized', this.local); 67 | if ( 68 | this.background.utils.addMissingKeys({ 69 | defaults: this.defaults, 70 | source: this.local, 71 | }) 72 | ) { 73 | await this.persist(); 74 | } 75 | 76 | // migrate if currently running version is different from version in storage 77 | if (this.local.version && this.background.version !== this.local.version) { 78 | try { 79 | await this.background.migration.migrate({ 80 | preferences: this.local.preferences, 81 | previousVersion: this.local.version, 82 | }); 83 | } catch (error) { 84 | this.debug('[initialize] migration failed', error.toString()); 85 | } 86 | } 87 | 88 | return true; 89 | } 90 | 91 | async persist(): Promise { 92 | try { 93 | if (!this.local || !Object.keys(this.local).length) { 94 | this.debug('[persist] tried to persist corrupt storage', this.local); 95 | return false; 96 | } 97 | await browser.storage.local.set(this.local); 98 | this.debug('[persist] storage persisted'); 99 | return true; 100 | } catch (error) { 101 | this.debug( 102 | '[persist] something went wrong while trying to persist the storage', 103 | error 104 | ); 105 | return false; 106 | } 107 | } 108 | 109 | async install(): Promise { 110 | this.debug('[install] installing storage'); 111 | 112 | this.local = this.background.utils.clone(this.defaults); 113 | this.local.version = this.background.version; 114 | 115 | if (!(await this.persist())) { 116 | throw new Error('[install] something went wrong while installing'); 117 | } 118 | this.debug('[install] storage installed', this.local); 119 | this.installed = true; 120 | return true; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/background/tmp.ts: -------------------------------------------------------------------------------- 1 | import { Log } from './log'; 2 | import { EventListeners } from './event-listeners'; 3 | import { BrowserAction } from './browseraction'; 4 | import { Cleanup } from './cleanup'; 5 | import { Commands } from './commands'; 6 | import { Container } from './container'; 7 | import { ContextMenu } from './contextmenu'; 8 | import { Convert } from './convert'; 9 | import { Cookies } from './cookies'; 10 | import { History } from './history'; 11 | import { Isolation } from './isolation'; 12 | import { MultiAccountContainers } from './mac'; 13 | import { Management } from './management'; 14 | import { Migration } from './migration'; 15 | import { MigrationLegacy } from './migration-legacy'; 16 | import { MouseClick } from './mouseclick'; 17 | import { PageAction } from './pageaction'; 18 | import { Preferences } from './preferences'; 19 | import { Request } from './request'; 20 | import { Runtime } from './runtime'; 21 | import { Scripts } from './scripts'; 22 | import { Statistics } from './statistics'; 23 | import { Storage } from './storage'; 24 | import { Tabs } from './tabs'; 25 | import { Utils } from './utils'; 26 | import { PreferencesSchema, Permissions } from '~/types'; 27 | 28 | export class TemporaryContainers { 29 | public initialized = false; 30 | public log = new Log(); 31 | public debug = this.log.debug; 32 | public utils = new Utils(this); 33 | public preferences = new Preferences(this); 34 | public storage = new Storage(this); 35 | public runtime = new Runtime(this); 36 | public management = new Management(this); 37 | public request = new Request(this); 38 | public container = new Container(this); 39 | public mouseclick = new MouseClick(this); 40 | public tabs = new Tabs(this); 41 | public commands = new Commands(this); 42 | public isolation = new Isolation(this); 43 | public browseraction = new BrowserAction(this); 44 | public pageaction = new PageAction(this); 45 | public contextmenu = new ContextMenu(this); 46 | public cookies = new Cookies(this); 47 | public scripts = new Scripts(this); 48 | public history = new History(this); 49 | public cleanup = new Cleanup(this); 50 | public convert = new Convert(this); 51 | public statistics = new Statistics(this); 52 | public mac = new MultiAccountContainers(this); 53 | public migration = new Migration(this); 54 | public migrationLegacy = new MigrationLegacy(this); 55 | public eventlisteners = new EventListeners(this); 56 | 57 | public version!: string; 58 | public containerPrefix = 'firefox'; 59 | public permissions!: Permissions; 60 | public pref!: PreferencesSchema; 61 | 62 | async initialize(): Promise { 63 | if (this.initialized) { 64 | throw new Error('already initialized'); 65 | } 66 | 67 | this.debug('[tmp] initializing'); 68 | browser.browserAction.disable(); 69 | this.version = browser.runtime.getManifest().version; 70 | const { permissions } = await browser.permissions.getAll(); 71 | if (!permissions) { 72 | throw new Error('permissions.getAll() failed'); 73 | } 74 | this.permissions = { 75 | bookmarks: permissions.includes('bookmarks'), 76 | history: permissions.includes('history'), 77 | notifications: permissions.includes('notifications'), 78 | downloads: permissions.includes('downloads'), 79 | webNavigation: permissions.includes('webNavigation'), 80 | }; 81 | 82 | this.preferences.initialize(); 83 | await this.storage.initialize(); 84 | 85 | this.pref = (new Proxy(this.storage, { 86 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 87 | get(target, key): any { 88 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 89 | return (target.local.preferences as any)[key]; 90 | }, 91 | }) as unknown) as PreferencesSchema; 92 | 93 | if (!this.storage.local.containerPrefix) { 94 | const browserInfo = await browser.runtime.getBrowserInfo(); 95 | this.storage.local.containerPrefix = browserInfo.name.toLowerCase(); 96 | await this.storage.persist(); 97 | } 98 | this.containerPrefix = this.storage.local.containerPrefix; 99 | 100 | this.request.initialize(); 101 | this.runtime.initialize(); 102 | this.container.initialize(); 103 | this.mouseclick.initialize(); 104 | this.commands.initialize(); 105 | this.isolation.initialize(); 106 | this.browseraction.initialize(); 107 | this.pageaction.initialize(); 108 | this.contextmenu.initialize(); 109 | this.cookies.initialize(); 110 | this.scripts.initialize(); 111 | this.statistics.initialize(); 112 | this.mac.initialize(); 113 | this.history.initialize(); 114 | this.cleanup.initialize(); 115 | this.convert.initialize(); 116 | 117 | await this.tabs.initialize(); 118 | await this.management.initialize(); 119 | 120 | this.debug('[tmp] initialized'); 121 | this.initialized = true; 122 | this.eventlisteners.tmpInitialized(); 123 | browser.browserAction.enable(); 124 | 125 | await this.tabs.handleAlreadyOpen(); 126 | 127 | return this; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/contentscript.ts: -------------------------------------------------------------------------------- 1 | document.body.addEventListener( 2 | 'mouseup', 3 | async (event) => { 4 | // event valid? 5 | if (typeof event !== 'object' || typeof event.target !== 'object') { 6 | return; 7 | } 8 | 9 | // don't handle right mouse button 10 | if (event.button === 2) { 11 | return; 12 | } 13 | 14 | // sometimes websites change links on click 15 | // so we wait for the next tick and with that increase 16 | // the chance that we actually see the correct link 17 | await new Promise((resolve) => setTimeout(resolve)); 18 | 19 | // check for a element with href 20 | const target = event?.target as HTMLElement | null; 21 | const aElement = target?.closest('a'); 22 | if (!aElement || typeof aElement !== 'object' || !aElement.href) { 23 | return; 24 | } 25 | 26 | // tell background process to handle the clicked url 27 | browser.runtime.sendMessage({ 28 | method: 'linkClicked', 29 | payload: { 30 | href: aElement.href, 31 | event: { 32 | button: event.button, 33 | ctrlKey: event.ctrlKey, 34 | metaKey: event.metaKey, 35 | }, 36 | }, 37 | }); 38 | }, 39 | false 40 | ); 41 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryContainers } from './background/tmp'; 2 | import { Log } from './background/log'; 3 | 4 | declare global { 5 | interface Window { 6 | tmp?: TemporaryContainers; 7 | log: Log; 8 | _mochaTest?: boolean; 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | debug: any; 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | migrationLegacy: any; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/icons/deleteshistory/deletes-history-icon-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 21 | 23 | 24 | 26 | image/svg+xml 27 | 29 | 30 | 31 | 32 | 34 | 54 | 60 | 61 | -------------------------------------------------------------------------------- /src/icons/page-black-simple-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/icons/page-black-simple-32.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/icons/page-blue-simple-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/icons/page-blue-simple-32.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/icons/page-d-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 83 | 88 | 93 | 94 | -------------------------------------------------------------------------------- /src/icons/page-d-32.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 83 | 88 | 93 | 94 | -------------------------------------------------------------------------------- /src/icons/page-red-simple-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 32 | 52 | 54 | 56 | 60 | 64 | 68 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/icons/page-red-simple-32.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 32 | 52 | 54 | 56 | 60 | 64 | 68 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/icons/page-w-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 64 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/icons/page-w-32.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 64 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/icons/page-white-simple-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/icons/page-white-simple-32.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/icons/pageaction-blue-19.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-blue-38.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-gray-19.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-gray-38.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-green-19.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-green-38.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-orange-19.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-orange-38.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-pink-19.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-pink-38.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-purple-19.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-purple-38.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-red-19.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-red-38.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-toolbar-19.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-toolbar-38.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-turquoise-19.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-turquoise-38.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-warning-red-19.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 58 | 59 | -------------------------------------------------------------------------------- /src/icons/pageaction-warning-red-38.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/icons/pageaction-yellow-19.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/pageaction-yellow-38.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 49 | 51 | 53 | 57 | 61 | 65 | 70 | 71 | 72 | 73 | 78 | 79 | -------------------------------------------------------------------------------- /src/icons/tmpcontainer-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stoically/temporary-containers/cbb39f7abbdf3c767bd1fbdc109394a0db169b05/src/icons/tmpcontainer-48.png -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Open tabs, websites, and links in automatically managed disposable containers. Containers isolate the data websites store (cookies, cache, and more) from each other, further enhancing your privacy while you browse.", 3 | "manifest_version": 2, 4 | "name": "Temporary Containers", 5 | "version": "1.9.2", 6 | "homepage_url": "https://github.com/stoically/temporary-containers", 7 | "icons": { 8 | "48": "icons/tmpcontainer-48.png" 9 | }, 10 | "default_locale": "en", 11 | "browser_action": { 12 | "browser_style": false, 13 | "default_area": "tabstrip", 14 | "default_title": "Open a new tab in a new Temporary Container", 15 | "default_icon": { 16 | "16": "icons/page-d-16.svg", 17 | "32": "icons/page-d-32.svg" 18 | }, 19 | "theme_icons": [ 20 | { 21 | "light": "icons/page-d-16.svg", 22 | "dark": "icons/page-w-16.svg", 23 | "size": 16 24 | }, 25 | { 26 | "light": "icons/page-d-32.svg", 27 | "dark": "icons/page-w-32.svg", 28 | "size": 32 29 | } 30 | ] 31 | }, 32 | "page_action": { 33 | "browser_style": false, 34 | "default_popup": "popup.html", 35 | "default_icon": { 36 | "19": "icons/pageaction-gray-19.svg", 37 | "38": "icons/pageaction-gray-38.svg" 38 | } 39 | }, 40 | "commands": { 41 | "new_temporary_container_tab": { 42 | "suggested_key": { 43 | "default": "Alt+C" 44 | }, 45 | "description": "Open a new tab in a new Temporary Container" 46 | }, 47 | "new_same_container_tab": { 48 | "suggested_key": { 49 | "default": "Alt+X" 50 | }, 51 | "description": "Open a new tab in the same Container as the current tab. Enable in Advanced Preferences > Keyboard shortcuts" 52 | }, 53 | "new_no_container_tab": { 54 | "suggested_key": { 55 | "default": "Alt+N" 56 | }, 57 | "description": "Open a new 'No Container' tab. Enable in Advanced Preferences > Keyboard shortcuts" 58 | }, 59 | "new_no_container_window_tab": { 60 | "suggested_key": { 61 | "default": "Alt+Shift+C" 62 | }, 63 | "description": "Open a new 'No Container' tab in a new Window. Enable in Advanced Preferences > Keyboard shortcuts" 64 | }, 65 | "new_temporary_container_tab_current_url": { 66 | "suggested_key": { 67 | "default": "Alt+O" 68 | }, 69 | "description": "Open current tab URL in a new Temporary Container tab. Enable in Advanced Preferences > Keyboard shortcuts" 70 | }, 71 | "new_no_history_tab": { 72 | "suggested_key": { 73 | "default": "Alt+P" 74 | }, 75 | "description": "Open a new tab in a new 'Deletes History Temporary Container'. History permissions under Advanced Preferences > Delete History must be given" 76 | }, 77 | "toggle_isolation": { 78 | "suggested_key": { 79 | "default": "Alt+I" 80 | }, 81 | "description": "Toggle Isolation ON and OFF. Enable in Advanced Preferences > Keyboard shortcuts" 82 | } 83 | }, 84 | "options_ui": { 85 | "page": "options.html", 86 | "open_in_tab": true 87 | }, 88 | "permissions": [ 89 | "", 90 | "contextMenus", 91 | "contextualIdentities", 92 | "cookies", 93 | "management", 94 | "storage", 95 | "tabs", 96 | "webRequest", 97 | "webRequestBlocking" 98 | ], 99 | "optional_permissions": [ 100 | "bookmarks", 101 | "downloads", 102 | "history", 103 | "notifications", 104 | "webNavigation" 105 | ], 106 | "content_scripts": [ 107 | { 108 | "matches": [""], 109 | "js": ["contentscript.js"], 110 | "run_at": "document_end", 111 | "all_frames": true 112 | } 113 | ], 114 | "background": { 115 | "scripts": ["background.js"] 116 | }, 117 | "incognito": "not_allowed", 118 | "applications": { 119 | "gecko": { 120 | "id": "{c607c8df-14a7-4f28-894f-29e8722976af}", 121 | "strict_min_version": "68.0" 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/shared.ts: -------------------------------------------------------------------------------- 1 | import { Permissions } from './types'; 2 | 3 | export const getPermissions = async (): Promise => { 4 | const { permissions } = await browser.permissions.getAll(); 5 | if (!permissions) { 6 | throw new Error('permissions.getAll didnt return permissions'); 7 | } 8 | return { 9 | bookmarks: permissions.includes('bookmarks'), 10 | downloads: permissions.includes('downloads'), 11 | history: permissions.includes('history'), 12 | notifications: permissions.includes('notifications'), 13 | webNavigation: permissions.includes('webNavigation'), 14 | }; 15 | }; 16 | 17 | export const CONTAINER_COLORS = [ 18 | 'blue', // #37ADFF 19 | 'turquoise', // #00C79A 20 | 'green', // #51CD00 21 | 'yellow', // #FFCB00 22 | 'orange', // #FF9F00 23 | 'red', // #FF613D 24 | 'pink', // #FF4BDA 25 | 'purple', // #AF51F5 26 | 'toolbar', 27 | ]; 28 | 29 | export const CONTAINER_ICONS = [ 30 | 'fingerprint', 31 | 'briefcase', 32 | 'dollar', 33 | 'cart', 34 | 'circle', 35 | 'gift', 36 | 'vacation', 37 | 'food', 38 | 'fruit', 39 | 'pet', 40 | 'tree', 41 | 'chill', 42 | 'fence', 43 | ]; 44 | 45 | export const TOOLBAR_ICON_COLORS = [ 46 | 'default', 47 | 'black-simple', 48 | 'blue-simple', 49 | 'red-simple', 50 | 'white-simple', 51 | ]; 52 | 53 | export const IGNORED_DOMAINS_DEFAULT = ['getpocket.com', 'addons.mozilla.org']; 54 | 55 | export const REDIRECTOR_DOMAINS_DEFAULT = [ 56 | 't.co', 57 | 'outgoing.prod.mozaws.net', 58 | 'slack-redir.net', 59 | 'away.vk.com', 60 | ]; 61 | 62 | export const formatBytes = (bytes: number, decimals = 2): string => { 63 | // https://stackoverflow.com/a/18650828 64 | if (bytes == 0) { 65 | return '0 Bytes'; 66 | } 67 | const k = 1024; 68 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 69 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 70 | return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + sizes[i]; 71 | }; 72 | -------------------------------------------------------------------------------- /src/ui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module" 4 | }, 5 | "globals": { 6 | "$": "readonly" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/ui/components/actions.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 133 | -------------------------------------------------------------------------------- /src/ui/components/advanced/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 62 | -------------------------------------------------------------------------------- /src/ui/components/breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /src/ui/components/domainpattern.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 62 | -------------------------------------------------------------------------------- /src/ui/components/glossary/link.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /src/ui/components/isolation/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 43 | -------------------------------------------------------------------------------- /src/ui/components/isolation/settings.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 68 | -------------------------------------------------------------------------------- /src/ui/components/options.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 85 | 86 | 134 | -------------------------------------------------------------------------------- /src/ui/mixin.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export const mixin = Vue.extend({ 4 | methods: { 5 | t: browser.i18n.getMessage, 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | clone: (input: any): any => JSON.parse(JSON.stringify(input)), 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/ui/options.ts: -------------------------------------------------------------------------------- 1 | import App from './components/options.vue'; 2 | import root from './root'; 3 | root(App, {}); 4 | -------------------------------------------------------------------------------- /src/ui/popup.ts: -------------------------------------------------------------------------------- 1 | import App from './components/popup.vue'; 2 | import root from './root'; 3 | root(App, { popup: true }); 4 | -------------------------------------------------------------------------------- /src/ui/ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Temporary Containers 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ui/vendor/fontello/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font license info 2 | 3 | 4 | ## Font Awesome 5 | 6 | Copyright (C) 2016 by Dave Gandy 7 | 8 | Author: Dave Gandy 9 | License: SIL () 10 | Homepage: http://fortawesome.github.com/Font-Awesome/ 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/ui/vendor/semantic/LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/ui/vendor/semantic/themes/default/assets/fonts/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stoically/temporary-containers/cbb39f7abbdf3c767bd1fbdc109394a0db169b05/src/ui/vendor/semantic/themes/default/assets/fonts/icons.woff2 -------------------------------------------------------------------------------- /src/ui/vue-shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /src/ui/vuedraggable.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vuedraggable'; 2 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "mocha": true 5 | }, 6 | "rules": { 7 | "no-restricted-globals": ["error", "browser"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/background.browseractions.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, preferencesTestSet, nextTick, loadBackground } from './setup'; 2 | import sinon from 'sinon'; 3 | 4 | preferencesTestSet.map(preferences => { 5 | describe(`preferences: ${JSON.stringify(preferences)}`, () => { 6 | describe('when triggering browseraction', () => { 7 | it('should open a new tab in a new temporary container', async () => { 8 | const { tmp: background, browser } = await loadBackground({ 9 | preferences, 10 | initialize: false, 11 | }); 12 | browser.tabs.create.resolves({ 13 | id: 1, 14 | }); 15 | browser.contextualIdentities.create.resolves({ 16 | cookieStoreId: 'firefox-container-1', 17 | }); 18 | 19 | await background.initialize(); 20 | browser.browserAction.onClicked.addListener.yield(); 21 | await nextTick(); 22 | 23 | browser.contextualIdentities.create.should.have.been.calledWith({ 24 | name: 'tmp1', 25 | color: 'toolbar', 26 | icon: 'circle', 27 | }); 28 | browser.tabs.create.should.have.been.calledWith({ 29 | url: undefined, 30 | cookieStoreId: 'firefox-container-1', 31 | }); 32 | browser.storage.local.set.should.have.been.calledWith( 33 | background.storage.local 34 | ); 35 | }); 36 | 37 | it('should open a new tab in a new temporary container with custom settings', async () => { 38 | const { tmp: background, browser } = await loadBackground({ 39 | preferences, 40 | initialize: false, 41 | }); 42 | browser.tabs.create.resolves({ 43 | id: 1, 44 | }); 45 | browser.contextualIdentities.create.resolves({ 46 | cookieStoreId: 'firefox-container-1', 47 | }); 48 | 49 | await background.initialize(); 50 | background.storage.local.preferences.container.colorRandom = true; 51 | background.storage.local.preferences.container.iconRandom = true; 52 | background.storage.local.preferences.container.numberMode = 'reuse'; 53 | browser.browserAction.onClicked.addListener.yield(); 54 | await nextTick(); 55 | 56 | expect(browser.contextualIdentities.create).to.have.been.calledWith({ 57 | name: sinon.match.string, 58 | color: sinon.match.string, 59 | icon: sinon.match.string, 60 | }); 61 | expect(browser.tabs.create).to.have.been.calledWith( 62 | sinon.match({ 63 | url: undefined, 64 | cookieStoreId: 'firefox-container-1', 65 | }) 66 | ); 67 | expect(browser.storage.local.set).to.have.been.calledWith( 68 | background.storage.local 69 | ); 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/background.cookies.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, preferencesTestSet, loadBackground } from './setup'; 2 | import { Cookie } from '~/types'; 3 | 4 | preferencesTestSet.map(preferences => { 5 | describe(`preferences: ${JSON.stringify(preferences)}`, () => { 6 | describe('Set Cookies', () => { 7 | it('should set the cookie and add it to the header if allowed', async () => { 8 | const { tmp: background, browser } = await loadBackground({ 9 | preferences, 10 | }); 11 | 12 | const cookie: Cookie = { 13 | domain: 'domain', 14 | expirationDate: '123', 15 | firstPartyDomain: 'firstPartyDomain', 16 | httpOnly: 'true', 17 | name: 'name', 18 | path: '/foo/bar', 19 | sameSite: '', 20 | secure: 'true', 21 | url: 'https://example.com', 22 | value: 'value', 23 | }; 24 | browser.cookies.get.resolves(cookie); 25 | background.storage.local.preferences.cookies.domain = { 26 | 'example.com': [cookie], 27 | }; 28 | 29 | const tab = await background.container.createTabInTempContainer({}); 30 | const results = await browser.tabs._navigate( 31 | tab?.id, 32 | 'https://example.com', 33 | { 34 | requestHeaders: [{ name: 'Cookie', value: 'foo=bar; moo=foo' }], 35 | } 36 | ); 37 | 38 | browser.cookies.set.should.have.been.calledWith({ 39 | domain: 'domain', 40 | expirationDate: 123, 41 | firstPartyDomain: 'firstPartyDomain', 42 | httpOnly: true, 43 | name: 'name', 44 | path: '/foo/bar', 45 | sameSite: undefined, 46 | secure: true, 47 | url: 'https://example.com', 48 | value: 'value', 49 | storeId: tab?.cookieStoreId, 50 | }); 51 | 52 | (await results.onBeforeSendHeaders[0]).should.deep.match({ 53 | url: 'https://example.com', 54 | requestHeaders: [ 55 | { name: 'Cookie', value: 'foo=bar; moo=foo; name=value' }, 56 | ], 57 | }); 58 | }); 59 | 60 | it('should set the cookie and not add it to the header if not allowed', async () => { 61 | const { tmp: background, browser, helper } = await loadBackground({ 62 | preferences, 63 | }); 64 | await helper.openNewTmpTab({ 65 | createsContainer: 'firefox-tmp1', 66 | }); 67 | const cookie: Cookie = { 68 | domain: 'domain', 69 | expirationDate: '123', 70 | httpOnly: '', 71 | name: 'name', 72 | path: '/foo/bar', 73 | secure: 'true', 74 | url: 'https://example.com', 75 | value: 'value', 76 | firstPartyDomain: '', 77 | sameSite: '', 78 | }; 79 | browser.cookies.get.resolves(null); 80 | background.storage.local.preferences.cookies.domain = { 81 | 'example.com': [cookie], 82 | }; 83 | const [ 84 | promise, 85 | ] = (browser.webRequest.onBeforeSendHeaders.addListener.yield({ 86 | tabId: 1, 87 | url: 'https://example.com', 88 | requestHeaders: [{ name: 'Cookie', value: 'foo=bar; moo=foo' }], 89 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 90 | }) as unknown) as any[]; 91 | const result = await promise; 92 | browser.cookies.set.should.have.been.called; 93 | expect(result).to.be.undefined; 94 | }); 95 | 96 | it('should do nothing if its not a temporary container', async () => { 97 | const { tmp: background, browser } = await loadBackground({ 98 | preferences, 99 | initialize: false, 100 | }); 101 | browser.tabs.get.resolves({ 102 | cookieStoreId: 'firefox-default', 103 | }); 104 | await background.initialize(); 105 | 106 | const cookie: Cookie = { 107 | domain: 'domain', 108 | expirationDate: '123', 109 | httpOnly: '', 110 | name: 'name', 111 | path: '/foo/bar', 112 | secure: 'true', 113 | url: 'https://example.com', 114 | value: 'value', 115 | firstPartyDomain: '', 116 | sameSite: '', 117 | }; 118 | 119 | background.storage.local.preferences.cookies.domain = { 120 | 'example.com': [{ ...cookie, name: 'example', value: 'content' }], 121 | }; 122 | const [ 123 | response, 124 | ] = (browser.webRequest.onBeforeSendHeaders.addListener.yield({ 125 | url: 'https://example.com', 126 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 127 | }) as unknown) as any[]; 128 | expect(await response).to.be.undefined; 129 | }); 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /test/background.storage.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, loadBackground } from './setup'; 2 | describe('storage', () => { 3 | it('should initialize storage and version', async () => { 4 | const { tmp: background } = await loadBackground(); 5 | expect(background.storage.local.preferences).to.deep.equal( 6 | background.preferences.defaults 7 | ); 8 | expect(background.storage.local.version).to.equal('0.1'); 9 | }); 10 | 11 | it('should add missing preferences', async () => { 12 | const { tmp: background, browser } = await loadBackground({ 13 | initialize: false, 14 | }); 15 | browser.storage.local.get.resolves({ 16 | ...background.storage.defaults, 17 | version: '0.1', 18 | }); 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | (background.preferences.defaults as any).newPreference = true; 21 | await background.initialize(); 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | expect((background.storage.local.preferences as any).newPreference).to.be 24 | .true; 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/chai-deep-match.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'chai-deep-match'; 2 | -------------------------------------------------------------------------------- /test/functional/index.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import path from 'path'; 3 | import { expect } from 'chai'; 4 | import webExtensionsGeckoDriver from 'webextensions-geckodriver'; 5 | const webdriver = webExtensionsGeckoDriver.webdriver; 6 | const until = webdriver.until; 7 | const By = webdriver.By; 8 | const manifestPath = path.resolve( 9 | path.join(__dirname, './../../dist/manifest.json') 10 | ); 11 | 12 | describe('Temporary Containers', () => { 13 | let helper: any; 14 | let geckodriver: any; 15 | 16 | before(async () => { 17 | const webExtension = await webExtensionsGeckoDriver(manifestPath); 18 | geckodriver = webExtension.geckodriver; 19 | helper = { 20 | toolbarButton(): any { 21 | return geckodriver.wait( 22 | until.elementLocated( 23 | By.id('_c607c8df-14a7-4f28-894f-29e8722976af_-browser-action') 24 | ), 25 | 5000 26 | ); 27 | }, 28 | }; 29 | }); 30 | 31 | it('should have a toolbar button', async () => { 32 | const button = await helper.toolbarButton(); 33 | expect(await button.getAttribute('tooltiptext')).to.equal( 34 | 'Open a new tab in a new Temporary Container' 35 | ); 36 | }); 37 | 38 | it('should open a new Temporary Container if toolbar button is clicked', async () => { 39 | await geckodriver.getAllWindowHandles(); 40 | const button = await helper.toolbarButton(); 41 | 42 | // give the extension a chance to fully initialize 43 | await new Promise((r) => setTimeout(r, 1500)); 44 | 45 | button.click(); 46 | 47 | await geckodriver.wait( 48 | async () => { 49 | const handles = await geckodriver.getAllWindowHandles(); 50 | return handles.length === 2; 51 | }, 52 | 5000, 53 | 'Should have opened a new tab' 54 | ); 55 | 56 | const element = await geckodriver.wait( 57 | until.elementLocated(By.id('userContext-label')), 58 | 5000, 59 | 'Should find the userContext label' 60 | ); 61 | 62 | await geckodriver.wait( 63 | async () => { 64 | const containerName = await element.getAttribute('value'); 65 | return containerName === 'tmp1'; 66 | }, 67 | 5000, 68 | 'Should have a containerName' 69 | ); 70 | }); 71 | 72 | after(() => { 73 | geckodriver.quit(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/global.d.ts: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom'; 2 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 3 | import { BrowserFake } from 'webextensions-api-fake'; 4 | 5 | declare global { 6 | interface GlobalWindow extends jsdom.DOMWindow { 7 | _mochaTest?: boolean; 8 | } 9 | 10 | namespace NodeJS { 11 | interface Global { 12 | document: Document; 13 | window: GlobalWindow; 14 | browser: BrowserFake; 15 | AbortController: { 16 | new (): AbortController; 17 | prototype: AbortController; 18 | }; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | const preferencesTestSet = [ 2 | { 3 | automaticMode: { 4 | active: false, 5 | newTab: 'created', 6 | }, 7 | }, 8 | { 9 | automaticMode: { 10 | active: true, 11 | newTab: 'created', 12 | }, 13 | }, 14 | { 15 | automaticMode: { 16 | active: true, 17 | newTab: 'navigation', 18 | }, 19 | }, 20 | { 21 | automaticMode: { 22 | active: false, 23 | newTab: 'navigation', 24 | }, 25 | }, 26 | ]; 27 | 28 | if (!process.listenerCount('unhandledRejection')) { 29 | process.on('unhandledRejection', (r) => { 30 | console.log('unhandledRejection', r); 31 | }); 32 | } 33 | 34 | import chai from 'chai'; 35 | import chaiDeepMatch from 'chai-deep-match'; 36 | import sinon from 'sinon'; 37 | import sinonChai from 'sinon-chai'; 38 | import { BrowserFake, WebExtensionsApiFake } from 'webextensions-api-fake'; 39 | import jsdom from 'jsdom'; 40 | import { TemporaryContainers } from '~/background/tmp'; 41 | import { Helper } from './helper'; 42 | 43 | const virtualConsole = new jsdom.VirtualConsole(); 44 | virtualConsole.sendTo(console); 45 | virtualConsole.on('jsdomError', (error) => { 46 | // eslint-disable-next-line no-console 47 | console.error(error); 48 | }); 49 | 50 | const browser = new WebExtensionsApiFake().createBrowser() as BrowserFake; 51 | 52 | const fakeBrowser = (): { 53 | browser: BrowserFake; 54 | clock: sinon.SinonFakeTimers; 55 | } => { 56 | const clock = sinon.useFakeTimers({ 57 | toFake: [ 58 | 'setTimeout', 59 | 'clearTimeout', 60 | 'setInterval', 61 | 'clearInterval', 62 | 'Date', 63 | ], 64 | now: new Date(), 65 | }); 66 | const html = ''; 67 | 68 | const dom = new jsdom.JSDOM(html, { 69 | url: 'https://localhost', 70 | virtualConsole, 71 | }); 72 | const window = dom.window as jsdom.DOMWindow; 73 | 74 | global.document = window.document; 75 | // FIXME 76 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 77 | // @ts-ignore 78 | global.window = window; 79 | global.AbortController = window.AbortController; 80 | 81 | browser.sinonSandbox.reset(); 82 | new WebExtensionsApiFake().fakeApi(browser); 83 | // FIXME 84 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 85 | // @ts-ignore 86 | global.browser = browser; 87 | 88 | global.window._mochaTest = true; 89 | 90 | global.browser.runtime.getManifest.returns({ 91 | version: '0.1', 92 | }); 93 | global.browser.runtime.getBrowserInfo.resolves({ 94 | name: 'Firefox', 95 | version: 67, 96 | }); 97 | global.browser.permissions.getAll.resolves({ 98 | permissions: [], 99 | }); 100 | global.browser.management.getAll.resolves([ 101 | { 102 | id: '@testpilot-containers', 103 | enabled: true, 104 | version: '6.0.0', 105 | }, 106 | ]); 107 | 108 | return { browser, clock }; 109 | }; 110 | 111 | chai.should(); 112 | chai.use(chaiDeepMatch); 113 | chai.use(sinonChai); 114 | 115 | const { expect } = chai; 116 | const nextTick = (): Promise => { 117 | return new Promise((resolve) => { 118 | process.nextTick(resolve); 119 | }); 120 | }; 121 | 122 | export interface Background { 123 | browser: BrowserFake; 124 | tmp: TemporaryContainers; 125 | clock: sinon.SinonFakeTimers; 126 | helper: Helper; 127 | } 128 | 129 | const loadBackground = async ({ 130 | initialize = true, 131 | preferences = false, 132 | beforeCtor = false, 133 | }: { 134 | initialize?: boolean; 135 | preferences?: false | Record; 136 | beforeCtor?: 137 | | false 138 | | (( 139 | browser: BrowserFake, 140 | clock: sinon.SinonFakeTimers 141 | ) => Promise | void); 142 | } = {}): Promise => { 143 | const { browser, clock } = fakeBrowser(); 144 | 145 | if (beforeCtor) { 146 | await beforeCtor(browser, clock); 147 | } 148 | 149 | const background = new TemporaryContainers(); 150 | global.window.tmp = background; 151 | 152 | if (preferences) { 153 | Object.assign(background.preferences.defaults, preferences); 154 | } 155 | 156 | if (process.argv.includes('--tmp-debug')) { 157 | background.log.DEBUG = true; 158 | } 159 | 160 | if (initialize) { 161 | await background.initialize(); 162 | } 163 | 164 | return { 165 | browser, 166 | tmp: background, 167 | clock, 168 | helper: new Helper(browser, background), 169 | }; 170 | }; 171 | 172 | export { 173 | preferencesTestSet, 174 | sinon, 175 | expect, 176 | nextTick, 177 | loadBackground, 178 | BrowserFake, 179 | }; 180 | -------------------------------------------------------------------------------- /test/webextensions-geckodriver.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'webextensions-geckodriver'; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "outDir": "./dist", 5 | "module": "commonjs", 6 | "noEmit": false, 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "~/*": ["src/*"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "include": ["src/**/*.ts", "test/**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const path = require('path'); 3 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 6 | const CopyPlugin = require('copy-webpack-plugin'); 7 | 8 | module.exports = { 9 | entry: { 10 | background: './src/background.ts', 11 | contentscript: './src/contentscript.ts', 12 | options: './src/ui/options.ts', 13 | popup: './src/ui/popup.ts', 14 | }, 15 | devtool: false, 16 | mode: 'production', 17 | performance: { 18 | hints: false, 19 | }, 20 | node: false, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.ts$/, 25 | loader: 'ts-loader', 26 | exclude: /node_modules/, 27 | options: { 28 | appendTsSuffixTo: [/\.vue$/], 29 | }, 30 | }, 31 | { 32 | test: /\.vue$/, 33 | loader: 'vue-loader', 34 | }, 35 | { 36 | test: /\.css$/, 37 | use: ['vue-style-loader', 'css-loader'], 38 | }, 39 | ], 40 | }, 41 | optimization: { 42 | minimize: false, 43 | splitChunks: { 44 | chunks: (chunk) => { 45 | return ['options', 'popup'].includes(chunk.name); 46 | }, 47 | }, 48 | }, 49 | resolve: { 50 | extensions: ['.ts', '.js', '.vue'], 51 | plugins: [new TsconfigPathsPlugin()], 52 | }, 53 | output: { 54 | filename: '[name].js', 55 | path: path.resolve(__dirname, 'dist'), 56 | }, 57 | plugins: [ 58 | new HtmlWebpackPlugin({ 59 | template: 'src/ui/ui.html', 60 | filename: 'options.html', 61 | chunks: ['options'], 62 | }), 63 | new HtmlWebpackPlugin({ 64 | template: 'src/ui/ui.html', 65 | filename: 'popup.html', 66 | chunks: ['popup'], 67 | }), 68 | new VueLoaderPlugin(), 69 | new CopyPlugin({ 70 | patterns: [ 71 | 'README.md', 72 | 'LICENSE', 73 | 'src/manifest.json', 74 | { from: 'src/icons', to: 'icons' }, 75 | { from: 'src/_locales', to: '_locales' }, 76 | { from: 'src/ui/vendor', to: 'vendor' }, 77 | ], 78 | }), 79 | ], 80 | devServer: { 81 | hot: false, 82 | inline: false, 83 | writeToDisk: true, 84 | }, 85 | }; 86 | --------------------------------------------------------------------------------