diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..abff8f31 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["@babel/plugin-proposal-optional-chaining"] +} \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index fb402bee..446f8986 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,3 @@ /src/ui/vendor -/src/background/lib.js -*.html \ No newline at end of file +*.html +/dist \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 3a0fd2e0..d192994f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,18 +1,19 @@ { + "parserOptions": { + "parser": "@typescript-eslint/parser", + "ecmaVersion": 2018 + }, "env": { "browser": true, "webextensions": true, - "es6": true, - "node": true + "node": true, + "es6": true }, "extends": [ - "eslint:recommended", + "plugin:@typescript-eslint/recommended", "plugin:vue/recommended", "plugin:prettier/recommended", - "prettier/vue" - ], - "parserOptions": { - "parser": "babel-eslint", - "ecmaVersion": 2018 - } + "prettier/vue", + "prettier/@typescript-eslint" + ] } diff --git a/.travis.yml b/.travis.yml index a2179f6f..bfa2c46f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ addons: script: - if [[ $TRAVIS_TAG == *"beta"* ]]; then - npm run build:sign; + npm run build:beta; else unset WEB_EXT_API_KEY; unset WEB_EXT_API_SECRET; diff --git a/build-sign.js b/build-sign.ts similarity index 71% rename from build-sign.js rename to build-sign.ts index 99ea575a..1f82aeb1 100644 --- a/build-sign.js +++ b/build-sign.ts @@ -1,7 +1,9 @@ -const fs = require('fs'); +import fs from 'fs'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const manifestJson = require('./src/manifest.json'); const updateUrl = 'https://raw.githubusercontent.com/stoically/temporary-containers/beta-updates/updates.json'; +// eslint-disable-next-line @typescript-eslint/camelcase manifestJson.applications.gecko.update_url = updateUrl; // eslint-disable-next-line quotes diff --git a/package.json b/package.json index bde726ab..cdef4948 100644 --- a/package.json +++ b/package.json @@ -5,21 +5,26 @@ "scripts": { "coverage": "nyc report --reporter=text-lcov | coveralls", "clean": "rimraf web-ext-artifacts .web-ext-artifacts .cache dist", - "build": "npm run lint && npm run test && npm run test:functional && npm run build:parcel && npm run build:cp && npm run build:webext && npm run lint:addon && ncu", - "build:parcel": "npm run clean && parcel build src/background.js src/*.html --no-minify --no-source-maps", - "build:sign": "node build-sign.js && npm run lint && npm run test && npm run test:functional && npm run build:parcel && npm run build:cp && npm run build:webext:sign && git checkout -- src/manifest.json && ncu", + "build": "npm-run-all build:core build:webext lint:addon check:dependencies", + "build:core": "npm-run-all lint test test:functional build:parcel build:contentscript build:cp", + "build:parcel": "npm run clean && parcel build src/background.ts src/*.html --no-minify --no-source-maps", + "build:contentscript": "tsc -p tsconfig.build.json", "build:webext": "web-ext build -s dist", - "build:webext:sign": "web-ext sign --channel unlisted -s dist", - "build:cp": "cp README.md LICENSE dist", - "lint": "eslint {*,src/**/*,test/**/*}.{js,vue}", + "build:cp": "copyfiles README.md LICENSE dist", + "build:beta": "npm-run-all build:beta:sign build:core build:beta:webext build:beta:checkout", + "build:beta:sign": "ts-node build-sign.ts", + "build:beta:checkout": "git checkout -- src/manifest.json", + "build:beta:webext": "web-ext sign --channel unlisted -s dist", + "lint": "npm run lint:eslint && tsc", + "lint:eslint": "eslint *.js src/**/*.{ts,js,vue} test/**/*.ts", "lint:addon": "addons-linter web-ext-artifacts/temporary_containers-*.zip", "format": "prettier --write '{*,src/**/*,test/**/*}.{html,css,json,yml,md}'", - "test": "npm run build:parcel && nyc --reporter=html --reporter=text mocha --reporter=progress ./test/setup.js test/*.test.js --timeout 60000", - "test:watch": "mocha ./test/setup.js test/*.test.js -b --reporter=progress --watch --watch-files 'src/**/*.js' --delay --inspect", - "test:watch:verbose": "mocha ./test/setup.js test/*.test.js -b --reporter=spec --tmp-debug --watch --inspect", - "test:functional": "npm run build:parcel && mocha ./test/functional/*.test.js --timeout 10000", + "test": "nyc --reporter=html --reporter=text ts-mocha --paths -p tsconfig.json -b --reporter=progress test/*.test.ts", + "test:watch": "ts-mocha --paths -p tsconfig.json test/*.test.ts -b --reporter=progress --watch", + "test:watch:verbose": "ts-mocha --paths -p tsconfig.json test/*.test.ts -b --reporter=spec --tmp-debug --watch", + "test:functional": "npm run build:parcel && ts-mocha ./test/functional/*.test.ts --timeout 10000", "check:dependencies": "ncu", - "watch": "rimraf dist && parcel src/background.js src/*.html --hmr-hostname=localhost" + "watch": "rimraf dist && parcel src/background.ts src/*.html --hmr-hostname=localhost" }, "repository": { "type": "git", @@ -33,33 +38,56 @@ "homepage": "https://github.com/stoically/temporary-containers#readme", "dependencies": { "delay": "4.3.0", - "p-queue": "6.2.0", - "psl": "1.4.0" + "p-queue": "6.2.1", + "psl": "1.4.0", + "vue": "2.6.10" }, "peerDependencies": { "jquery": "3.4.1", "jquery-address": "1.6.0", "semantic-ui": "2.4.2", "sortablejs": "1.10.1", - "vue": "2.6.10", "vuedraggable": "2.23.2" }, "devDependencies": { + "@babel/core": "^7.7.4", + "@babel/plugin-proposal-optional-chaining": "^7.7.4", "@commitlint/cli": "^8.2.0", "@commitlint/config-conventional": "^8.2.0", - "@vue/component-compiler-utils": "^3.0.0", - "addons-linter": "^1.15.3", + "@istanbuljs/nyc-config-typescript": "^0.1.3", + "@types/chai": "^4.2.5", + "@types/firefox-webext-browser": "github:stoically/DefinitelyTyped#firefox-webext-browser-dist", + "@types/jquery": "^3.3.31", + "@types/jquery.address": "^1.5.29", + "@types/jsdom-global": "^3.0.1", + "@types/mocha": "^5.2.7", + "@types/psl": "^1.1.0", + "@types/semantic-ui": "^2.2.7", + "@types/semantic-ui-dropdown": "github:stoically/DefinitelyTyped#semantic-ui-dropdown-dist", + "@types/semantic-ui-form": "github:stoically/DefinitelyTyped#semantic-ui-form-dist", + "@types/sinon": "^7.5.1", + "@types/sinon-chai": "^3.2.3", + "@types/sinon-chrome": "^2.2.6", + "@typescript-eslint/eslint-plugin": "^2.9.0", + "@typescript-eslint/parser": "^2.9.0", + "@vue/component-compiler-utils": "^3.0.2", + "addons-linter": "^1.17.0", "babel-eslint": "^10.0.3", "chai": "^4.2.0", - "coveralls": "^3.0.7", - "eslint": "^6.6.0", - "eslint-config-prettier": "^6.5.0", + "chai-deep-match": "^1.2.1", + "copyfiles": "^2.1.1", + "coveralls": "^3.0.8", + "eslint": "^6.7.1", + "eslint-config-prettier": "^6.7.0", "eslint-plugin-prettier": "^3.1.1", - "eslint-plugin-vue": "^6.0.0", - "husky": "^3.0.9", + "eslint-plugin-vue": "^6.0.1", + "husky": "^3.1.0", + "jsdom": "^15.2.1", + "jsdom-global": "^3.0.2", "lint-staged": "^10.0.0-1", "mocha": "^6.2.2", - "npm-check-updates": "^3.1.26", + "npm-check-updates": "^3.2.2", + "npm-run-all": "^4.1.5", "nyc": "^14.1.1", "parcel-bundler": "^1.12.4", "parcel-plugin-html-externals": "^0.1.2", @@ -68,14 +96,19 @@ "rimraf": "^3.0.0", "sinon": "^7.5.0", "sinon-chai": "^3.3.0", + "ts-mocha": "^6.0.0", + "ts-node": "^8.5.2", + "tsconfig-paths": "^3.9.0", + "typescript": "^3.7.2", "vue-hot-reload-api": "^2.3.4", "vue-template-compiler": "^2.6.10", - "web-ext": "^3.2.0", - "webextensions-geckodriver": "^0.6.1", - "webextensions-jsdom": "^0.18.1" + "vue-typed-mixins": "^0.2.0", + "web-ext": "^3.2.1", + "webextensions-api-fake": "^1.0.1", + "webextensions-geckodriver": "^0.6.1" }, "browserslist": [ - "Firefox >= 60" + "Firefox >= 67" ], "staticFiles": { "staticPath": [ @@ -87,7 +120,7 @@ ], "watcherGlob": "**", "excludeGlob": [ - "background.js", + "background.ts", "background", "background/**", "*.html", @@ -113,19 +146,20 @@ } }, "lint-staged": { - "*.+(js|vue)": [ + "*.{ts,js,vue}": [ "eslint --fix", "git add" ], - "*.+(html|css|json|yml|md)": [ + "*.{html,css,json,yml,md}": [ "prettier --write", "git add" ] }, "private": true, "nyc": { + "extends": "@istanbuljs/nyc-config-typescript", "exclude": [ - "test/**/*.js" + "test/**/*.ts" ] }, "prettier": { diff --git a/src/.eslintrc.json b/src/.eslintrc.json index 45032c7e..fe04e91d 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -1,14 +1,5 @@ { "env": { - "browser": true, "node": false - }, - "globals": { - "tmp": "readonly", - "debug": "readonly", - "psl": "readonly", - "log": "readonly", - "delay": "readonly", - "PQueue": "readonly" } } diff --git a/src/background.js b/src/background.js deleted file mode 100644 index adfa2c70..00000000 --- a/src/background.js +++ /dev/null @@ -1,153 +0,0 @@ -import './background/log'; -import eventListeners from './background/event-listeners'; - -import './background/lib'; -import BrowserAction from './background/browseraction'; -import Cleanup from './background/cleanup'; -import Commands from './background/commands'; -import Container from './background/container'; -import ContextMenu from './background/contextmenu'; -import Cookies from './background/cookies'; -import Convert from './background/convert'; -import History from './background/history'; -import Isolation from './background/isolation'; -import MultiAccountContainers from './background/mac'; -import Management from './background/management'; -import Migration from './background/migration'; -import MouseClick from './background/mouseclick'; -import PageAction from './background/pageaction'; -import Preferences from './background/preferences.js'; -import Request from './background/request'; -import Runtime from './background/runtime'; -import Statistics from './background/statistics'; -import Storage from './background/storage'; -import Tabs from './background/tabs'; -import Utils from './background/utils'; - -class TemporaryContainers { - constructor() { - this.initialized = false; - debug('[tmp] initializing'); - - this.utils = new Utils(this); - this.preferences = new Preferences(this); - this.storage = new Storage(this); - - this.runtime = new Runtime(this); - this.management = new Management(this); - this.request = new Request(this); - this.container = new Container(this); - this.mouseclick = new MouseClick(this); - this.tabs = new Tabs(this); - this.commands = new Commands(this); - this.browseraction = new BrowserAction(this); - this.pageaction = new PageAction(this); - this.contextmenu = new ContextMenu(this); - this.cookies = new Cookies(this); - this.isolation = new Isolation(this); - this.history = new History(this); - this.cleanup = new Cleanup(this); - this.convert = new Convert(this); - this.statistics = new Statistics(this); - this.mac = new MultiAccountContainers(this); - this.migration = new Migration(this); - - this.containerPrefix = 'firefox'; - } - - async initialize() { - this.version = browser.runtime.getManifest().version; - this.browserVersion = parseInt( - (await browser.runtime.getBrowserInfo()).version - ); - const { permissions } = await browser.permissions.getAll(); - this.permissions = { - bookmarks: permissions.includes('bookmarks'), - history: permissions.includes('history'), - notifications: permissions.includes('notifications'), - }; - - this.preferences.initialize(); - await this.storage.initialize(); - - this.pref = new Proxy(this.storage, { - get(target, key) { - return target.local.preferences[key]; - }, - }); - - if (!this.storage.local.containerPrefix) { - const browserInfo = await browser.runtime.getBrowserInfo(); - this.storage.local.containerPrefix = browserInfo.name.toLowerCase(); - await this.storage.persist(); - } - this.containerPrefix = this.storage.local.containerPrefix; - - this.request.initialize(); - this.runtime.initialize(); - this.container.initialize(); - this.mouseclick.initialize(); - this.commands.initialize(); - this.browseraction.initialize(); - this.pageaction.initialize(); - this.contextmenu.initialize(); - this.cookies.initialize(); - this.statistics.initialize(); - this.mac.initialize(); - this.isolation.initialize(); - this.history.initialize(); - this.cleanup.initialize(); - this.convert.initialize(); - - await this.management.initialize(); - await this.tabs.initialize(); - - debug('[tmp] initialized'); - this.initialized = true; - eventListeners.tmpInitialized(true); - browser.browserAction.enable(); - } -} - -browser.browserAction.disable(); - -window.TemporaryContainers = TemporaryContainers; -window.tmp = new TemporaryContainers(); - -(async () => { - if (browser._mochaTest) { - return; - } - - try { - await tmp.initialize(); - - if (tmp.storage.installed) { - debug('[bg] fresh install, showing options'); - browser.tabs.create({ - url: browser.runtime.getURL('options.html?installed'), - }); - } - } catch (error) { - browser.browserAction.onClicked.addListener(() => { - browser.tabs.create({ - url: browser.runtime.getURL(` - options.html?error=${encodeURIComponent(error.toString())} - `), - }); - }); - browser.browserAction.setPopup({ - popup: null, - }); - browser.browserAction.setTitle({ title: 'Temporary Containers Error' }); - browser.browserAction.setBadgeBackgroundColor({ - color: 'red', - }); - browser.browserAction.setBadgeText({ - text: 'E', - }); - browser.browserAction.enable(); - - eventListeners.remove(); - } -})(); diff --git a/src/background.ts b/src/background.ts new file mode 100644 index 00000000..35e55149 --- /dev/null +++ b/src/background.ts @@ -0,0 +1,36 @@ +import { TemporaryContainers } from './background/tmp'; + +window.tmp = new TemporaryContainers(); +window.tmp + .initialize() + .then(tmp => { + if (tmp.storage.installed) { + tmp.debug('[bg] fresh install, showing options'); + browser.tabs.create({ + url: browser.runtime.getURL('options.html?installed'), + }); + } + }) + .catch(error => { + browser.browserAction.onClicked.addListener(() => { + browser.tabs.create({ + url: browser.runtime.getURL(` + options.html?error=${encodeURIComponent(error.toString())} + `), + }); + }); + browser.browserAction.setPopup({ + popup: null, + }); + browser.browserAction.setTitle({ title: 'Temporary Containers Error' }); + browser.browserAction.setBadgeBackgroundColor({ + color: 'red', + }); + browser.browserAction.setBadgeText({ + text: 'E', + }); + browser.browserAction.enable(); + + window.tmp?.eventlisteners?.remove(); + throw error; + }); diff --git a/src/background/browseraction.js b/src/background/browseraction.ts similarity index 68% rename from src/background/browseraction.js rename to src/background/browseraction.ts index fb520889..0971d30c 100644 --- a/src/background/browseraction.js +++ b/src/background/browseraction.ts @@ -1,11 +1,18 @@ -class BrowserAction { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { Container } from './container'; +import { PreferencesSchema, ToolbarIconColor, Tab, TabId } from '~/types'; + +export class BrowserAction { + private background: TemporaryContainers; + private pref!: PreferencesSchema; + private container!: Container; + + constructor(background: TemporaryContainers) { this.background = background; } - initialize() { + initialize(): void { this.pref = this.background.pref; - this.storage = this.background.storage; this.container = this.background.container; if (this.pref.browserActionPopup) { @@ -19,60 +26,61 @@ class BrowserAction { } } - onClicked() { + onClicked(): Promise { return this.container.createTabInTempContainer({ deletesHistory: this.pref.deletesHistory.automaticMode === 'automatic', }); } - setPopup() { + setPopup(): void { browser.browserAction.setPopup({ popup: 'popup.html', }); browser.browserAction.setTitle({ title: 'Temporary Containers' }); } - unsetPopup() { + unsetPopup(): void { browser.browserAction.setPopup({ popup: null, }); browser.browserAction.setTitle({ title: null }); } - setIcon(iconColor) { - const iconPath = '../icons'; + setIcon(iconColor: ToolbarIconColor): void { + const iconPath = '../../icons'; + let iconColorFileName: string = iconColor; if (iconColor === 'default') { - iconColor = 'd'; + iconColorFileName = 'd'; } const icon = { path: { - 16: `${iconPath}/page-${iconColor}-16.svg`, - 32: `${iconPath}/page-${iconColor}-32.svg`, + 16: `${iconPath}/page-${iconColorFileName}-16.svg`, + 32: `${iconPath}/page-${iconColorFileName}-32.svg`, }, }; browser.browserAction.setIcon(icon); } - addBadge(tabId) { + addBadge(tabId: TabId): void { if (!this.pref.isolation.active) { return; } browser.browserAction.setTitle({ title: 'Automatic Mode on navigation active', - tabId: tabId, + tabId, }); browser.browserAction.setBadgeBackgroundColor({ color: '#f9f9fa', - tabId: tabId, + tabId, }); browser.browserAction.setBadgeText({ text: 'A', - tabId: tabId, + tabId, }); } - removeBadge(tabId) { + removeBadge(tabId: TabId): void { if (!this.pref.isolation.active) { return; } @@ -85,11 +93,11 @@ class BrowserAction { }); browser.browserAction.setBadgeText({ text: null, - tabId: tabId, + tabId, }); } - async addIsolationInactiveBadge() { + async addIsolationInactiveBadge(): Promise { browser.browserAction.setBadgeBackgroundColor({ color: 'red', }); @@ -111,11 +119,9 @@ class BrowserAction { }); } - removeIsolationInactiveBadge() { + removeIsolationInactiveBadge(): void { browser.browserAction.setBadgeText({ text: '', }); } } - -export default BrowserAction; diff --git a/src/background/cleanup.js b/src/background/cleanup.ts similarity index 58% rename from src/background/cleanup.js rename to src/background/cleanup.ts index b1fdb00d..a9f65817 100644 --- a/src/background/cleanup.js +++ b/src/background/cleanup.ts @@ -1,37 +1,59 @@ -class ContainerCleanup { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { Container } from './container'; +import { History } from './history'; +import { delay, PQueue } from './lib'; +import { Statistics } from './statistics'; +import { Storage } from './storage'; +import { PreferencesSchema, CookieStoreId, Permissions, Debug } from '~/types'; + +export class Cleanup { + private background: TemporaryContainers; + private debug: Debug; + private pref!: PreferencesSchema; + private storage!: Storage; + private container!: Container; + private history!: History; + private statistics!: Statistics; + private permissions!: Permissions; + + private queued = new Set(); + private queue = new PQueue({ concurrency: 1 }); + + constructor(background: TemporaryContainers) { this.background = background; - - this.queued = new Set(); - this.queue = new PQueue({ concurrency: 1 }); + this.debug = background.debug; setInterval(() => { - debug('[interval] container cleanup interval'); + this.debug('[interval] container cleanup interval'); this.cleanup(); }, 600000); } - initialize() { + initialize(): void { this.pref = this.background.pref; this.storage = this.background.storage; this.container = this.background.container; - this.tabs = this.background.tabs; this.history = this.background.history; - this.utils = this.background.utils; this.statistics = this.background.statistics; this.permissions = this.background.permissions; } - async addToRemoveQueue(cookieStoreId, skipDelay = false) { + async addToRemoveQueue( + cookieStoreId: CookieStoreId, + skipDelay = false + ): Promise { if (this.queued.has(cookieStoreId)) { - debug('[addToRemoveQueue] container already in queue', cookieStoreId); + this.debug( + '[addToRemoveQueue] container already in queue', + cookieStoreId + ); return; } this.queued.add(cookieStoreId); const containerRemovalDelay = this.container.getRemovalDelay(cookieStoreId); if (containerRemovalDelay && !skipDelay) { - debug( + this.debug( '[addToRemoveQueue] waiting to add container removal to queue', containerRemovalDelay, cookieStoreId @@ -39,102 +61,80 @@ class ContainerCleanup { await delay(containerRemovalDelay); } - debug('[addToRemoveQueue] queuing container removal', cookieStoreId); + this.debug('[addToRemoveQueue] queuing container removal', cookieStoreId); this.queue .add(async () => { const containerRemoved = await this.tryToRemove(cookieStoreId); - this.queued.delete(cookieStoreId); - if (containerRemoved) { - debug( + this.debug( '[addToRemoveQueue] container removed, waiting a bit', cookieStoreId ); await delay(3000); } }) - .then(() => { + .finally(() => { + this.queued.delete(cookieStoreId); + if (this.queue.pending) { return; } - debug('[addToRemoveQueue] queue empty'); - this.statistics.finish(); - // fallback cleanup of container numbers + this.debug('[addToRemoveQueue] queue empty'); + this.statistics.finish(); this.container.cleanupNumbers(); }); } - async tryToRemove(cookieStoreId) { - try { - await browser.contextualIdentities.get(cookieStoreId); - } catch (error) { - debug( - '[tryToRemove] container not found, removing entry from storage', - cookieStoreId, - error - ); - await this.container.removeFromStorage(cookieStoreId); - return false; - } - + async tryToRemove(cookieStoreId: CookieStoreId): Promise { try { const tempTabs = await browser.tabs.query({ cookieStoreId, }); if (tempTabs.length > 0) { - debug( + this.debug( '[tryToRemove] not removing container because it still has tabs', cookieStoreId, tempTabs.length ); return false; } - debug( + this.debug( '[tryToRemove] no tabs in temp container anymore, deleting container', cookieStoreId ); } catch (error) { - debug('[tryToRemove] failed to query tabs', cookieStoreId, error); + this.debug('[tryToRemove] failed to query tabs', cookieStoreId, error); return false; } - let cookies = []; - try { - cookies = await browser.cookies.getAll({ storeId: cookieStoreId }); - } catch (error) { - debug('[tryToRemove] couldnt get cookies', cookieStoreId, error); - } const historyClearedCount = this.history.maybeClearHistory(cookieStoreId); - this.statistics.update(historyClearedCount, cookies.length, cookieStoreId); + this.statistics.update(historyClearedCount, cookieStoreId); this.container.cleanupNumber(cookieStoreId); - const containerRemoved = await this.removeContainer(cookieStoreId); - if (containerRemoved) { - delete this.storage.local.tempContainers[cookieStoreId]; - } - + await this.removeContainer(cookieStoreId); await this.storage.persist(); return true; } - async removeContainer(cookieStoreId) { + async removeContainer(cookieStoreId: CookieStoreId): Promise { try { const contextualIdentity = await browser.contextualIdentities.remove( cookieStoreId ); if (!contextualIdentity) { - debug( + this.debug( '[removeContainer] couldnt find container to remove, probably already removed', cookieStoreId ); } else { - debug('[removeContainer] container removed', cookieStoreId); + this.debug('[removeContainer] container removed', cookieStoreId); } + await this.container.removeFromStorage(cookieStoreId); return true; } catch (error) { - debug( + this.debug( '[removeContainer] error while removing container', cookieStoreId, error @@ -143,27 +143,25 @@ class ContainerCleanup { } } - async cleanup(skipDelay = false) { + async cleanup(skipDelay = false): Promise { const containers = this.container.getAllIds(); if (!containers.length) { - debug('[cleanup] canceling, no containers at all'); + this.debug('[cleanup] canceling, no containers at all'); return; } if (await this.onlySessionRestoreOrNoTabs()) { - debug('[cleanup] canceling, only sessionrestore or no tabs'); + this.debug('[cleanup] canceling, only sessionrestore or no tabs'); return; } - if (containers.length) { - containers.map( - cookieStoreId => - !this.queued.has(cookieStoreId) && - this.addToRemoveQueue(cookieStoreId, skipDelay) - ); - } + containers.map( + cookieStoreId => + !this.queued.has(cookieStoreId) && + this.addToRemoveQueue(cookieStoreId, skipDelay) + ); } - async onlySessionRestoreOrNoTabs() { + async onlySessionRestoreOrNoTabs(): Promise { // don't do a cleanup if there are no tabs or a sessionrestore tab try { const tabs = await browser.tabs.query({}); @@ -174,17 +172,17 @@ class ContainerCleanup { return true; } } catch (error) { - debug('[onlySessionRestoreOrNoTabs] failed to query tabs', error); + this.debug('[onlySessionRestoreOrNoTabs] failed to query tabs', error); } return false; } - maybeShowNotification(message) { + maybeShowNotification(message: string): void { if (!this.pref.notifications || !this.permissions.notifications) { return; } - debug('[maybeShowNotification] showing notification'); + this.debug('[maybeShowNotification] showing notification'); browser.notifications.create({ type: 'basic', title: 'Temporary Containers', @@ -193,5 +191,3 @@ class ContainerCleanup { }); } } - -export default ContainerCleanup; diff --git a/src/background/commands.js b/src/background/commands.ts similarity index 68% rename from src/background/commands.js rename to src/background/commands.ts index b951b14e..0193e5b7 100644 --- a/src/background/commands.js +++ b/src/background/commands.ts @@ -1,9 +1,26 @@ -class Commands { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { Container } from './container'; +import { Storage } from './storage'; +import { Tabs } from './tabs'; +import { PreferencesSchema, Tab, Permissions, Debug } from '~/types'; +import { PageAction } from './pageaction'; + +export class Commands { + private background: TemporaryContainers; + private debug: Debug; + private pref!: PreferencesSchema; + private storage!: Storage; + private container!: Container; + private permissions!: Permissions; + private tabs!: Tabs; + private pageaction!: PageAction; + + constructor(background: TemporaryContainers) { this.background = background; + this.debug = background.debug; } - initialize() { + initialize(): void { this.pref = this.background.pref; this.storage = this.background.storage; this.container = this.background.container; @@ -12,7 +29,7 @@ class Commands { this.pageaction = this.background.pageaction; } - async onCommand(name) { + async onCommand(name: string): Promise { switch (name) { case 'new_temporary_container_tab': if (!this.pref.keyboardShortcuts.AltC) { @@ -29,16 +46,16 @@ class Commands { return; } try { - const tab = await browser.tabs.create({ + const tab = (await browser.tabs.create({ url: 'about:blank', - }); + })) as Tab; this.container.noContainerTabs[tab.id] = true; - debug( + this.debug( '[onCommand] new no container tab created', this.container.noContainerTabs ); } catch (error) { - debug('[onCommand] couldnt create tab', error); + this.debug('[onCommand] couldnt create tab', error); } break; @@ -47,17 +64,21 @@ class Commands { return; } try { - const window = await browser.windows.create({ + const browserWindow = await browser.windows.create({ url: 'about:blank', }); - this.container.noContainerTabs[window.tabs[0].id] = true; - debug( + if (!browserWindow.tabs) { + return; + } + const [tab] = browserWindow.tabs as Tab[]; + this.container.noContainerTabs[tab.id] = true; + this.debug( '[onCommand] new no container tab created in window', - window, + browserWindow, this.container.noContainerTabs ); } catch (error) { - debug('[onCommand] couldnt create tab in window', error); + this.debug('[onCommand] couldnt create tab in window', error); } break; @@ -81,10 +102,10 @@ class Commands { if (!this.pref.keyboardShortcuts.AltO) { return; } - const [activeTab] = await browser.tabs.query({ + const [activeTab] = (await browser.tabs.query({ currentWindow: true, active: true, - }); + })) as Tab[]; if (!activeTab || !activeTab.url.startsWith('http')) { return; } @@ -113,5 +134,3 @@ class Commands { } } } - -export default Commands; diff --git a/src/background/container.js b/src/background/container.ts similarity index 73% rename from src/background/container.js rename to src/background/container.ts index bfaa51df..26b1e41d 100644 --- a/src/background/container.js +++ b/src/background/container.ts @@ -1,51 +1,59 @@ -class Container { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { delay, psl } from './lib'; +import { Storage } from './storage'; +import { Tabs } from './tabs'; +import { CONTAINER_COLORS, CONTAINER_ICONS } from '~/shared'; +import { + ContainerColor, + ContainerIcon, + PreferencesSchema, + CookieStoreId, + TabId, + ContainerOptions, + Tab, + Permissions, + Debug, + TmpTabOptions, + CreateTabOptions, +} from '~/types'; + +export class Container { + public noContainerTabs: { + [key: number]: boolean; + } = {}; + public urlCreatedContainer: { + [key: string]: CookieStoreId; + } = {}; + public tabCreatedAsMacConfirmPage: { + [key: number]: boolean; + } = {}; + public lastCreatedInactiveTab: { + [key: number]: TabId; + } = {}; + + private containerColors: ContainerColor[] = CONTAINER_COLORS; + private containerIcons: ContainerIcon[] = CONTAINER_ICONS; + private requestCreatedTab: { + [key: string]: boolean; + } = {}; + + private background: TemporaryContainers; + private debug: Debug; + private pref!: PreferencesSchema; + private storage!: Storage; + private permissions!: Permissions; + private tabs!: Tabs; + + constructor(background: TemporaryContainers) { this.background = background; - this.containerColors = [ - 'blue', // #37ADFF - 'turquoise', // #00C79A - 'green', // #51CD00 - 'yellow', // #FFCB00 - 'orange', // #FF9F00 - 'red', // #FF613D - 'pink', // #FF4BDA - 'purple', // #AF51F5 - ]; - - this.containerIcons = [ - 'fingerprint', - 'briefcase', - 'dollar', - 'cart', - 'circle', - 'gift', - 'vacation', - 'food', - 'fruit', - 'pet', - 'tree', - 'chill', - ]; - - this.urlCreatedContainer = {}; - this.requestCreatedTab = {}; - this.tabCreatedAsMacConfirmPage = {}; - this.noContainerTabs = {}; - this.lastCreatedInactiveTab = {}; + this.debug = background.debug; } - async initialize() { + async initialize(): Promise { this.pref = this.background.pref; this.storage = this.background.storage; - this.request = this.background.request; - this.mouseclick = this.background.mouseclick; this.permissions = this.background.permissions; this.tabs = this.background.tabs; - - if (this.background.browserVersion >= 67) { - this.containerColors.push('toolbar'); - this.containerIcons.push('fence'); - } } async createTabInTempContainer({ @@ -56,11 +64,11 @@ class Container { dontPin = true, deletesHistory = false, macConfirmPage = false, - }) { + }: TmpTabOptions): Promise { if (request && request.requestId) { // we saw that request already if (this.requestCreatedTab[request.requestId]) { - debug( + this.debug( '[createTabInTempContainer] we already created a tab for this request, so we stop here, probably redirect', tab, request @@ -71,7 +79,7 @@ class Container { // cleanup tracked requests later // requestIds are unique per session, so we have no pressure to remove them delay(300000).then(() => { - debug('[createTabInTempContainer] cleanup timeout', request); + this.debug('[createTabInTempContainer] cleanup timeout', request); delete this.requestCreatedTab[request.requestId]; }); } @@ -92,7 +100,15 @@ class Container { }); } - async createTempContainer({ url, request, deletesHistory }) { + async createTempContainer({ + url, + request, + deletesHistory, + }: { + url?: string; + request?: false | browser.webRequest.WebRequestOnBeforeRequestDetails; + deletesHistory?: boolean; + }): Promise { const containerOptions = this.generateContainerNameIconColor( (request && request.url) || url ); @@ -111,7 +127,7 @@ class Container { containerOptions.deletesHistory = deletesHistory; try { - debug( + this.debug( '[createTabInTempContainer] creating new container', containerOptions ); @@ -120,7 +136,7 @@ class Container { icon: containerOptions.icon, color: containerOptions.color, }); - debug( + this.debug( '[createTabInTempContainer] contextualIdentity created', contextualIdentity ); @@ -132,11 +148,12 @@ class Container { return contextualIdentity; } catch (error) { - debug( + this.debug( '[createTabInTempContainer] error while creating container', containerOptions.name, error ); + throw error; } } @@ -147,11 +164,18 @@ class Container { dontPin, macConfirmPage, contextualIdentity, - }) { + }: { + url?: string; + tab?: Tab; + active?: boolean; + dontPin?: boolean; + macConfirmPage?: boolean; + contextualIdentity: browser.contextualIdentities.ContextualIdentity; + }): Promise { try { - const newTabOptions = { - url, + const newTabOptions: CreateTabOptions = { cookieStoreId: contextualIdentity.cookieStoreId, + url, }; if (tab) { newTabOptions.active = tab.active; @@ -160,7 +184,7 @@ class Container { !tab.active && this.lastCreatedInactiveTab[browser.windows.WINDOW_ID_CURRENT] ) { - debug( + this.debug( '[createTabInTempContainer] lastCreatedInactiveTab id', this.lastCreatedInactiveTab ); @@ -168,13 +192,13 @@ class Container { const lastCreatedInactiveTab = await browser.tabs.get( this.lastCreatedInactiveTab[browser.windows.WINDOW_ID_CURRENT] ); - debug( + this.debug( '[createTabInTempContainer] lastCreatedInactiveTab', lastCreatedInactiveTab ); newTabOptions.index = lastCreatedInactiveTab.index + 1; } catch (error) { - debug( + this.debug( '[createTabInTempContainer] failed to get lastCreatedInactiveTab', error ); @@ -195,23 +219,23 @@ class Container { newTabOptions.active = false; } - debug( + this.debug( '[createTabInTempContainer] creating tab in temporary container', newTabOptions ); - const newTab = await browser.tabs.create(newTabOptions); + const newTab = (await browser.tabs.create(newTabOptions)) as Tab; if (tab && !tab.active) { this.lastCreatedInactiveTab[browser.windows.WINDOW_ID_CURRENT] = newTab.id; } - debug( + this.debug( '[createTabInTempContainer] new tab in temp container created', newTab ); if (url) { this.urlCreatedContainer[url] = contextualIdentity.cookieStoreId; delay(1000).then(() => { - debug( + this.debug( '[createTabInTempContainer] cleaning up urlCreatedContainer', url ); @@ -226,7 +250,11 @@ class Container { return newTab; } catch (error) { - debug('[createTabInTempContainer] error while creating new tab', error); + this.debug( + '[createTabInTempContainer] error while creating new tab', + error + ); + throw error; } } @@ -238,7 +266,15 @@ class Container { request, macConfirmPage, dontPin = true, - }) { + }: { + tab?: Tab; + url?: string; + active?: boolean; + deletesHistory?: boolean; + request?: browser.webRequest.WebRequestOnBeforeRequestDetails; + macConfirmPage?: boolean; + dontPin?: boolean; + }): Promise { const newTab = await this.createTabInTempContainer({ tab, url, @@ -255,8 +291,8 @@ class Container { return newTab; } - generateContainerNameIconColor(url) { - let tempContainerNumber = ''; + generateContainerNameIconColor(url?: string): ContainerOptions { + let tempContainerNumber = 0; if (this.pref.container.numberMode.startsWith('keep')) { this.storage.local.tempContainerCounter++; tempContainerNumber = this.storage.local.tempContainerCounter; @@ -277,14 +313,18 @@ class Container { const domain = psl.isValid(parsedUrl.hostname) ? psl.get(parsedUrl.hostname) : parsedUrl.hostname; - containerName = containerName.replace('%domain%', domain); + if (domain) { + containerName = containerName.replace('%domain%', domain); + } } } else { containerName = containerName .replace('%fulldomain%', '') .replace('%domain%', ''); } - containerName = `${containerName}${tempContainerNumber}`; + if (tempContainerNumber) { + containerName = `${containerName}${tempContainerNumber}`; + } if (!containerName) { containerName = ' '; } @@ -315,7 +355,7 @@ class Container { }; } - isPermanent(cookieStoreId) { + isPermanent(cookieStoreId: CookieStoreId): boolean { if ( cookieStoreId !== `${this.background.containerPrefix}-default` && !this.storage.local.tempContainers[cookieStoreId] @@ -325,24 +365,24 @@ class Container { return false; } - isTemporary(cookieStoreId, type) { + isTemporary(cookieStoreId: CookieStoreId, type?: 'deletesHistory'): boolean { return !!( this.storage.local.tempContainers[cookieStoreId] && (!type || this.storage.local.tempContainers[cookieStoreId][type]) ); } - isClean(cookieStoreId) { + isClean(cookieStoreId: CookieStoreId): boolean { return ( this.storage.local.tempContainers[cookieStoreId] && this.storage.local.tempContainers[cookieStoreId].clean ); } - markUnclean(tabId) { + markUnclean(tabId: TabId): void { const cookieStoreId = this.tabs.containerMap.get(tabId); if (cookieStoreId && this.isClean(cookieStoreId)) { - debug( + this.debug( '[markUnclean] marking tmp container as not clean anymore', cookieStoreId ); @@ -350,12 +390,12 @@ class Container { } } - getReusedContainerNumber() { + getReusedContainerNumber(): number { const tempContainersNumbers = this.storage.local.tempContainersNumbers.sort(); if (!tempContainersNumbers.length) { return 1; } else { - const maxContainerNumber = Math.max.apply(Math, tempContainersNumbers); + const maxContainerNumber = Math.max(...tempContainersNumbers); for (let i = 1; i < maxContainerNumber; i++) { if (!tempContainersNumbers.includes(i)) { return i; @@ -365,11 +405,13 @@ class Container { } } - getAvailableContainerColors() { + getAvailableContainerColors(): ContainerColor[] { // even out colors let availableColors = []; const containersOptions = Object.values(this.storage.local.tempContainers); - const assignedColors = {}; + const assignedColors: { + [key: string]: number; + } = {}; let maxColors = 0; for (const containerOptions of containersOptions) { if (typeof containerOptions !== 'object') { @@ -405,43 +447,43 @@ class Container { return availableColors; } - removeFromStorage(cookieStoreId) { + removeFromStorage(cookieStoreId: CookieStoreId): Promise { this.storage.local.tempContainersNumbers = this.storage.local.tempContainersNumbers.filter( - number => - this.storage.local.tempContainers[cookieStoreId].number !== number + containerNumber => + this.storage.local.tempContainers[cookieStoreId].number !== + containerNumber ); delete this.storage.local.tempContainers[cookieStoreId]; return this.storage.persist(); } - getType(cookieStoreId) { + getType(cookieStoreId: CookieStoreId): string { return this.storage.local.tempContainers[cookieStoreId].deletesHistory ? 'deletesHistory' : 'regular'; } - getRemovalDelay(cookieStoreId) { + getRemovalDelay(cookieStoreId: CookieStoreId): number { return this.getType(cookieStoreId) === 'deletesHistory' ? this.pref.deletesHistory.containerRemoval : this.pref.container.removal; } - cleanupNumbers() { + cleanupNumbers(): void { this.storage.local.tempContainersNumbers = Object.values( this.storage.local.tempContainers ).map(container => container.number); } - cleanupNumber(cookieStoreId) { + cleanupNumber(cookieStoreId: CookieStoreId): void { this.storage.local.tempContainersNumbers = this.storage.local.tempContainersNumbers.filter( - number => - this.storage.local.tempContainers[cookieStoreId].number !== number + containerNumber => + this.storage.local.tempContainers[cookieStoreId].number !== + containerNumber ); } - getAllIds() { + getAllIds(): CookieStoreId[] { return Object.keys(this.storage.local.tempContainers); } } - -export default Container; diff --git a/src/background/contextmenu.js b/src/background/contextmenu.ts similarity index 85% rename from src/background/contextmenu.js rename to src/background/contextmenu.ts index 6e4fa880..e6e67993 100644 --- a/src/background/contextmenu.js +++ b/src/background/contextmenu.ts @@ -1,20 +1,27 @@ -class ContextMenu { - constructor(background) { - this.background = background; +import { TemporaryContainers } from './tmp'; +import { Container } from './container'; +import { PreferencesSchema, WindowId, Tab } from '~/types'; + +export class ContextMenu { + private nextMenuInstanceId = 0; + private lastMenuInstanceId = 0; + + private background: TemporaryContainers; + private pref!: PreferencesSchema; + private container!: Container; - this.nextMenuInstanceId = 0; - this.lastMenuInstanceId = 0; + constructor(background: TemporaryContainers) { + this.background = background; } - initialize() { + initialize(): void { this.pref = this.background.pref; - this.storage = this.background.storage; this.container = this.background.container; this.add(); } - async onClicked(info, tab) { + async onClicked(info: browser.menus.OnClickData, tab: Tab): Promise { switch (info.menuItemId) { case 'open-link-in-new-temporary-container-tab': this.container.createTabInTempContainer({ @@ -64,7 +71,7 @@ class ContextMenu { } } - async onShown(info) { + async onShown(info: { bookmarkId: string }): Promise { if (!info.bookmarkId) { return; } @@ -86,7 +93,7 @@ class ContextMenu { this.toggleBookmarks(true); } - async toggleBookmarks(visible) { + async toggleBookmarks(visible: boolean): Promise { if ( this.pref.contextMenuBookmarks && this.background.permissions.bookmarks @@ -112,7 +119,7 @@ class ContextMenu { } } - async add() { + async add(): Promise { if (this.pref.contextMenu) { browser.contextMenus.create({ id: 'open-link-in-new-temporary-container-tab', @@ -169,11 +176,11 @@ class ContextMenu { } } - remove() { + remove(): Promise { return browser.contextMenus.removeAll(); } - async windowsOnFocusChanged(windowId) { + async windowsOnFocusChanged(windowId: WindowId): Promise { if (windowId === browser.windows.WINDOW_ID_NONE) { return; } @@ -181,5 +188,3 @@ class ContextMenu { this.add(); } } - -export default ContextMenu; diff --git a/src/background/convert.js b/src/background/convert.ts similarity index 58% rename from src/background/convert.js rename to src/background/convert.ts index 67448879..93b483c8 100644 --- a/src/background/convert.js +++ b/src/background/convert.ts @@ -1,14 +1,31 @@ -class Convert { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { Container } from './container'; +import { Storage } from './storage'; +import { CookieStoreId, TabId } from '~/types'; + +export class Convert { + private background: TemporaryContainers; + private storage!: Storage; + private container!: Container; + + constructor(background: TemporaryContainers) { this.background = background; } - initialize() { + initialize(): void { this.storage = this.background.storage; this.container = this.background.container; } - async convertTempContainerToPermanent({ cookieStoreId, tabId, name }) { + async convertTempContainerToPermanent({ + cookieStoreId, + tabId, + name, + }: { + cookieStoreId: CookieStoreId; + tabId: TabId; + name: string; + }): Promise { delete this.storage.local.tempContainers[cookieStoreId]; await this.storage.persist(); await browser.contextualIdentities.update(cookieStoreId, { @@ -18,7 +35,13 @@ class Convert { await browser.tabs.reload(tabId); } - async convertTempContainerToRegular({ cookieStoreId, tabId }) { + async convertTempContainerToRegular({ + cookieStoreId, + tabId, + }: { + cookieStoreId: CookieStoreId; + tabId: TabId; + }): Promise { this.storage.local.tempContainers[cookieStoreId].deletesHistory = false; delete this.storage.local.tempContainers[cookieStoreId].history; await this.storage.persist(); @@ -30,7 +53,13 @@ class Convert { await browser.tabs.reload(tabId); } - async convertPermanentToTempContainer({ cookieStoreId, tabId }) { + async convertPermanentToTempContainer({ + cookieStoreId, + tabId, + }: { + cookieStoreId: CookieStoreId; + tabId: TabId; + }): Promise { const containerOptions = this.container.generateContainerNameIconColor(); await browser.contextualIdentities.update(cookieStoreId, { name: containerOptions.name, @@ -42,5 +71,3 @@ class Convert { await browser.tabs.reload(tabId); } } - -export default Convert; diff --git a/src/background/cookies.js b/src/background/cookies.ts similarity index 57% rename from src/background/cookies.js rename to src/background/cookies.ts index 49f9e01b..4627ca81 100644 --- a/src/background/cookies.js +++ b/src/background/cookies.ts @@ -1,55 +1,74 @@ -class Cookies { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { Isolation } from './isolation'; +import { Storage } from './storage'; +import { PreferencesSchema, Tab, Debug } from '~/types'; + +export class Cookies { + private background: TemporaryContainers; + private debug: Debug; + private pref!: PreferencesSchema; + private storage!: Storage; + private isolation!: Isolation; + + constructor(background: TemporaryContainers) { this.background = background; + this.debug = background.debug; } - initialize() { + initialize(): void { this.pref = this.background.pref; this.storage = this.background.storage; this.isolation = this.background.isolation; } - async maybeSetAndAddToHeader(details) { - if (details.tabId < 0 || !Object.keys(this.pref.cookies.domain).length) { + async maybeSetAndAddToHeader( + request: browser.webRequest.WebRequestOnBeforeSendHeadersDetails + ): Promise { + if (request.tabId < 0 || !Object.keys(this.pref.cookies.domain).length) { return; } let tab; try { let cookieHeader; - let cookiesHeader = {}; + let cookiesHeader: { + [key: string]: string; + } = {}; let cookieHeaderChanged = false; for (const domainPattern in this.pref.cookies.domain) { - if (!this.isolation.matchDomainPattern(details.url, domainPattern)) { + if (!this.isolation.matchDomainPattern(request.url, domainPattern)) { continue; } if (!tab) { - tab = await browser.tabs.get(details.tabId); + tab = (await browser.tabs.get(request.tabId)) as Tab; if (!this.storage.local.tempContainers[tab.cookieStoreId]) { - debug( + this.debug( '[maybeSetAndAddCookiesToHeader] not a temporary container', tab ); return; } - cookieHeader = details.requestHeaders.find( - element => element.name.toLowerCase() === 'cookie' + cookieHeader = request.requestHeaders?.find( + (element): boolean => element.name.toLowerCase() === 'cookie' ); - if (cookieHeader) { + if (cookieHeader && cookieHeader.value) { cookiesHeader = cookieHeader.value .split('; ') - .reduce((accumulator, cookie) => { - const split = cookie.split('='); - if (split.length === 2) { - accumulator[split[0]] = split[1]; - } - return accumulator; - }, {}); + .reduce( + (accumulator: { [key: string]: string }, cookie: string) => { + const split = cookie.split('='); + if (split.length === 2) { + accumulator[split[0]] = split[1]; + } + return accumulator; + }, + {} + ); } - debug( + this.debug( '[maybeAddCookiesToHeader] found temp tab and header', - details, + request, cookieHeader, cookiesHeader ); @@ -63,7 +82,7 @@ class Cookies { const setCookie = { domain: cookie.domain || undefined, expirationDate: cookie.expirationDate - ? parseInt(cookie.expirationDate) + ? parseInt(cookie.expirationDate, 10) : undefined, firstPartyDomain: cookie.firstPartyDomain || undefined, httpOnly: @@ -85,12 +104,12 @@ class Cookies { sameSite: cookie.sameSite || undefined, storeId: tab.cookieStoreId, }; - debug('[maybeSetCookies] setting cookie', cookie, setCookie); + this.debug('[maybeSetCookies] setting cookie', cookie, setCookie); const cookieSet = await browser.cookies.set(setCookie); - debug('[maybeSetCookies] cookie set', cookieSet); + this.debug('[maybeSetCookies] cookie set', cookieSet); if (cookiesHeader[cookie.name] === cookie.value) { - debug( + this.debug( '[maybeSetCookies] the set cookie is already in the header', cookie, cookiesHeader @@ -99,35 +118,34 @@ class Cookies { } // check if we're allowed to send the cookie with the current request - let cookieAllowed; try { - cookieAllowed = await browser.cookies.get({ + const cookieAllowed = await browser.cookies.get({ name: cookie.name, - url: details.url, + url: request.url, storeId: tab.cookieStoreId, firstPartyDomain: cookie.firstPartyDomain || undefined, }); - } catch (error) { - cookieAllowed = false; - } - - debug( - '[maybeAddCookiesToHeader] checked if allowed to add cookie to header', - cookieAllowed - ); - if (cookieAllowed) { - cookieHeaderChanged = true; - // eslint-disable-next-line require-atomic-updates - cookiesHeader[cookieAllowed.name] = cookieAllowed.value; - debug( - '[maybeAddCookiesToHeader] cookie value changed', - cookiesHeader + this.debug( + '[maybeAddCookiesToHeader] checked if allowed to add cookie to header', + cookieAllowed ); + + if (cookieAllowed) { + cookieHeaderChanged = true; + // eslint-disable-next-line require-atomic-updates + cookiesHeader[cookieAllowed.name] = cookieAllowed.value; + this.debug( + '[maybeAddCookiesToHeader] cookie value changed', + cookiesHeader + ); + } + } catch (error) { + this.debug('[maybeAddCookiesToHeader] couldnt get cookie', cookie); } } } - debug( + this.debug( '[maybeAddCookiesToHeader] cookieHeaderChanged', cookieHeaderChanged, cookieHeader, @@ -136,42 +154,40 @@ class Cookies { if (!cookieHeaderChanged) { return; } else { - const changedCookieHeaderValues = []; + const changedCookieHeaderValues: string[] = []; Object.keys(cookiesHeader).map(cookieName => { changedCookieHeaderValues.push( `${cookieName}=${cookiesHeader[cookieName]}` ); }); const changedCookieHeaderValue = changedCookieHeaderValues.join('; '); - debug( + this.debug( '[maybeAddCookiesToHeader] changedCookieHeaderValue', changedCookieHeaderValue ); if (cookieHeader) { cookieHeader.value = changedCookieHeaderValue; } else { - details.requestHeaders.push({ + request.requestHeaders?.push({ name: 'Cookie', value: changedCookieHeaderValue, }); } - debug( + this.debug( '[maybeAddCookiesToHeader] changed cookieHeader to', cookieHeader, - details + request ); - return details; + return request; } } catch (error) { - debug( + this.debug( '[maybeAddCookiesToHeader] something went wrong while adding cookies to header', tab, - details.url, + request.url, error ); return; } } } - -export default Cookies; diff --git a/src/background/event-listeners.js b/src/background/event-listeners.js deleted file mode 100644 index dc8cbce2..00000000 --- a/src/background/event-listeners.js +++ /dev/null @@ -1,162 +0,0 @@ -// to have persistent listeners we need to register them early+sync -// and wait for tmp to fully initialize before handling events - -class EventListeners { - constructor() { - debug('[event-listeners] initializing'); - this.tmpInitializedPromiseResolvers = []; - this.tmpInitialized = this.tmpInitialized.bind(this); - this.defaultTimeout = 30; // seconds - this._listeners = []; - - [ - { - api: ['webRequest', 'onBeforeRequest'], - options: [ - { urls: [''], types: ['main_frame'] }, - ['blocking'], - ], - target: ['request', 'webRequestOnBeforeRequest'], - timeout: 5, - }, - { - api: ['webRequest', 'onCompleted'], - options: [{ urls: [''], types: ['main_frame'] }], - target: ['request', 'cleanupCanceled'], - }, - { - api: ['webRequest', 'onErrorOccurred'], - options: [{ urls: [''], types: ['main_frame'] }], - target: ['request', 'cleanupCanceled'], - }, - { - api: ['webRequest', 'onCompleted'], - options: [ - { - urls: [''], - types: ['script', 'font', 'image', 'imageset', 'stylesheet'], - }, - ['responseHeaders'], - ], - target: ['statistics', 'collect'], - }, - { - api: ['browserAction', 'onClicked'], - target: ['browseraction', 'onClicked'], - }, - { - api: ['contextMenus', 'onClicked'], - target: ['contextmenu', 'onClicked'], - }, - { - api: ['contextMenus', 'onShown'], - target: ['contextmenu', 'onShown'], - }, - { - api: ['windows', 'onFocusChanged'], - target: ['contextmenu', 'windowsOnFocusChanged'], - }, - { - api: ['webRequest', 'onBeforeSendHeaders'], - options: [ - { urls: [''], types: ['main_frame'] }, - ['blocking', 'requestHeaders'], - ], - target: ['cookies', 'maybeSetAndAddToHeader'], - }, - { - api: ['management', 'onDisabled'], - target: ['management', 'disable'], - }, - { - api: ['management', 'onUninstalled'], - target: ['management', 'disable'], - }, - { - api: ['management', 'onEnabled'], - target: ['management', 'enable'], - }, - ['commands', 'onCommand'], - ['tabs', 'onActivated'], - ['tabs', 'onCreated'], - ['tabs', 'onUpdated'], - ['tabs', 'onRemoved'], - ['runtime', 'onMessage'], - ['runtime', 'onMessageExternal'], - ['runtime', 'onStartup'], - ].map(conf => { - const confIsObj = typeof conf === 'object'; - const api = (confIsObj && conf.api) || conf; - const target = (confIsObj && conf.target) || api; - const timeout = (confIsObj && conf.timeout) || this.defaultTimeout; - - const listener = this.wrap( - api.join('.'), - function() { - return window.tmp[target[0]][target[1]].call( - window.tmp[target[0]], - ...arguments - ); - }, - { timeout } - ); - - browser[api[0]][api[1]].addListener( - listener, - ...((confIsObj && conf.options) || []) - ); - - this._listeners.push({ listener, api }); - }); - } - - wrap(apiName, listener, options) { - const tmpInitializedPromise = this.createTmpInitializedPromise(options); - - return async function() { - if (!window.tmp || !window.tmp.initialized) { - try { - await tmpInitializedPromise; - } catch (error) { - debug( - `[event-listeners] call to ${apiName} timed out after ${options.timeout}s` - ); - throw error; - } - } - return listener(...arguments); - }; - } - - createTmpInitializedPromise(options) { - const abortController = new AbortController(); - const timeout = window.setTimeout(() => { - abortController.abort(); - }, options.timeout * 1000); - - return new Promise((resolve, reject) => { - this.tmpInitializedPromiseResolvers.push({ resolve, timeout }); - - abortController.signal.addEventListener('abort', () => { - reject('Timed out while waiting for Add-on to initialize'); - }); - }); - } - - tmpInitialized() { - this.tmpInitializedPromiseResolvers.map(resolver => { - clearTimeout(resolver.timeout); - resolver.resolve(); - }); - } - - remove() { - this._listeners.map(listener => { - browser[listener.api[0]][listener.api[1]].removeListener( - listener.listener - ); - }); - } -} - -export default new EventListeners(); diff --git a/src/background/event-listeners.ts b/src/background/event-listeners.ts new file mode 100644 index 00000000..a882ab5e --- /dev/null +++ b/src/background/event-listeners.ts @@ -0,0 +1,223 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { TemporaryContainers } from './tmp'; +import { Debug } from '~/types'; + +// to have persistent listeners we need to register them early+sync +// and wait for tmp to fully initialize before handling events +export class EventListeners { + private background: TemporaryContainers; + private debug: Debug; + private tmpInitializedPromiseResolvers: Array<{ + resolve: () => void; + timeout: number; + }> = []; + private defaultTimeout = 30; // seconds + private listeners: Array<{ + listener: () => void; + api: any; + }> = []; + + constructor(background: TemporaryContainers) { + this.background = background; + this.debug = background.debug; + + this.register(); + } + + register(): void { + this.debug('[event-listeners] registering'); + + browser.webRequest.onBeforeRequest.addListener( + this.wrap( + browser.webRequest.onBeforeRequest, + this.background.request, + 'webRequestOnBeforeRequest', + { timeout: 5 } + ), + { urls: [''], types: ['main_frame'] }, + ['blocking'] + ); + browser.webRequest.onBeforeSendHeaders.addListener( + this.wrap( + browser.webRequest.onBeforeSendHeaders, + this.background.cookies, + 'maybeSetAndAddToHeader' + ), + { urls: [''], types: ['main_frame'] }, + ['blocking', 'requestHeaders'] + ); + browser.webRequest.onCompleted.addListener( + this.wrap( + browser.webRequest.onCompleted, + this.background.statistics, + 'collect' + ), + { + urls: [''], + types: ['script', 'font', 'image', 'imageset', 'stylesheet'], + }, + ['responseHeaders'] + ); + browser.webRequest.onCompleted.addListener( + this.wrap( + browser.webRequest.onCompleted, + this.background.request, + 'cleanupCanceled' + ), + { urls: [''], types: ['main_frame'] } + ); + browser.webRequest.onErrorOccurred.addListener( + this.wrap( + browser.webRequest.onErrorOccurred, + this.background.request, + 'cleanupCanceled' + ), + { urls: [''], types: ['main_frame'] } + ); + browser.browserAction.onClicked.addListener( + this.wrap( + browser.browserAction.onClicked, + this.background.browseraction, + 'onClicked' + ) + ); + browser.contextMenus.onClicked.addListener( + this.wrap( + browser.contextMenus.onClicked, + this.background.contextmenu, + 'onClicked' + ) + ); + browser.contextMenus.onShown.addListener( + this.wrap( + browser.contextMenus.onShown, + this.background.contextmenu, + 'onShown' + ) + ); + browser.windows.onFocusChanged.addListener( + this.wrap( + browser.windows.onFocusChanged, + this.background.contextmenu, + 'windowsOnFocusChanged' + ) + ); + browser.management.onDisabled.addListener( + this.wrap( + browser.management.onDisabled, + this.background.management, + 'disable' + ) + ); + browser.management.onUninstalled.addListener( + this.wrap( + browser.management.onUninstalled, + this.background.management, + 'disable' + ) + ); + browser.management.onEnabled.addListener( + this.wrap( + browser.management.onEnabled, + this.background.management, + 'enable' + ) + ); + browser.management.onInstalled.addListener( + this.wrap( + browser.management.onUninstalled, + this.background.management, + 'enable' + ) + ); + browser.commands.onCommand.addListener( + this.wrap( + browser.commands.onCommand, + this.background.commands, + 'onCommand' + ) + ); + browser.tabs.onActivated.addListener( + this.wrap(browser.tabs.onActivated, this.background.tabs, 'onActivated') + ); + browser.tabs.onCreated.addListener( + this.wrap(browser.tabs.onCreated, this.background.tabs, 'onCreated') + ); + browser.tabs.onUpdated.addListener( + this.wrap(browser.tabs.onUpdated, this.background.tabs, 'onUpdated') + ); + browser.tabs.onRemoved.addListener( + this.wrap(browser.tabs.onRemoved, this.background.tabs, 'onRemoved') + ); + browser.runtime.onMessage.addListener( + this.wrap(browser.runtime.onMessage, this.background.runtime, 'onMessage') + ); + browser.runtime.onMessageExternal.addListener( + this.wrap( + browser.runtime.onMessageExternal, + this.background.runtime, + 'onMessageExternal' + ) + ); + browser.runtime.onStartup.addListener( + this.wrap(browser.runtime.onStartup, this.background.runtime, 'onStartup') + ); + } + + wrap( + api: any, + context: any, + target: any, + options: { timeout: number } = { timeout: this.defaultTimeout } + ): (...listenerArgs: any) => Promise { + const tmpInitializedPromise = this.createTmpInitializedPromise(options); + + const listener = async (...listenerArgs: any): Promise => { + if (!this.background.initialized) { + try { + await tmpInitializedPromise; + } catch (error) { + this.debug( + `[event-listeners] call to ${target.join('.')} timed out after ${ + options.timeout + }s` + ); + throw error; + } + } + + return context[target].call(context, ...listenerArgs); + }; + + this.listeners.push({ listener, api }); + return listener; + } + + createTmpInitializedPromise(options: { timeout: number }): Promise { + const abortController = new AbortController(); + const timeout = window.setTimeout(() => { + abortController.abort(); + }, options.timeout * 1000); + + return new Promise((resolve, reject) => { + this.tmpInitializedPromiseResolvers.push({ resolve, timeout }); + + abortController.signal.addEventListener('abort', () => { + reject('Timed out while waiting for Add-on to initialize'); + }); + }); + } + + public tmpInitialized = (): void => { + this.tmpInitializedPromiseResolvers.map(resolver => { + resolver.resolve(); + window.clearTimeout(resolver.timeout); + }); + }; + + remove(): void { + this.listeners.map(listener => { + listener.api.removeListener(listener.listener); + }); + } +} diff --git a/src/background/external-addons.ts b/src/background/external-addons.ts new file mode 100644 index 00000000..05e6e2b1 --- /dev/null +++ b/src/background/external-addons.ts @@ -0,0 +1,602 @@ +export const CONTAIN_URLS: Map< + string, + { + urls: string[]; + REs: RegExp[]; + } +> = new Map([ + [ + 'facebook', + { + // https://github.com/mozilla/contain-facebook/blob/master/src/background.js + urls: [ + 'facebook.com', + 'www.facebook.com', + 'facebook.net', + 'fb.com', + 'fbcdn.net', + 'fbcdn.com', + 'fbsbx.com', + 'tfbnw.net', + 'facebook-web-clients.appspot.com', + 'fbcdn-profile-a.akamaihd.net', + 'fbsbx.com.online-metrix.net', + 'connect.facebook.net.edgekey.net', + 'instagram.com', + 'cdninstagram.com', + 'instagramstatic-a.akamaihd.net', + 'instagramstatic-a.akamaihd.net.edgesuite.net', + 'messenger.com', + 'm.me', + 'messengerdevelopers.com', + 'atdmt.com', + 'onavo.com', + 'oculus.com', + 'oculusvr.com', + 'oculusbrand.com', + 'oculusforbusiness.com', + ], + REs: [], + }, + ], + [ + 'google', + { + // https://github.com/containers-everywhere/contain-google/blob/master/background.js + urls: [ + 'google.com', + 'google.org', + 'googleapis.com', + 'g.co', + 'ggpht.com', + 'blogger.com', + 'googleblog.com', + 'blog.google', + 'googleusercontent.com', + 'googlesource.com', + 'google.org', + 'google.net', + '466453.com', + 'gooogle.com', + 'gogle.com', + 'ggoogle.com', + 'gogole.com', + 'goolge.com', + 'googel.com', + 'googlee.com', + 'googil.com', + 'googlr.com', + 'elgoog.im', + 'ai.google', + 'com.google', + 'google.ac', + 'google.ad', + 'google.ae', + 'google.com.af', + 'google.com.ag', + 'google.com.ai', + 'google.al', + 'google.am', + 'google.co.ao', + 'google.com.ar', + 'google.as', + 'google.at', + 'google.com.au', + 'google.az', + 'google.ba', + 'google.com.bd', + 'google.be', + 'google.bf', + 'google.bg', + 'google.com.bh', + 'google.bi', + 'google.bj', + 'google.com.bn', + 'google.com.bo', + 'google.com.br', + 'google.bs', + 'google.bt', + 'google.com.bw', + 'google.by', + 'google.com.bz', + 'google.ca', + 'google.com.kh', + 'google.cc', + 'google.cd', + 'google.cf', + 'google.cat', + 'google.cg', + 'google.ch', + 'google.ci', + 'google.co.ck', + 'google.cl', + 'google.cm', + 'google.cn', + 'google.com.co', + 'google.co.cr', + 'google.com.cu', + 'google.cv', + 'google.com.cy', + 'google.cz', + 'google.de', + 'google.dj', + 'google.dk', + 'google.dm', + 'google.com.do', + 'google.dz', + 'google.com.ec', + 'google.ee', + 'google.com.eg', + 'google.es', + 'google.com.et', + 'google.fi', + 'google.com.fj', + 'google.fm', + 'google.fr', + 'google.ga', + 'google.ge', + 'google.gf', + 'google.gg', + 'google.com.gh', + 'google.com.gi', + 'google.gl', + 'google.gm', + 'google.gp', + 'google.gr', + 'google.com.gt', + 'google.gy', + 'google.com.hk', + 'google.hn', + 'google.hr', + 'google.ht', + 'google.hu', + 'google.co.id', + 'google.iq', + 'google.ie', + 'google.co.il', + 'google.im', + 'google.co.in', + 'google.io', + 'google.is', + 'google.it', + 'google.je', + 'google.com.jm', + 'google.jo', + 'google.co.jp', + 'google.co.ke', + 'google.ki', + 'google.kg', + 'google.co.kr', + 'google.com.kw', + 'google.kz', + 'google.la', + 'google.lb', + 'google.com.lc', + 'google.li', + 'google.lk', + 'google.co.ls', + 'google.lt', + 'google.lu', + 'google.lv', + 'google.com.ly', + 'google.co.ma', + 'google.md', + 'google.me', + 'google.mg', + 'google.mk', + 'google.ml', + 'google.com.mm', + 'google.mn', + 'google.ms', + 'google.com.mt', + 'google.mu', + 'google.mv', + 'google.mw', + 'google.com.mx', + 'google.com.my', + 'google.co.mz', + 'google.com.na', + 'google.ne', + 'google.com.nf', + 'google.com.ng', + 'google.com.ni', + 'google.nl', + 'google.no', + 'google.com.np', + 'google.nr', + 'google.nu', + 'google.co.nz', + 'google.com.om', + 'google.com.pk', + 'google.com.pa', + 'google.com.pe', + 'google.com.ph', + 'google.pl', + 'google.com.pg', + 'google.pn', + 'google.com.pr', + 'google.ps', + 'google.pt', + 'google.com.py', + 'google.com.qa', + 'google.ro', + 'google.rs', + 'google.ru', + 'google.rw', + 'google.com.sa', + 'google.com.sb', + 'google.sc', + 'google.se', + 'google.com.sg', + 'google.sh', + 'google.si', + 'google.sk', + 'google.com.sl', + 'google.sn', + 'google.sm', + 'google.so', + 'google.st', + 'google.sr', + 'google.com.sv', + 'google.td', + 'google.tg', + 'google.co.th', + 'google.com.tj', + 'google.tk', + 'google.tl', + 'google.tm', + 'google.to', + 'google.tn', + 'google.com.tr', + 'google.tt', + 'google.com.tw', + 'google.co.tz', + 'google.com.ua', + 'google.co.ug', + 'google.co.uk', + 'google.us', + 'google.com.uy', + 'google.co.uz', + 'google.com.vc', + 'google.co.ve', + 'google.vg', + 'google.co.vi', + 'google.com.vn', + 'google.vu', + 'google.ws', + 'google.co.za', + 'google.co.zm', + 'google.co.zw', + 'like.com', + 'keyhole.com', + 'panoramio.com', + 'picasa.com', + 'urchin.com', + 'igoogle.com', + 'foofle.com', + 'froogle.com', + 'localguidesconnect.com', + 'googlemail.com', + 'googleanalytics.com', + 'google-analytics.com', + 'googletagmanager.com', + 'googlecode.com', + 'googlesource.com', + 'googledrive.com', + 'googlearth.com', + 'googleearth.com', + 'googlemaps.com', + 'googlepagecreator.com', + 'googlescholar.com', + 'advertisercommunity.com', + 'thinkwithgoogle.com', + 'youtube.com', + 'youtu.be', + 'yt.be', + 'ytimg.com', + ' youtube-nocookie.com', + 'youtubegaming.com', + 'youtubeeducation.com', + 'youtube-nocookie.com', + 'blogspot.com', + 'blogspot.ae', + 'blogspot.al', + 'blogspot.am', + 'blogspot.com.ar', + 'blogspot.co.at', + 'blogspot.com.au', + 'blogspot.ba', + 'blogspot.be', + 'blogspot.bg', + 'blogspot.bj', + 'blogspot.com.br', + 'blogspot.com.by', + 'blogspot.ca', + 'blogspot.cf', + 'blogspot.ch', + 'blogspot.cl', + 'blogspot.com.co', + 'blogspot.cv', + 'blogspot.com.cy', + 'blogspot.cz', + 'blogspot.de', + 'blogspot.dj', + 'blogspot.dk', + 'blogspot.dm', + 'blogspot.com.do', + 'blogspot.dz', + 'blogspot.com.eg', + 'blogspot.es', + 'blogspot.fi', + 'blogspot.fr', + 'blogspot.gr', + 'blogspot.hr', + 'blogspot.hu', + 'blogspot.co.id', + 'blogspot.ie', + 'blogspot.co.il', + 'blogspot.in', + 'blogspot.is', + 'blogspot.it', + 'blogspot.jp', + 'blogspot.co.ke', + 'blogspot.kr', + 'blogspot.li', + 'blogspot.lt', + 'blogspot.lu', + 'blogspot.md', + 'blogspot.mk', + 'blogspot.com.mt', + 'blogspot.mx', + 'blogspot.my', + 'blogspot.com.ng', + 'blogspot.nl', + 'blogspot.no', + 'blogspot.co.nz', + 'blogspot.pt', + 'blogspot.qa', + 'blogspot.ro', + 'blogspot.rs', + 'blogspot.ru', + 'blogspot.se', + 'blogspot.sg', + 'blogspot.si', + 'blogspot.sk', + 'blogspot.sn', + 'blogspot.com.sr', + 'blogspot.td', + 'blogspot.co.tl', + 'blogspot.co.to', + 'blogspot.com.tr', + 'blogspot.tw', + 'blogspot.co.uk', + 'blogspot.com.uy', + 'blogspot.co.za', + 'abc.xyz', + 'waze.com', + 'capitalg.com', + 'gv.com', + 'calicolabs.com', + 'x.company', + 'nest.com', + 'sidewalklabs.com', + 'verily.com', + 'doubleclickbygoogle.com', + 'feedburner.com', + 'doubleclick.com', + 'doubleclick.net', + 'adwords.com', + 'adsense.com', + 'admob.com', + 'advertisercommunity.com', + 'googlesyndication.com', + 'googlecommerce.com', + 'googlebot.com', + 'googleapps.com', + 'googleadservices.com', + 'gmodules.com', + 'googl.com', + '1e100.net', + 'domains.google', + 'gv.com', + 'madewithcode.com', + 'design.google', + 'gallery.io', + 'domains.google', + 'material.io', + 'android.com', + 'chromium.org', + 'cobrasearch.com', + 'chromecast.com', + 'chrome.com', + 'chromebook.com', + 'madewithcode.com', + 'whatbrowser.org', + 'withgoogle.com', + 'web.dev', + ], + REs: [], + }, + ], + [ + 'twitter', + { + // https://github.com/v1shwa/contain-twitter/blob/master/background.js + urls: [ + 'twitter.com', + 'www.twitter.com', + 't.co', + 'twimg.com', + 'mobile.twitter.com', + 'm.twitter.com', + 'api.twitter.com', + 'abs.twimg.com', + 'ton.twimg.com', + 'pbs.twimg.com', + 'tweetdeck.twitter.com', + ], + REs: [], + }, + ], + [ + 'youtube', + { + // https://github.com/AbdullahDiaa/contain-youtube/blob/master/src/background.js + urls: ['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be'], + REs: [], + }, + ], + [ + 'amazon', + { + // https://github.com/Jackymancs4/contain-amazon/blob/master/background.js + urls: [ + 'amazon.it', + 'amazon.de', + 'amazon.com', + 'amazon.com.br', + 'amazon.in', + 'amazon.com.au', + 'amazon.es', + 'amazon.com.mx', + 'amazon.co.jp', + 'amazon.in', + 'amazon.co.uk', + 'amazon.ca', + 'amazon.fr', + 'amazon.com.sg', + 'awscloud.com', + 'amazon.company', + 'amazon.express', + 'amazon.gd', + 'amazon.international', + 'amazon.ltda', + 'amazon.press', + 'amazon.shopping', + 'amazon.tickets', + 'amazon.tv', + 'amazon.cruises', + 'amazon.dog', + 'amazon.express', + 'amazon.game', + 'amazon.gent', + 'amazon.salon', + 'amazon.shopping', + 'amazon.tours', + 'amazon.wiki', + 'amazon.clothing', + 'amazon.energy', + 'amazon.fund', + 'amazon.hockey', + 'amazon.kiwi', + 'amazon.re', + 'amazon.soccer', + 'amazon.tienda', + 'amazon.training', + 'amazon.jobs', + 'primevideo.com', + 'mturk.com', + 'lab126.com', + 'amazonpay.in', + 'amazonteam.org', + 'awsevents.com', + 'seattlespheres.com', + ], + REs: [], + }, + ], +]); + +CONTAIN_URLS.forEach(containWhat => { + for (const url of containWhat.urls) { + containWhat.REs.push(new RegExp(`^(.*\\.)?${url}$`)); + } +}); + +export const addons: Map< + string, + { + name: string; + enabled: boolean; + version: false | string; + REs?: RegExp[]; + } +> = new Map([ + [ + '@testpilot-containers', + { + name: 'Firefox Multi-Account Containers', + enabled: false, + version: false, + }, + ], + [ + 'containerise@kinte.sh', + { + name: 'Containerise', + enabled: false, + version: false, + }, + ], + [ + 'block_outside_container@jspenguin.org', + { + name: 'Block sites outside container', + enabled: false, + version: false, + }, + ], + [ + 'treestyletab@piro.sakura.ne.jp', + { + name: 'Tree Style Tab', + enabled: false, + version: false, + }, + ], + [ + '@contain-facebook', + { + name: 'Facebook Container', + enabled: false, + version: false, + REs: CONTAIN_URLS.get('facebook')?.REs, + }, + ], + [ + '@contain-google', + { + name: 'Google Container', + enabled: false, + version: false, + REs: CONTAIN_URLS.get('google')?.REs, + }, + ], + [ + '@contain-twitter', + { + name: 'Twitter Container', + enabled: false, + version: false, + REs: CONTAIN_URLS.get('twitter')?.REs, + }, + ], + [ + '@contain-youtube', + { + name: 'YouTube Container', + enabled: false, + version: false, + REs: CONTAIN_URLS.get('youtube')?.REs, + }, + ], + [ + '@contain-amazon', + { + name: 'Amazon Container', + enabled: false, + version: false, + REs: CONTAIN_URLS.get('amazon')?.REs, + }, + ], +]); diff --git a/src/background/history.js b/src/background/history.js deleted file mode 100644 index eed31d34..00000000 --- a/src/background/history.js +++ /dev/null @@ -1,52 +0,0 @@ -class History { - constructor(background) { - this.background = background; - } - - initialize() { - this.storage = this.background.storage; - } - - async maybeAddHistory(tab, url) { - if (!tab || url === 'about:blank' || url === 'about:newtab') { - return; - } - if ( - tab.cookieStoreId !== `${this.background.containerPrefix}-default` && - this.storage.local.tempContainers[tab.cookieStoreId] && - this.storage.local.tempContainers[tab.cookieStoreId].deletesHistory - ) { - if (!this.storage.local.tempContainers[tab.cookieStoreId].history) { - this.storage.local.tempContainers[tab.cookieStoreId].history = {}; - } - this.storage.local.tempContainers[tab.cookieStoreId].history[url] = { - tabId: tab.id, - }; - await this.storage.persist(); - } - } - - maybeClearHistory(cookieStoreId) { - let count = 0; - if ( - this.storage.local.tempContainers[cookieStoreId] && - this.storage.local.tempContainers[cookieStoreId].deletesHistory && - this.storage.local.tempContainers[cookieStoreId].history - ) { - const urls = Object.keys( - this.storage.local.tempContainers[cookieStoreId].history - ); - count = urls.length; - urls.map(url => { - if (!url) { - return; - } - debug('[maybeClearHistory] removing url from history', url); - browser.history.deleteUrl({ url }); - }); - } - return count; - } -} - -export default History; diff --git a/src/background/history.ts b/src/background/history.ts new file mode 100644 index 00000000..1aafd408 --- /dev/null +++ b/src/background/history.ts @@ -0,0 +1,56 @@ +import { TemporaryContainers } from './tmp'; +import { Storage } from './storage'; +import { Tab, CookieStoreId, Debug } from '~/types'; + +export class History { + private background: TemporaryContainers; + private debug: Debug; + private storage!: Storage; + + constructor(background: TemporaryContainers) { + this.background = background; + this.debug = background.debug; + } + + initialize(): void { + this.storage = this.background.storage; + } + + async maybeAddHistory(tab: Tab | undefined, url: string): Promise { + if (!tab || url === 'about:blank' || url === 'about:newtab') { + return; + } + const cookieStoreId = tab.cookieStoreId; + const container = this.storage.local.tempContainers[cookieStoreId]; + if ( + cookieStoreId !== `${this.background.containerPrefix}-default` && + container && + container.deletesHistory + ) { + if (!container.history) { + container.history = {}; + } + container.history[url] = { + tabId: tab.id, + }; + await this.storage.persist(); + } + } + + maybeClearHistory(cookieStoreId: CookieStoreId): number { + let count = 0; + const container = this.storage.local.tempContainers[cookieStoreId]; + if (container && container.deletesHistory && container.history) { + const urls = Object.keys(container.history); + count = urls.length; + urls.map(url => { + if (!url) { + return; + } + this.debug('[maybeClearHistory] removing url from history', url); + browser.history.deleteUrl({ url }); + }); + } + return count; + } +} diff --git a/src/background/isolation.js b/src/background/isolation.ts similarity index 71% rename from src/background/isolation.js rename to src/background/isolation.ts index 17f2253a..68521955 100644 --- a/src/background/isolation.js +++ b/src/background/isolation.ts @@ -1,11 +1,36 @@ -class Isolation { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { Container } from './container'; +import { MultiAccountContainers } from './mac'; +import { Management } from './management'; +import { MouseClick } from './mouseclick'; +import { Request } from './request'; +import { Utils } from './utils'; +import { + PreferencesSchema, + IsolationAction, + Tab, + MacAssignment, + Debug, +} from '~/types'; + +export class Isolation { + private background: TemporaryContainers; + private debug: Debug; + private pref!: PreferencesSchema; + private container!: Container; + private request!: Request; + private mouseclick!: MouseClick; + private mac!: MultiAccountContainers; + private management!: Management; + private utils!: Utils; + + constructor(background: TemporaryContainers) { this.background = background; + this.debug = background.debug; } - initialize() { + initialize(): void { this.pref = this.background.pref; - this.storage = this.background.storage; this.container = this.background.container; this.request = this.background.request; this.mouseclick = this.background.mouseclick; @@ -14,10 +39,20 @@ class Isolation { this.utils = this.background.utils; } - async maybeIsolate({ tab, request, openerTab, macAssignment }) { + async maybeIsolate({ + tab, + request, + openerTab, + macAssignment, + }: { + tab?: Tab; + request: browser.webRequest.WebRequestOnBeforeRequestDetails; + openerTab?: Tab; + macAssignment?: MacAssignment; + }): Promise { if (!this.pref.isolation.active) { - debug('[maybeIsolate] isolation is disabled'); - return; + this.debug('[maybeIsolate] isolation is disabled'); + return false; } if ( tab && @@ -25,9 +60,9 @@ class Isolation { request.originUrl && this.mac.isConfirmPage(request.originUrl) ) { - debug('[maybeIsolate] we are coming from a mac confirm page'); + this.debug('[maybeIsolate] we are coming from a mac confirm page'); this.mac.containerConfirmed[tab.id] = tab.cookieStoreId; - return; + return false; } if ( @@ -36,21 +71,21 @@ class Isolation { tab.cookieStoreId !== `${this.background.containerPrefix}-default` && this.container.urlCreatedContainer[request.url] === tab.cookieStoreId ) { - debug( + this.debug( '[maybeIsolate] link click already created this container, we can stop here', request, tab ); - return; + return false; } if ( !(await this.shouldIsolate({ tab, request, openerTab, macAssignment })) ) { - debug('[maybeIsolate] decided to not isolate', tab, request); + this.debug('[maybeIsolate] decided to not isolate', tab, request); return false; } - debug('[maybeIsolate] decided to isolate', tab, request); + this.debug('[maybeIsolate] decided to isolate', tab, request); const excludedDomainPatterns = Object.keys( this.pref.isolation.global.excluded @@ -60,7 +95,7 @@ class Isolation { return this.matchDomainPattern(request.url, excludedDomainPattern); }); if (excluded) { - debug( + this.debug( '[maybeIsolate] request url matches global excluded domain pattern', request, excludedDomainPatterns @@ -74,7 +109,10 @@ class Isolation { this.container.isPermanent(tab.cookieStoreId) && this.pref.isolation.global.excludedContainers[tab.cookieStoreId] ) { - debug('[maybeIsolate] container on global excluded containers list', tab); + this.debug( + '[maybeIsolate] container on global excluded containers list', + tab + ); return false; } @@ -84,17 +122,17 @@ class Isolation { this.mac.containerConfirmed[tab.id] && tab.cookieStoreId === this.mac.containerConfirmed[tab.id] ) { - debug( + this.debug( '[maybeIsolate] mac confirmed container, not isolating', this.mac.containerConfirmed, macAssignment ); - return; + return false; } - debug('[maybeIsolate] isolating', tab, request); + this.debug('[maybeIsolate] isolating', tab, request); if (this.request.cancelRequest(request)) { - debug('[maybeIsolate] canceling request'); + this.debug('[maybeIsolate] canceling request'); return { cancel: true }; } @@ -102,7 +140,7 @@ class Isolation { macAssignment && (!tab || (tab && tab.cookieStoreId !== macAssignment.cookieStoreId)) ) { - debug( + this.debug( '[maybeIsolate] decided to reopen but mac assigned, maybe reopen confirmpage', request, tab, @@ -140,11 +178,12 @@ class Isolation { } if ( - reload || - tab.url === 'about:home' || - tab.url === 'about:newtab' || - tab.url === 'about:blank' || - this.pref.replaceTabs + tab && + (reload || + tab.url === 'about:home' || + tab.url === 'about:newtab' || + tab.url === 'about:blank' || + this.pref.replaceTabs) ) { await this.container.reloadTabInTempContainer(params); } else { @@ -153,13 +192,23 @@ class Isolation { return { cancel: true }; } - async shouldIsolate({ tab, request, openerTab, macAssignment }) { - debug('[shouldIsolate]', tab, request); + async shouldIsolate({ + tab, + request, + openerTab, + macAssignment, + }: { + tab?: Tab; + request: browser.webRequest.WebRequestOnBeforeRequestDetails; + openerTab?: Tab; + macAssignment?: MacAssignment; + }): Promise { + this.debug('[shouldIsolate]', tab, request); // special-case TST group tabs #264 if ( openerTab && - this.management.addons['treestyletab@piro.sakura.ne.jp'].enabled + this.management.addons.get('treestyletab@piro.sakura.ne.jp')?.enabled ) { try { const treeItem = await browser.runtime.sendMessage( @@ -170,7 +219,7 @@ class Isolation { } ); if (treeItem && treeItem.states.includes('group-tab')) { - debug( + this.debug( '[shouldIsolate] not isolating because originated from TST group tag', openerTab, tab, @@ -179,7 +228,7 @@ class Isolation { return false; } } catch (error) { - debug('[shouldIsolate] failed contacting TST', error.toString()); + this.debug('[shouldIsolate] failed contacting TST', error.toString()); } } @@ -191,7 +240,15 @@ class Isolation { ); } - shouldIsolateMouseClick({ request, tab, openerTab }) { + shouldIsolateMouseClick({ + request, + tab, + openerTab, + }: { + tab?: Tab; + request: browser.webRequest.WebRequestOnBeforeRequestDetails; + openerTab?: Tab; + }): boolean { if (!this.mouseclick.isolated[request.url]) { return false; } @@ -202,7 +259,7 @@ class Isolation { this.mouseclick.isolated[request.url].tab.id ) ) { - debug( + this.debug( '[shouldIsolateMouseClick] not isolating mouse click because tab/openerTab id is different', request, tab, @@ -212,14 +269,14 @@ class Isolation { return false; } - debug( + this.debug( '[beforeHandleRequest] decreasing isolated mouseclick count', this.mouseclick.isolated[request.url] ); this.mouseclick.isolated[request.url].count--; if (this.mouseclick.isolated[request.url].count < 0) { - debug( + this.debug( '[shouldIsolateMouseClick] not isolating and removing isolated mouseclick because its count is < 0', this.mouseclick.isolated[request.url] ); @@ -229,7 +286,7 @@ class Isolation { } if (!this.mouseclick.isolated[request.url].count) { - debug( + this.debug( '[shouldIsolateMouseClick] removing isolated mouseclick because its count is 0', this.mouseclick.isolated[request.url] ); @@ -237,16 +294,24 @@ class Isolation { delete this.mouseclick.isolated[request.url]; } - debug( + this.debug( '[shouldIsolateMouseClick] decided to isolate mouseclick', this.mouseclick.isolated[request.url] ); return true; } - async shouldIsolateNavigation({ request, tab, openerTab }) { + async shouldIsolateNavigation({ + request, + tab, + openerTab, + }: { + tab?: Tab; + request: browser.webRequest.WebRequestOnBeforeRequestDetails; + openerTab?: Tab; + }): Promise { if (!tab || !tab.url) { - debug( + this.debug( '[shouldIsolateNavigation] we cant proceed without tab url information', tab, request @@ -260,7 +325,7 @@ class Isolation { tab.url === 'about:home') && !openerTab ) { - debug( + this.debug( '[shouldIsolateNavigation] not isolating because the tab url is blank/newtab/home and no openerTab' ); return false; @@ -272,7 +337,7 @@ class Isolation { this.container.isPermanent(tab.cookieStoreId) && openerTab.cookieStoreId !== tab.cookieStoreId ) { - debug( + this.debug( '[shouldIsolateNavigation] the tab loads a permanent container that is different from the openerTab, probaby explicitly selected in the context menu' ); return false; @@ -290,7 +355,7 @@ class Isolation { const parsedURL = url.startsWith('about:') || url.startsWith('moz-extension:') ? url - : new URL(url); + : new URL(url).hostname; const parsedRequestURL = new URL(request.url); for (const patternPreferences of this.pref.isolation.domain) { @@ -315,7 +380,7 @@ class Isolation { if (!this.matchDomainPattern(request.url, excludedDomainPattern)) { continue; } - debug( + this.debug( '[shouldIsolateNavigation] not isolating because excluded domain pattern matches', request.url, excludedDomainPattern @@ -326,22 +391,21 @@ class Isolation { if (patternPreferences.navigation) { const navigationPreferences = patternPreferences.navigation; - debug( + this.debug( '[shouldIsolateNavigation] found pattern', domainPattern, navigationPreferences ); if (navigationPreferences.action === 'global') { - debug('[shouldIsolateNavigation] breaking because "global"'); + this.debug('[shouldIsolateNavigation] breaking because "global"'); break; } return await this.checkIsolationPreferenceAgainstUrl( navigationPreferences.action, - parsedURL.hostname, - parsedRequestURL.hostname, - tab + parsedURL, + parsedRequestURL.hostname ); } } @@ -349,21 +413,28 @@ class Isolation { if ( await this.checkIsolationPreferenceAgainstUrl( this.pref.isolation.global.navigation.action, - parsedURL.hostname, - parsedRequestURL.hostname, - tab + parsedURL, + parsedRequestURL.hostname ) ) { return true; } - debug('[shouldIsolateNavigation] not isolating'); + this.debug('[shouldIsolateNavigation] not isolating'); return false; } - async shouldIsolateAlways({ request, tab, openerTab }) { + async shouldIsolateAlways({ + request, + tab, + openerTab, + }: { + tab?: Tab; + request: browser.webRequest.WebRequestOnBeforeRequestDetails; + openerTab?: Tab; + }): Promise { if (!tab || !tab.url) { - debug( + this.debug( '[shouldIsolateAlways] we cant proceed without tab url information', tab, request @@ -381,13 +452,15 @@ class Isolation { } const preferences = patternPreferences.always; - debug( + this.debug( '[shouldIsolateAlways] found pattern for incoming request url', domainPattern, preferences ); if (preferences.action === 'disabled') { - debug('[shouldIsolateAlways] not isolating because "always" disabled'); + this.debug( + '[shouldIsolateAlways] not isolating because "always" disabled' + ); continue; } @@ -395,7 +468,7 @@ class Isolation { preferences.allowedInPermanent && this.container.isPermanent(tab.cookieStoreId) ) { - debug( + this.debug( '[shouldIsolateAlways] not isolating because disabled in permanent container' ); continue; @@ -403,12 +476,14 @@ class Isolation { const isTemporary = this.container.isTemporary(tab.cookieStoreId); if (!isTemporary) { - debug('[shouldIsolateAlways] isolating because not in a tmp container'); + this.debug( + '[shouldIsolateAlways] isolating because not in a tmp container' + ); return true; } if (preferences.allowedInTemporary && isTemporary) { - debug( + this.debug( '[shouldIsolateAlways] not isolating because disabled in tmp container' ); return false; @@ -422,14 +497,14 @@ class Isolation { this.matchDomainPattern(openerTab.url, domainPattern) ) { openerMatches = true; - debug( + this.debug( '[shouldIsolateAlways] opener tab url matched the pattern', openerTab.url, domainPattern ); } if (!openerMatches) { - debug( + this.debug( '[shouldIsolateAlways] isolating because the tab/opener url doesnt match the pattern', tab.url, openerTab, @@ -439,68 +514,82 @@ class Isolation { } } } + return false; } - shouldIsolateMac({ tab, macAssignment }) { + shouldIsolateMac({ + tab, + macAssignment, + }: { + tab?: Tab; + macAssignment?: MacAssignment; + }): boolean { if (this.pref.isolation.mac.action === 'disabled') { - debug('[shouldIsolateMac] mac isolation disabled'); + this.debug('[shouldIsolateMac] mac isolation disabled'); return false; } - if (!this.container.isPermanent(tab.cookieStoreId)) { - debug('[shouldIsolateMac] we are not in a permanent container'); + if (tab && !this.container.isPermanent(tab.cookieStoreId)) { + this.debug('[shouldIsolateMac] we are not in a permanent container'); return false; } if ( !macAssignment || - (macAssignment && tab.cookieStoreId !== macAssignment.cookieStoreId) + (macAssignment && + tab && + tab.cookieStoreId !== macAssignment.cookieStoreId) ) { - debug( + this.debug( '[shouldIsolateMac] mac isolating because request url is not assigned to the tabs container' ); return true; } - debug('[shouldIsolateMac] no mac isolation', tab, macAssignment); + this.debug('[shouldIsolateMac] no mac isolation', tab, macAssignment); return false; } - async checkIsolationPreferenceAgainstUrl(preference, origin, target, tab) { - debug( + async checkIsolationPreferenceAgainstUrl( + preference: IsolationAction, + origin: string, + target: string + ): Promise { + this.debug( '[checkIsolationPreferenceAgainstUrl]', preference, origin, - target, - tab + target ); switch (preference) { case 'always': - debug( + this.debug( '[checkIsolationPreferenceAgainstUrl] isolating based on "always"' ); return true; case 'notsamedomainexact': if (target !== origin) { - debug( + this.debug( '[checkIsolationPreferenceAgainstUrl] isolating based on "notsamedomainexact"' ); return true; } - return false; case 'notsamedomain': if (!this.utils.sameDomain(origin, target)) { - debug( + this.debug( '[checkIsolationPreferenceAgainstUrl] isolating based on "notsamedomain"' ); return true; } - return false; } + return false; } - matchDomainPattern(url, domainPattern) { + matchDomainPattern(url: string, domainPattern: string): boolean { if (domainPattern.startsWith('/')) { const regexp = domainPattern.match(/^\/(.*)\/([gimsuy]+)?$/); + if (!regexp) { + return false; + } try { return new RegExp(regexp[1], regexp[2]).test(url); } catch (error) { @@ -510,13 +599,11 @@ class Isolation { const parsedUrl = url.startsWith('about:') || url.startsWith('moz-extension:') ? url - : new URL(url); + : new URL(url).hostname; return ( - parsedUrl.hostname === domainPattern || - this.utils.globToRegexp(domainPattern).test(parsedUrl.hostname) + parsedUrl === domainPattern || + this.utils.globToRegexp(domainPattern).test(parsedUrl) ); } } } - -export default Isolation; diff --git a/src/background/lib.js b/src/background/lib.ts similarity index 56% rename from src/background/lib.js rename to src/background/lib.ts index 43f55489..34a0fe8d 100644 --- a/src/background/lib.js +++ b/src/background/lib.ts @@ -2,6 +2,4 @@ import delay from 'delay'; import PQueue from 'p-queue'; import psl from 'psl'; -window.delay = delay; -window.PQueue = PQueue; -window.psl = psl; \ No newline at end of file +export { delay, PQueue, psl }; diff --git a/src/background/log.js b/src/background/log.ts similarity index 76% rename from src/background/log.js rename to src/background/log.ts index 92de9240..337ec5d7 100644 --- a/src/background/log.js +++ b/src/background/log.ts @@ -1,21 +1,23 @@ -/* eslint-disable no-console */ +import { Debug } from '~/types'; -class Log { - constructor() { - this.DEBUG = false; - this.stringify = true; - this.checkedLocalStorage = false; - this.checkLocalStoragePromise = this.checkLocalStorage(); +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export class Log { + public DEBUG = false; + public stringify = true; + private checkedLocalStorage = false; + private checkLocalStoragePromise = this.checkLocalStorage(); + constructor() { this.debug = this.debug.bind(this); browser.runtime.onInstalled.addListener( this.onInstalledListener.bind(this) ); } - async debug(...args) { + public debug: Debug = async (...args: any[]): Promise => { let date; - if (!this.checkedLocalStorage && !browser._mochaTest) { + if (!this.checkedLocalStorage && !window._mochaTest) { date = new Date().toUTCString(); await this.checkLocalStoragePromise; } @@ -37,15 +39,15 @@ class Log { return arg; }); - if (this.stringify && !browser._mochaTest) { - console.log(date, ...args.map(JSON.stringify)); + if (this.stringify && !window._mochaTest) { + console.log(date, ...args.map(value => JSON.stringify(value))); console.log('------------------------------------------'); } else { console.log(date, ...args.slice(0)); } - } + }; - checkLocalStorage() { + checkLocalStorage(): void | Promise { if (this.DEBUG) { return; } @@ -70,7 +72,7 @@ class Log { ); } - onInstalledListener(details) { + onInstalledListener(details: any): void { browser.runtime.onInstalled.removeListener(this.onInstalledListener); if (!this.DEBUG && details.temporary) { @@ -90,7 +92,3 @@ class Log { } } } - -window.log = new Log(); -// eslint-disable-next-line -window.debug = log.debug; \ No newline at end of file diff --git a/src/background/mac.js b/src/background/mac.ts similarity index 65% rename from src/background/mac.js rename to src/background/mac.ts index 1024a945..3d01fea5 100644 --- a/src/background/mac.js +++ b/src/background/mac.ts @@ -1,24 +1,65 @@ -class MultiAccountContainers { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { Container } from './container'; +import { delay } from './lib'; +import { Storage } from './storage'; +import { + PreferencesSchema, + CookieStoreId, + MacAssignment, + Tab, + Debug, +} from '~/types'; + +interface ConfirmPage { + tab: Tab; + targetURL: string; + targetContainer: CookieStoreId; + currentContainer: false | CookieStoreId; +} + +interface WaitingForConfirmPage { + targetContainer: CookieStoreId; + request: browser.webRequest.WebRequestOnBeforeRequestDetails; + tab?: Tab; + deletesHistoryContainer: boolean; +} + +export class MultiAccountContainers { + public containerConfirmed: { + [key: number]: CookieStoreId; + } = {}; + + private confirmPage: { + [key: string]: ConfirmPage; + } = {}; + private waitingForConfirmPage: { + [key: string]: WaitingForConfirmPage; + } = {}; + + private background: TemporaryContainers; + private debug: Debug; + private pref!: PreferencesSchema; + private storage!: Storage; + private container!: Container; + + constructor(background: TemporaryContainers) { this.background = background; - this.confirmPage = {}; - this.waitingForConfirmPage = {}; - this.containerConfirmed = {}; + this.debug = background.debug; } - initialize() { + initialize(): void { this.pref = this.background.pref; this.storage = this.background.storage; this.container = this.background.container; } - isConfirmPage(url) { - return url.match(/moz-extension:\/\/[^/]*\/confirm-page.html\?url=/); + isConfirmPage(url: string): boolean { + return !!url.match(/moz-extension:\/\/[^/]*\/confirm-page.html\?url=/); } - handleConfirmPage(tab) { + handleConfirmPage(tab: Tab): void { if (tab && tab.id && this.container.tabCreatedAsMacConfirmPage[tab.id]) { - debug( + this.debug( '[handleConfirmPage] we reopened a confirmpage in that tab already', tab ); @@ -26,45 +67,52 @@ class MultiAccountContainers { } const multiAccountMatch = this.isConfirmPage(tab.url); if (multiAccountMatch) { - debug('[handleConfirmPage] is intervening', tab, multiAccountMatch); + this.debug('[handleConfirmPage] is intervening', tab, multiAccountMatch); const parsedURL = new URL(tab.url); const queryParams = parsedURL.search .split('&') .map(param => param.split('=')); - const confirmPage = { + + const confirmPage: ConfirmPage = { tab, targetURL: decodeURIComponent(queryParams[0][1]), targetContainer: queryParams[1][1], currentContainer: queryParams[2] ? queryParams[2][1] : false, }; - debug('[handleConfirmPage] parsed url', queryParams, confirmPage); + this.debug('[handleConfirmPage] parsed url', queryParams, confirmPage); if (this.waitingForConfirmPage[confirmPage.targetContainer]) { - debug( + this.debug( '[handleConfirmPage] we are already waiting for this confirm page, maybe reopen', confirmPage.targetContainer ); this._maybeReopenConfirmPage( this.waitingForConfirmPage[confirmPage.targetContainer], + false, confirmPage ); } else { - debug( + this.debug( '[handleConfirmPage] we remember that we saw this confirm page, maybe it needs to be reopened', confirmPage.targetContainer ); this.confirmPage[confirmPage.targetContainer] = confirmPage; delay(2000).then(() => { - debug('[handleConfirmPage] cleaning up', confirmPage); + this.debug('[handleConfirmPage] cleaning up', confirmPage); delete this.confirmPage[confirmPage.targetContainer]; }); } } } - async maybeReopenConfirmPage(macAssignment, request, tab, isolation = false) { + async maybeReopenConfirmPage( + macAssignment: MacAssignment, + request: browser.webRequest.WebRequestOnBeforeRequestDetails, + tab: Tab | undefined, + isolation = false + ): Promise { const deletesHistoryContainer = this.pref.deletesHistory.automaticMode === 'automatic'; - debug( + this.debug( '[maybeReopenConfirmPage]', macAssignment, request, @@ -78,40 +126,44 @@ class MultiAccountContainers { request.tabId && this.container.tabCreatedAsMacConfirmPage[request.tabId]) ) { - debug( + this.debug( '[maybeReopenConfirmPage] we reopened a confirmpage in that tab / for that request.tabId already', tab, request ); - return; + return false; } const targetContainer = `${this.background.containerPrefix}-container-${macAssignment.userContextId}`; if (this.confirmPage[targetContainer]) { - debug( + this.debug( '[maybeReopenConfirmPage] we saw a mac confirm page for the target container already', targetContainer, this.confirmPage[targetContainer] ); if (tab && tab.cookieStoreId && tab.cookieStoreId === targetContainer) { - debug( + this.debug( '[maybeReopenConfirmPage] tab is loading in target container, we do nothing' ); return false; } else { - return this._maybeReopenConfirmPage({ - targetContainer, - request, - tab, - deletesHistoryContainer, + return this._maybeReopenConfirmPage( + { + targetContainer, + request, + tab, + deletesHistoryContainer, + }, isolation, - }); + false + ); } } else { - debug( + this.debug( '[maybeReopenConfirmPage] we didnt saw a mac confirm page yet, waiting', targetContainer, tab ); + this.waitingForConfirmPage[targetContainer] = { targetContainer, request, @@ -119,7 +171,7 @@ class MultiAccountContainers { deletesHistoryContainer, }; delay(2000).then(() => { - debug('[maybeReopenConfirmPage] cleaning up', targetContainer); + this.debug('[maybeReopenConfirmPage] cleaning up', targetContainer); delete this.waitingForConfirmPage[targetContainer]; }); return false; @@ -127,10 +179,16 @@ class MultiAccountContainers { } async _maybeReopenConfirmPage( - { targetContainer, request, tab, deletesHistoryContainer, isolation }, - confirmPage - ) { - debug( + { + targetContainer, + request, + tab, + deletesHistoryContainer, + }: WaitingForConfirmPage, + isolation: boolean, + confirmPage: false | ConfirmPage + ): Promise { + this.debug( '[_maybeReopenConfirmPage]', targetContainer, request, @@ -144,13 +202,13 @@ class MultiAccountContainers { confirmPage = this.confirmPage[targetContainer]; } if (!confirmPage) { - debug('[_maybeReopenConfirmPage] something went wrong, aborting'); + this.debug('[_maybeReopenConfirmPage] something went wrong, aborting'); return false; } const currentContainer = confirmPage.currentContainer; if (currentContainer) { if (!isolation && this.container.isPermanent(currentContainer)) { - debug( + this.debug( '[_maybeReopenConfirmPage] currentContainer is permanent, we do nothing' ); return false; @@ -158,17 +216,17 @@ class MultiAccountContainers { this.storage.local.tempContainers[currentContainer] && this.storage.local.tempContainers[currentContainer].clean ) { - debug( + this.debug( '[_maybeReopenConfirmPage] the currentContainer mac confirm wants to open is a clean tmp container, we just cancel' ); return { clean: true }; } else { - debug( + this.debug( '[_maybeReopenConfirmPage] currentContainer not clean, reopen in new tmp container' ); } } else { - debug( + this.debug( '[_maybeReopenConfirmPage] no currentContainer, reopen in new tmp container' ); } @@ -183,7 +241,7 @@ class MultiAccountContainers { return true; } - async getAssignment(url) { + async getAssignment(url: string): Promise { const assignment = await browser.runtime.sendMessage( '@testpilot-containers', { @@ -203,5 +261,3 @@ class MultiAccountContainers { }; } } - -export default MultiAccountContainers; diff --git a/src/background/management.js b/src/background/management.js deleted file mode 100644 index b89e1755..00000000 --- a/src/background/management.js +++ /dev/null @@ -1,584 +0,0 @@ -const CONTAIN_URLS = { - facebook: { - // https://github.com/mozilla/contain-facebook/blob/master/src/background.js - urls: [ - 'facebook.com', - 'www.facebook.com', - 'facebook.net', - 'fb.com', - 'fbcdn.net', - 'fbcdn.com', - 'fbsbx.com', - 'tfbnw.net', - 'facebook-web-clients.appspot.com', - 'fbcdn-profile-a.akamaihd.net', - 'fbsbx.com.online-metrix.net', - 'connect.facebook.net.edgekey.net', - 'instagram.com', - 'cdninstagram.com', - 'instagramstatic-a.akamaihd.net', - 'instagramstatic-a.akamaihd.net.edgesuite.net', - 'messenger.com', - 'm.me', - 'messengerdevelopers.com', - 'atdmt.com', - 'onavo.com', - 'oculus.com', - 'oculusvr.com', - 'oculusbrand.com', - 'oculusforbusiness.com', - ], - REs: [], - }, - google: { - // https://github.com/containers-everywhere/contain-google/blob/master/background.js - urls: [ - 'google.com', - 'google.org', - 'googleapis.com', - 'g.co', - 'ggpht.com', - 'blogger.com', - 'googleblog.com', - 'blog.google', - 'googleusercontent.com', - 'googlesource.com', - 'google.org', - 'google.net', - '466453.com', - 'gooogle.com', - 'gogle.com', - 'ggoogle.com', - 'gogole.com', - 'goolge.com', - 'googel.com', - 'googlee.com', - 'googil.com', - 'googlr.com', - 'elgoog.im', - 'ai.google', - 'com.google', - 'google.ac', - 'google.ad', - 'google.ae', - 'google.com.af', - 'google.com.ag', - 'google.com.ai', - 'google.al', - 'google.am', - 'google.co.ao', - 'google.com.ar', - 'google.as', - 'google.at', - 'google.com.au', - 'google.az', - 'google.ba', - 'google.com.bd', - 'google.be', - 'google.bf', - 'google.bg', - 'google.com.bh', - 'google.bi', - 'google.bj', - 'google.com.bn', - 'google.com.bo', - 'google.com.br', - 'google.bs', - 'google.bt', - 'google.com.bw', - 'google.by', - 'google.com.bz', - 'google.ca', - 'google.com.kh', - 'google.cc', - 'google.cd', - 'google.cf', - 'google.cat', - 'google.cg', - 'google.ch', - 'google.ci', - 'google.co.ck', - 'google.cl', - 'google.cm', - 'google.cn', - 'google.com.co', - 'google.co.cr', - 'google.com.cu', - 'google.cv', - 'google.com.cy', - 'google.cz', - 'google.de', - 'google.dj', - 'google.dk', - 'google.dm', - 'google.com.do', - 'google.dz', - 'google.com.ec', - 'google.ee', - 'google.com.eg', - 'google.es', - 'google.com.et', - 'google.fi', - 'google.com.fj', - 'google.fm', - 'google.fr', - 'google.ga', - 'google.ge', - 'google.gf', - 'google.gg', - 'google.com.gh', - 'google.com.gi', - 'google.gl', - 'google.gm', - 'google.gp', - 'google.gr', - 'google.com.gt', - 'google.gy', - 'google.com.hk', - 'google.hn', - 'google.hr', - 'google.ht', - 'google.hu', - 'google.co.id', - 'google.iq', - 'google.ie', - 'google.co.il', - 'google.im', - 'google.co.in', - 'google.io', - 'google.is', - 'google.it', - 'google.je', - 'google.com.jm', - 'google.jo', - 'google.co.jp', - 'google.co.ke', - 'google.ki', - 'google.kg', - 'google.co.kr', - 'google.com.kw', - 'google.kz', - 'google.la', - 'google.lb', - 'google.com.lc', - 'google.li', - 'google.lk', - 'google.co.ls', - 'google.lt', - 'google.lu', - 'google.lv', - 'google.com.ly', - 'google.co.ma', - 'google.md', - 'google.me', - 'google.mg', - 'google.mk', - 'google.ml', - 'google.com.mm', - 'google.mn', - 'google.ms', - 'google.com.mt', - 'google.mu', - 'google.mv', - 'google.mw', - 'google.com.mx', - 'google.com.my', - 'google.co.mz', - 'google.com.na', - 'google.ne', - 'google.com.nf', - 'google.com.ng', - 'google.com.ni', - 'google.nl', - 'google.no', - 'google.com.np', - 'google.nr', - 'google.nu', - 'google.co.nz', - 'google.com.om', - 'google.com.pk', - 'google.com.pa', - 'google.com.pe', - 'google.com.ph', - 'google.pl', - 'google.com.pg', - 'google.pn', - 'google.com.pr', - 'google.ps', - 'google.pt', - 'google.com.py', - 'google.com.qa', - 'google.ro', - 'google.rs', - 'google.ru', - 'google.rw', - 'google.com.sa', - 'google.com.sb', - 'google.sc', - 'google.se', - 'google.com.sg', - 'google.sh', - 'google.si', - 'google.sk', - 'google.com.sl', - 'google.sn', - 'google.sm', - 'google.so', - 'google.st', - 'google.sr', - 'google.com.sv', - 'google.td', - 'google.tg', - 'google.co.th', - 'google.com.tj', - 'google.tk', - 'google.tl', - 'google.tm', - 'google.to', - 'google.tn', - 'google.com.tr', - 'google.tt', - 'google.com.tw', - 'google.co.tz', - 'google.com.ua', - 'google.co.ug', - 'google.co.uk', - 'google.us', - 'google.com.uy', - 'google.co.uz', - 'google.com.vc', - 'google.co.ve', - 'google.vg', - 'google.co.vi', - 'google.com.vn', - 'google.vu', - 'google.ws', - 'google.co.za', - 'google.co.zm', - 'google.co.zw', - 'like.com', - 'keyhole.com', - 'panoramio.com', - 'picasa.com', - 'urchin.com', - 'igoogle.com', - 'foofle.com', - 'froogle.com', - 'localguidesconnect.com', - 'googlemail.com', - 'googleanalytics.com', - 'google-analytics.com', - 'googletagmanager.com', - 'googlecode.com', - 'googlesource.com', - 'googledrive.com', - 'googlearth.com', - 'googleearth.com', - 'googlemaps.com', - 'googlepagecreator.com', - 'googlescholar.com', - 'advertisercommunity.com', - 'thinkwithgoogle.com', - 'youtube.com', - 'youtu.be', - 'yt.be', - 'ytimg.com', - ' youtube-nocookie.com', - 'youtubegaming.com', - 'youtubeeducation.com', - 'youtube-nocookie.com', - 'blogspot.com', - 'blogspot.ae', - 'blogspot.al', - 'blogspot.am', - 'blogspot.com.ar', - 'blogspot.co.at', - 'blogspot.com.au', - 'blogspot.ba', - 'blogspot.be', - 'blogspot.bg', - 'blogspot.bj', - 'blogspot.com.br', - 'blogspot.com.by', - 'blogspot.ca', - 'blogspot.cf', - 'blogspot.ch', - 'blogspot.cl', - 'blogspot.com.co', - 'blogspot.cv', - 'blogspot.com.cy', - 'blogspot.cz', - 'blogspot.de', - 'blogspot.dj', - 'blogspot.dk', - 'blogspot.dm', - 'blogspot.com.do', - 'blogspot.dz', - 'blogspot.com.eg', - 'blogspot.es', - 'blogspot.fi', - 'blogspot.fr', - 'blogspot.gr', - 'blogspot.hr', - 'blogspot.hu', - 'blogspot.co.id', - 'blogspot.ie', - 'blogspot.co.il', - 'blogspot.in', - 'blogspot.is', - 'blogspot.it', - 'blogspot.jp', - 'blogspot.co.ke', - 'blogspot.kr', - 'blogspot.li', - 'blogspot.lt', - 'blogspot.lu', - 'blogspot.md', - 'blogspot.mk', - 'blogspot.com.mt', - 'blogspot.mx', - 'blogspot.my', - 'blogspot.com.ng', - 'blogspot.nl', - 'blogspot.no', - 'blogspot.co.nz', - 'blogspot.pt', - 'blogspot.qa', - 'blogspot.ro', - 'blogspot.rs', - 'blogspot.ru', - 'blogspot.se', - 'blogspot.sg', - 'blogspot.si', - 'blogspot.sk', - 'blogspot.sn', - 'blogspot.com.sr', - 'blogspot.td', - 'blogspot.co.tl', - 'blogspot.co.to', - 'blogspot.com.tr', - 'blogspot.tw', - 'blogspot.co.uk', - 'blogspot.com.uy', - 'blogspot.co.za', - 'abc.xyz', - 'waze.com', - 'capitalg.com', - 'gv.com', - 'calicolabs.com', - 'x.company', - 'nest.com', - 'sidewalklabs.com', - 'verily.com', - 'doubleclickbygoogle.com', - 'feedburner.com', - 'doubleclick.com', - 'doubleclick.net', - 'adwords.com', - 'adsense.com', - 'admob.com', - 'advertisercommunity.com', - 'googlesyndication.com', - 'googlecommerce.com', - 'googlebot.com', - 'googleapps.com', - 'googleadservices.com', - 'gmodules.com', - 'googl.com', - '1e100.net', - 'domains.google', - 'gv.com', - 'madewithcode.com', - 'design.google', - 'gallery.io', - 'domains.google', - 'material.io', - 'android.com', - 'chromium.org', - 'cobrasearch.com', - 'chromecast.com', - 'chrome.com', - 'chromebook.com', - 'madewithcode.com', - 'whatbrowser.org', - 'withgoogle.com', - 'web.dev', - ], - REs: [], - }, - twitter: { - // https://github.com/v1shwa/contain-twitter/blob/master/background.js - urls: [ - 'twitter.com', - 'www.twitter.com', - 't.co', - 'twimg.com', - 'mobile.twitter.com', - 'm.twitter.com', - 'api.twitter.com', - 'abs.twimg.com', - 'ton.twimg.com', - 'pbs.twimg.com', - 'tweetdeck.twitter.com', - ], - REs: [], - }, - youtube: { - // https://github.com/AbdullahDiaa/contain-youtube/blob/master/src/background.js - urls: ['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be'], - REs: [], - }, - amazon: { - // https://github.com/Jackymancs4/contain-amazon/blob/master/background.js - urls: [ - 'amazon.it', - 'amazon.de', - 'amazon.com', - 'amazon.com.br', - 'amazon.in', - 'amazon.com.au', - 'amazon.es', - 'amazon.com.mx', - 'amazon.co.jp', - 'amazon.in', - 'amazon.co.uk', - 'amazon.ca', - 'amazon.fr', - 'amazon.com.sg', - 'awscloud.com', - 'amazon.company', - 'amazon.express', - 'amazon.gd', - 'amazon.international', - 'amazon.ltda', - 'amazon.press', - 'amazon.shopping', - 'amazon.tickets', - 'amazon.tv', - 'amazon.cruises', - 'amazon.dog', - 'amazon.express', - 'amazon.game', - 'amazon.gent', - 'amazon.salon', - 'amazon.shopping', - 'amazon.tours', - 'amazon.wiki', - 'amazon.clothing', - 'amazon.energy', - 'amazon.fund', - 'amazon.hockey', - 'amazon.kiwi', - 'amazon.re', - 'amazon.soccer', - 'amazon.tienda', - 'amazon.training', - 'amazon.jobs', - 'primevideo.com', - 'mturk.com', - 'lab126.com', - 'amazonpay.in', - 'amazonteam.org', - 'awsevents.com', - 'seattlespheres.com', - ], - REs: [], - }, -}; - -for (const containWhat of Object.keys(CONTAIN_URLS)) { - for (const url of CONTAIN_URLS[containWhat].urls) { - CONTAIN_URLS[containWhat].REs.push(new RegExp(`^(.*\\.)?${url}$`)); - } -} - -class Management { - constructor() { - this.addons = { - '@testpilot-containers': { - name: 'Firefox Multi-Account Containers', - enabled: false, - version: false, - }, - 'containerise@kinte.sh': { - name: 'Containerise', - enabled: false, - version: false, - }, - 'block_outside_container@jspenguin.org': { - name: 'Block sites outside container', - enabled: false, - version: false, - }, - 'treestyletab@piro.sakura.ne.jp': { - name: 'Tree Style Tab', - enabled: false, - version: false, - }, - '@contain-facebook': { - name: 'Facebook Container', - enabled: false, - version: false, - REs: CONTAIN_URLS.facebook.REs, - }, - '@contain-google': { - name: 'Google Container', - enabled: false, - version: false, - REs: CONTAIN_URLS.google.REs, - }, - '@contain-twitter': { - name: 'Twitter Container', - enabled: false, - version: false, - REs: CONTAIN_URLS.twitter.REs, - }, - '@contain-youtube': { - name: 'YouTube Container', - enabled: false, - version: false, - REs: CONTAIN_URLS.youtube.REs, - }, - '@contain-amazon': { - name: 'Amazon Container', - enabled: false, - version: false, - REs: CONTAIN_URLS.amazon.REs, - }, - }; - } - - async initialize() { - try { - const extensions = await browser.management.getAll(); - extensions.map(extension => { - if (!this.addons[extension.id]) { - return; - } - this.addons[extension.id].enabled = extension.enabled; - this.addons[extension.id].version = extension.version; - }); - } catch (error) { - debug('[management:initialize] couldnt getAll extensions', error); - return; - } - } - - disable(extension) { - if (!this.addons[extension.id]) { - return; - } - this.addons[extension.id].enabled = false; - this.addons[extension.id].version = extension.version; - } - - enable(extension) { - if (!this.addons[extension.id]) { - return; - } - this.addons[extension.id].enabled = true; - this.addons[extension.id].version = extension.version; - } -} - -export default Management; diff --git a/src/background/management.ts b/src/background/management.ts new file mode 100644 index 00000000..7d62d0c7 --- /dev/null +++ b/src/background/management.ts @@ -0,0 +1,44 @@ +import { addons } from './external-addons'; +import { TemporaryContainers } from './tmp'; +import { Debug } from '~/types'; + +export class Management { + public addons = addons; + private debug: Debug; + + constructor(background: TemporaryContainers) { + this.debug = background.debug; + } + + async initialize(): Promise { + try { + const extensions = await browser.management.getAll(); + extensions.map(extension => { + const addon = this.addons.get(extension.id); + if (addon) { + addon.enabled = extension.enabled; + addon.version = extension.version; + } + }); + } catch (error) { + this.debug('[management:initialize] couldnt getAll extensions', error); + return; + } + } + + disable(extension: browser.management.ExtensionInfo): void { + const addon = this.addons.get(extension.id); + if (addon) { + addon.enabled = false; + addon.version = extension.version; + } + } + + enable(extension: browser.management.ExtensionInfo): void { + const addon = this.addons.get(extension.id); + if (addon && extension.enabled) { + addon.enabled = true; + addon.version = extension.version; + } + } +} diff --git a/src/background/migration-legacy.js b/src/background/migration-legacy.js deleted file mode 100644 index 8bac3f33..00000000 --- a/src/background/migration-legacy.js +++ /dev/null @@ -1,53 +0,0 @@ -// this is only needed once for upgrades from <1.0 and should be removed in the next major version -// we now store the addon version in storage instead of waiting for onInstalled - -const migrationReadyAbortController = new AbortController(); -let migrationReady; -const migrationReadyPromise = new Promise((resolve, reject) => { - migrationReady = resolve; - - migrationReadyAbortController.signal.addEventListener('abort', () => { - reject('[migration-legacy] waiting for migration ready timed out'); - }); -}).catch(debug); -const migrationReadyTimeout = window.setTimeout(() => { - migrationReadyAbortController.abort(); -}, 10000); - -const migrationOnInstalledListener = async function() { - browser.runtime.onInstalled.removeListener(migrationOnInstalledListener); - const { version } = await browser.storage.local.get('version'); - if (version) { - clearTimeout(migrationReadyTimeout); - debug('[migration-legacy] version found, skip', version); - return; - } - - await migrationReadyPromise; - return window.tmp.migration.onInstalled.call( - window.tmp.migration, - ...arguments - ); -}; -browser.runtime.onInstalled.addListener(migrationOnInstalledListener); - -window.migrationLegacy = async migration => { - try { - debug( - '[migration-legacy] no previousVersion found, waiting for onInstalled' - ); - const updateDetails = await new Promise((resolve, reject) => { - migration.onInstalled = resolve; - window.setTimeout(() => { - // onInstalled didnt fire, again. - reject(); - }, 10000); - debug('[migration-legacy] ready'); - migrationReady(); - }); - migration.previousVersion = updateDetails.previousVersion; - } catch (error) { - debug('[migration-legacy] waiting for onInstalled failed, assuming 0.103'); - migration.previousVersion = '0.103'; - } -}; diff --git a/src/background/migration-legacy.ts b/src/background/migration-legacy.ts new file mode 100644 index 00000000..af3845dc --- /dev/null +++ b/src/background/migration-legacy.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// this is only needed once for upgrades from <1.0 and should be removed in the next major version +// we now store the addon version in storage instead of waiting for onInstalled +import { TemporaryContainers } from './tmp'; + +export class MigrationLegacy { + constructor(background: TemporaryContainers) { + const debug = background.debug; + + const migrationReadyAbortController = new AbortController(); + let migrationReady: () => void; + let migrationReadyTimeout: number; + const migrationReadyPromise = new Promise((resolve, reject) => { + migrationReady = resolve; + + migrationReadyAbortController.signal.addEventListener('abort', () => { + reject('[migration-legacy] waiting for migration ready timed out'); + }); + }).catch(debug); + + const migrationOnInstalledListener = async ( + ...args: any[] + ): Promise => { + browser.runtime.onInstalled.removeListener(migrationOnInstalledListener); + const { version } = await browser.storage.local.get('version'); + if (version) { + clearTimeout(migrationReadyTimeout); + debug('[migration-legacy] version found, skip', version); + return; + } + + await migrationReadyPromise; + return background.migration.onInstalled.call( + background.migration, + ...args + ); + }; + browser.runtime.onInstalled.addListener(migrationOnInstalledListener); + + window.migrationLegacy = async (migration: any): Promise => { + try { + debug( + '[migration-legacy] no previousVersion found, waiting for onInstalled' + ); + const updateDetails: any = await new Promise((resolve, reject) => { + migration.onInstalled = resolve; + window.setTimeout(() => { + migrationReadyAbortController.abort(); + reject(); + }, 10000); + debug('[migration-legacy] ready'); + migrationReady(); + }); + migration.previousVersion = updateDetails.previousVersion; + } catch (error) { + debug( + '[migration-legacy] waiting for onInstalled failed, assuming 0.103' + ); + migration.previousVersion = '0.103'; + } + }; + } +} diff --git a/src/background/migration.js b/src/background/migration.ts similarity index 72% rename from src/background/migration.js rename to src/background/migration.ts index a4d29d75..08e1fbbd 100644 --- a/src/background/migration.js +++ b/src/background/migration.ts @@ -1,10 +1,34 @@ -/* istanbul ignore next */ -class Migration { - constructor(background) { +/* istanbul ignore */ +import { TemporaryContainers } from './tmp'; +import { Storage } from './storage'; +import { Utils } from './utils'; +import { IsolationDomain, Debug } from '~/types'; + +export class Migration { + private background: TemporaryContainers; + private debug: Debug; + private storage!: Storage; + private utils!: Utils; + private previousVersion!: string; + private previousVersionBeta!: boolean; + + // migration-legacy + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onInstalled: (...args: any) => void = () => {}; + + constructor(background: TemporaryContainers) { this.background = background; + this.debug = background.debug; } - async migrate({ preferences, previousVersion }) { + async migrate({ + preferences, + previousVersion, + }: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + preferences: any; + previousVersion: string; + }): Promise { this.storage = this.background.storage; this.utils = this.background.utils; this.previousVersion = previousVersion; @@ -13,7 +37,7 @@ class Migration { await window.migrationLegacy(this); } - debug('[migrate] previousVersion', this.previousVersion); + this.debug('[migrate] previousVersion', this.previousVersion); this.previousVersionBeta = false; if (this.previousVersion.includes('beta')) { this.previousVersionBeta = true; @@ -21,7 +45,7 @@ class Migration { } if (this.updatedFromVersionEqualToOrLessThan('0.91')) { - debug( + this.debug( 'updated from version <= 0.91, migrate container numbers into dedicated array' ); Object.values(this.storage.local.tempContainers).map(container => { @@ -30,7 +54,7 @@ class Migration { } if (this.updatedFromVersionEqualToOrLessThan('0.103', '1.0.1')) { - debug( + this.debug( 'updated from version <= 0.103, migrate deletesHistory.active and ignoreRequestsTo' ); if (this.background.permissions.history) { @@ -39,12 +63,12 @@ class Migration { if (preferences.ignoreRequestsToAMO === false) { preferences.ignoreRequests = preferences.ignoreRequests.filter( - ignoredPattern => ignoredPattern !== 'addons.mozilla.org' + (ignoredPattern: string) => ignoredPattern !== 'addons.mozilla.org' ); } if (preferences.ignoreRequestsToPocket === false) { preferences.ignoreRequests = preferences.ignoreRequests.filter( - ignoredPattern => ignoredPattern !== 'getpocket.com' + (ignoredPattern: string) => ignoredPattern !== 'getpocket.com' ); } delete preferences.ignoreRequestsToAMO; @@ -52,10 +76,10 @@ class Migration { } if (this.updatedFromVersionEqualToOrLessThan('0.103', '1.0.6')) { - debug( + this.debug( 'updated from version <= 0.103, migrate per domain isolation to array' ); - const perDomainIsolation = []; + const perDomainIsolation: IsolationDomain[] = []; Object.keys(preferences.isolation.domain).map(domainPattern => { perDomainIsolation.push( Object.assign( @@ -70,7 +94,7 @@ class Migration { } if (this.updatedFromVersionEqualToOrLessThan('0.103')) { - debug( + this.debug( '[migrate] updated from version <= 0.103, migrate popup default tab to isolation-per-domain' ); if (preferences.browserActionPopup || preferences.pageAction) { @@ -79,14 +103,16 @@ class Migration { } if (this.updatedFromVersionEqualToOrLessThan('1.1')) { - debug( + this.debug( '[migrate] updated from version <= 1.1, migrate redirectorCloseTabs' ); preferences.closeRedirectorTabs.domains.push('slack-redir.net'); } if (this.updatedFromVersionEqualToOrLessThan('1.3', '1.4.1')) { - debug('[migrate] updated from version <= 1.3, migrate container.removal'); + this.debug( + '[migrate] updated from version <= 1.3, migrate container.removal' + ); switch (preferences.container.removal) { case 'instant': @@ -126,12 +152,13 @@ class Migration { await this.storage.persist(); } - updatedFromVersionEqualToOrLessThan(compareVersion, compareBetaVersion) { + updatedFromVersionEqualToOrLessThan( + compareVersion: string, + compareBetaVersion = '' + ): boolean { if (compareBetaVersion && this.previousVersionBeta) { compareVersion = compareBetaVersion; } return this.utils.versionCompare(compareVersion, this.previousVersion) >= 0; } } - -export default Migration; diff --git a/src/background/mouseclick.js b/src/background/mouseclick.ts similarity index 61% rename from src/background/mouseclick.js rename to src/background/mouseclick.ts index 0fc5dcc9..ed7c1072 100644 --- a/src/background/mouseclick.js +++ b/src/background/mouseclick.ts @@ -1,21 +1,49 @@ -class MouseClick { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { Isolation } from './isolation'; +import { delay } from './lib'; +import { Utils } from './utils'; +import { + PreferencesSchema, + IsolationAction, + Tab, + Debug, + ClickType, + ClickMessage, +} from '~/types'; + +export class MouseClick { + public isolated: { + [key: string]: { + clickType: ClickType; + tab: Tab; + count: number; + abortController: AbortController; + }; + } = {}; + + private background: TemporaryContainers; + private debug: Debug; + private pref!: PreferencesSchema; + private utils!: Utils; + private isolation!: Isolation; + + constructor(background: TemporaryContainers) { this.background = background; + this.debug = background.debug; this.isolated = {}; - - this.checkClickPreferences.bind(this); } - initialize() { + initialize(): void { this.pref = this.background.pref; - this.storage = this.background.storage; - this.container = this.background.container; this.utils = this.background.utils; this.isolation = this.background.isolation; } - linkClicked(message, sender) { - let clickType; + linkClicked( + message: ClickMessage, + sender: browser.runtime.MessageSender + ): void { + let clickType: ClickType | false = false; const url = message.href; if (message.event.button === 1) { clickType = 'middle'; @@ -31,12 +59,17 @@ class MouseClick { ) { clickType = 'ctrlleft'; } + + if (!clickType || !sender.tab) { + return; + } + if (!this.checkClick(clickType, message, sender)) { return; } if (this.isolated[url]) { - debug('[linkClicked] aborting isolated cleanup', url); + this.debug('[linkClicked] aborting isolated cleanup', url); this.isolated[url].abortController.abort(); } @@ -44,7 +77,7 @@ class MouseClick { if (!this.isolated[url]) { this.isolated[url] = { clickType, - tab: sender.tab, + tab: sender.tab as Tab, abortController, count: 0, }; @@ -53,15 +86,19 @@ class MouseClick { delay(1500, { signal: abortController.signal }) .then(() => { - debug('[linkClicked] cleaning up isolated', url); + this.debug('[linkClicked] cleaning up isolated', url); delete this.isolated[url]; }) - .catch(debug); + .catch(this.debug); } - checkClickPreferences(preferences, parsedClickedURL, parsedSenderTabURL) { + public checkClickPreferences = ( + preferences: { action: IsolationAction }, + parsedClickedURL: { hostname: string }, + parsedSenderTabURL: { hostname: string } + ): boolean => { if (preferences.action === 'always') { - debug( + this.debug( '[checkClick] click handled based on preference "always"', preferences ); @@ -69,7 +106,7 @@ class MouseClick { } if (preferences.action === 'never') { - debug( + this.debug( '[checkClickPreferences] click not handled based on preference "never"', preferences, parsedClickedURL, @@ -80,7 +117,7 @@ class MouseClick { if (preferences.action === 'notsamedomainexact') { if (parsedSenderTabURL.hostname !== parsedClickedURL.hostname) { - debug( + this.debug( '[checkClickPreferences] click handled based on preference "notsamedomainexact"', preferences, parsedClickedURL, @@ -88,7 +125,7 @@ class MouseClick { ); return true; } else { - debug( + this.debug( '[checkClickPreferences] click not handled based on preference "notsamedomainexact"', preferences, parsedClickedURL, @@ -105,14 +142,14 @@ class MouseClick { parsedClickedURL.hostname ) ) { - debug( + this.debug( '[checkClickPreferences] click not handled from preference "notsamedomain"', parsedClickedURL, parsedSenderTabURL ); return false; } else { - debug( + this.debug( '[checkClickPreferences] click handled from preference "notsamedomain"', parsedClickedURL, parsedSenderTabURL @@ -121,27 +158,32 @@ class MouseClick { } } - debug('[checkClickPreferences] this should never happen'); + this.debug('[checkClickPreferences] this should never happen'); return false; - } - - checkClick(type, message, sender) { - const parsedSenderTabURL = new URL(sender.tab.url); + }; + + checkClick( + type: ClickType, + message: ClickMessage, + sender: browser.runtime.MessageSender + ): boolean { + const tab = sender.tab as Tab; + const parsedSenderTabURL = new URL(tab.url); const parsedClickedURL = new URL(message.href); - debug('[checkClick] checking click', type, message, sender); + this.debug('[checkClick] checking click', type, message, sender); for (const domainPatternPreferences of this.pref.isolation.domain) { const domainPattern = domainPatternPreferences.pattern; - if (!this.isolation.matchDomainPattern(sender.tab.url, domainPattern)) { + if (!this.isolation.matchDomainPattern(tab.url, domainPattern)) { continue; } if (!domainPatternPreferences.mouseClick[type]) { continue; } const preferences = domainPatternPreferences.mouseClick[type]; - debug('[checkClick] per website pattern found'); + this.debug('[checkClick] per website pattern found'); if (preferences.action === 'global') { - debug('[checkClick] breaking because "global"'); + this.debug('[checkClick] breaking because "global"'); break; } return this.checkClickPreferences( @@ -150,7 +192,9 @@ class MouseClick { parsedSenderTabURL ); } - debug('[checkClick] no website pattern found, checking global preferences'); + this.debug( + '[checkClick] no website pattern found, checking global preferences' + ); return this.checkClickPreferences( this.pref.isolation.global.mouseClick[type], parsedClickedURL, @@ -158,29 +202,31 @@ class MouseClick { ); } - beforeHandleRequest(request) { + beforeHandleRequest( + request: browser.webRequest.WebRequestOnBeforeRequestDetails + ): void { if (!this.isolated[request.url]) { return; } - debug( + this.debug( '[beforeHandleRequest] aborting isolated mouseclick cleanup', request.url ); this.isolated[request.url].abortController.abort(); } - afterHandleRequest(request) { + afterHandleRequest( + request: browser.webRequest.WebRequestOnBeforeRequestDetails + ): void { if (!this.isolated[request.url]) { return; } this.isolated[request.url].abortController = new AbortController(); delay(1500, { signal: this.isolated[request.url].abortController.signal }) .then(() => { - debug('[beforeHandleRequest] cleaning up isolated', request.url); + this.debug('[beforeHandleRequest] cleaning up isolated', request.url); delete this.isolated[request.url]; }) - .catch(debug); + .catch(this.debug); } } - -export default MouseClick; diff --git a/src/background/pageaction.js b/src/background/pageaction.ts similarity index 72% rename from src/background/pageaction.js rename to src/background/pageaction.ts index 350d0aff..934b5b25 100644 --- a/src/background/pageaction.js +++ b/src/background/pageaction.ts @@ -1,19 +1,27 @@ -class PageAction { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { Storage } from './storage'; +import { PreferencesSchema, Tab } from '~/types'; + +export class PageAction { + private background: TemporaryContainers; + private pref!: PreferencesSchema; + private storage!: Storage; + + constructor(background: TemporaryContainers) { this.background = background; } - initialize() { + initialize(): void { this.pref = this.background.pref; this.storage = this.background.storage; } - async showOrHide(activatedTab) { + async showOrHide(activatedTab?: Tab): Promise { if (!activatedTab) { - const [activeTab] = await browser.tabs.query({ + const [activeTab] = (await browser.tabs.query({ currentWindow: true, active: true, - }); + })) as Tab[]; activatedTab = activeTab; } @@ -51,5 +59,3 @@ class PageAction { } } } - -export default PageAction; diff --git a/src/background/preferences.js b/src/background/preferences.js deleted file mode 100644 index e0f32f1d..00000000 --- a/src/background/preferences.js +++ /dev/null @@ -1,153 +0,0 @@ -class Preferences { - constructor(background) { - this.background = background; - - this.defaults = { - automaticMode: { - active: false, - newTab: 'created', - }, - notifications: false, - container: { - namePrefix: 'tmp', - color: 'toolbar', - colorRandom: false, - colorRandomExcluded: [], - icon: 'circle', - iconRandom: false, - iconRandomExcluded: [], - numberMode: 'keep', - removal: 900000, // ms - }, - iconColor: 'default', - isolation: { - active: true, - global: { - navigation: { - action: 'never', - }, - mouseClick: { - middle: { - action: 'never', - container: 'default', - }, - ctrlleft: { - action: 'never', - container: 'default', - }, - left: { - action: 'never', - container: 'default', - }, - }, - excluded: {}, - excludedContainers: {}, - }, - domain: [], - mac: { - action: 'disabled', - }, - }, - browserActionPopup: false, - pageAction: false, - contextMenu: true, - contextMenuBookmarks: false, - keyboardShortcuts: { - AltC: true, - AltP: true, - AltN: false, - AltShiftC: false, - AltX: false, - AltO: false, - AltI: false, - }, - replaceTabs: false, - closeRedirectorTabs: { - active: false, - delay: 2000, - domains: ['t.co', 'outgoing.prod.mozaws.net', 'slack-redir.net'], - }, - ignoreRequests: ['getpocket.com', 'addons.mozilla.org'], - cookies: { - domain: {}, - }, - deletesHistory: { - active: false, - automaticMode: 'never', - contextMenu: false, - contextMenuBookmarks: false, - containerAlwaysPerDomain: 'never', - containerIsolation: 'never', - containerRemoval: 0, // ms - containerMouseClicks: 'never', - statistics: false, - }, - statistics: false, - ui: { - expandPreferences: false, - popupDefaultTab: 'isolation-global', - }, - }; - } - - initialize() { - this.storage = this.background.storage; - this.permissions = this.background.permissions; - this.contextmenu = this.background.contextmenu; - this.browseraction = this.background.browseraction; - this.pageaction = this.background.pageaction; - } - - async handleChanges({ oldPreferences, newPreferences }) { - if (oldPreferences.iconColor !== newPreferences.iconColor) { - this.browseraction.setIcon(newPreferences.iconColor); - } - if ( - oldPreferences.browserActionPopup !== newPreferences.browserActionPopup - ) { - if (newPreferences.browserActionPopup) { - this.browseraction.setPopup(); - } else { - this.browseraction.unsetPopup(); - } - } - if (oldPreferences.pageAction !== newPreferences.pageAction) { - this.pageaction.showOrHide(); - } - if (oldPreferences.isolation.active !== newPreferences.isolation.active) { - this.pageaction.showOrHide(); - if (newPreferences.isolation.active) { - this.browseraction.removeIsolationInactiveBadge(); - } else { - this.browseraction.addIsolationInactiveBadge(); - } - } - if (newPreferences.notifications) { - this.permissions.notifications = true; - } - if (newPreferences.deletesHistory.active) { - this.permissions.history = true; - } - if ( - newPreferences.contextMenuBookmarks || - newPreferences.deletesHistory.contextMenuBookmarks - ) { - this.permissions.bookmarks = true; - } - - if ( - oldPreferences.contextMenu !== newPreferences.contextMenu || - oldPreferences.contextMenuBookmarks !== - newPreferences.contextMenuBookmarks || - oldPreferences.deletesHistory.contextMenu !== - newPreferences.deletesHistory.contextMenu || - oldPreferences.deletesHistory.contextMenuBookmarks !== - newPreferences.deletesHistory.contextMenuBookmarks - ) { - await this.contextmenu.remove(); - this.contextmenu.add(); - } - } -} - -export default Preferences; diff --git a/src/background/preferences.ts b/src/background/preferences.ts new file mode 100644 index 00000000..3240fa29 --- /dev/null +++ b/src/background/preferences.ts @@ -0,0 +1,169 @@ +import { TemporaryContainers } from './tmp'; +import { BrowserAction } from './browseraction'; +import { ContextMenu } from './contextmenu'; +import { PageAction } from './pageaction'; +import { PreferencesSchema, Permissions } from '~/types'; +import { REDIRECTOR_DOMAINS_DEFAULT, IGNORED_DOMAINS_DEFAULT } from '~/shared'; + +export class Preferences { + public defaults: PreferencesSchema = { + automaticMode: { + active: false, + newTab: 'created', + }, + notifications: false, + container: { + namePrefix: 'tmp', + color: 'toolbar', + colorRandom: false, + colorRandomExcluded: [], + icon: 'circle', + iconRandom: false, + iconRandomExcluded: [], + numberMode: 'keep', + removal: 900000, // ms + }, + iconColor: 'default', + isolation: { + active: true, + global: { + navigation: { + action: 'never', + }, + mouseClick: { + middle: { + action: 'never', + container: 'default', + }, + ctrlleft: { + action: 'never', + container: 'default', + }, + left: { + action: 'never', + container: 'default', + }, + }, + excluded: {}, + excludedContainers: {}, + }, + domain: [], + mac: { + action: 'disabled', + }, + }, + browserActionPopup: false, + pageAction: false, + contextMenu: true, + contextMenuBookmarks: false, + keyboardShortcuts: { + AltC: true, + AltP: true, + AltN: false, + AltShiftC: false, + AltX: false, + AltO: false, + AltI: false, + }, + replaceTabs: false, + closeRedirectorTabs: { + active: false, + delay: 2000, + domains: REDIRECTOR_DOMAINS_DEFAULT, + }, + ignoreRequests: IGNORED_DOMAINS_DEFAULT, + cookies: { + domain: {}, + }, + deletesHistory: { + active: false, + automaticMode: 'never', + contextMenu: false, + contextMenuBookmarks: false, + containerAlwaysPerDomain: 'never', + containerIsolation: 'never', + containerRemoval: 0, // ms + containerMouseClicks: 'never', + statistics: false, + }, + statistics: false, + ui: { + expandPreferences: false, + popupDefaultTab: 'isolation-global', + }, + }; + + private background: TemporaryContainers; + private permissions!: Permissions; + private contextmenu!: ContextMenu; + private browseraction!: BrowserAction; + private pageaction!: PageAction; + + constructor(background: TemporaryContainers) { + this.background = background; + } + + initialize(): void { + this.permissions = this.background.permissions; + this.contextmenu = this.background.contextmenu; + this.browseraction = this.background.browseraction; + this.pageaction = this.background.pageaction; + } + + async handleChanges({ + oldPreferences, + newPreferences, + }: { + oldPreferences: PreferencesSchema; + newPreferences: PreferencesSchema; + }): Promise { + if (oldPreferences.iconColor !== newPreferences.iconColor) { + this.browseraction.setIcon(newPreferences.iconColor); + } + if ( + oldPreferences.browserActionPopup !== newPreferences.browserActionPopup + ) { + if (newPreferences.browserActionPopup) { + this.browseraction.setPopup(); + } else { + this.browseraction.unsetPopup(); + } + } + if (oldPreferences.pageAction !== newPreferences.pageAction) { + this.pageaction.showOrHide(); + } + if (oldPreferences.isolation.active !== newPreferences.isolation.active) { + this.pageaction.showOrHide(); + if (newPreferences.isolation.active) { + this.browseraction.removeIsolationInactiveBadge(); + } else { + this.browseraction.addIsolationInactiveBadge(); + } + } + if (newPreferences.notifications) { + this.permissions.notifications = true; + } + if (newPreferences.deletesHistory.active) { + this.permissions.history = true; + } + if ( + newPreferences.contextMenuBookmarks || + newPreferences.deletesHistory.contextMenuBookmarks + ) { + this.permissions.bookmarks = true; + } + + if ( + oldPreferences.contextMenu !== newPreferences.contextMenu || + oldPreferences.contextMenuBookmarks !== + newPreferences.contextMenuBookmarks || + oldPreferences.deletesHistory.contextMenu !== + newPreferences.deletesHistory.contextMenu || + oldPreferences.deletesHistory.contextMenuBookmarks !== + newPreferences.deletesHistory.contextMenuBookmarks + ) { + await this.contextmenu.remove(); + this.contextmenu.add(); + } + } +} diff --git a/src/background/request.js b/src/background/request.ts similarity index 65% rename from src/background/request.js rename to src/background/request.ts index 0e48f671..7a893204 100644 --- a/src/background/request.js +++ b/src/background/request.ts @@ -1,32 +1,73 @@ -class Request { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { BrowserAction } from './browseraction'; +import { Container } from './container'; +import { History } from './history'; +import { Isolation } from './isolation'; +import { delay } from './lib'; +import { MultiAccountContainers } from './mac'; +import { Management } from './management'; +import { MouseClick } from './mouseclick'; +import { PreferencesSchema, Tab, Debug, OnBeforeRequestResult } from '~/types'; + +export class Request { + public lastSeenRequestUrl: { + [key: string]: string; + } = {}; + + private canceledTabs: { + [key: number]: { + requestIds: { + [key: string]: true; + }; + urls: { + [key: string]: true; + }; + }; + } = {}; + private canceledRequests: { + [key: string]: boolean; + } = {}; + private requestIdUrlSeen: { + [key: string]: boolean; + } = {}; + private cleanRequests: { + [key: string]: boolean; + } = {}; + + private background: TemporaryContainers; + private debug: Debug; + private pref!: PreferencesSchema; + private container!: Container; + private mouseclick!: MouseClick; + private browseraction!: BrowserAction; + private mac!: MultiAccountContainers; + private isolation!: Isolation; + private management!: Management; + private history!: History; + + constructor(background: TemporaryContainers) { this.background = background; - this.canceledTabs = {}; - this.canceledRequests = {}; - this.requestIdUrlSeen = {}; - this.cleanRequests = {}; - this.lastSeenRequestUrl = {}; + this.debug = background.debug; } - async initialize() { + initialize(): void { this.pref = this.background.pref; - this.storage = this.background.storage; this.container = this.background.container; this.mouseclick = this.background.mouseclick; this.browseraction = this.background.browseraction; this.mac = this.background.mac; - this.utils = this.background.utils; - this.tabs = this.background.tabs; this.isolation = this.background.isolation; this.management = this.background.management; this.history = this.background.history; } - async webRequestOnBeforeRequest(request) { - debug('[webRequestOnBeforeRequest] incoming request', request); + async webRequestOnBeforeRequest( + request: browser.webRequest.WebRequestOnBeforeRequestDetails + ): Promise { + this.debug('[webRequestOnBeforeRequest] incoming request', request); const requestIdUrl = `${request.requestId}+${request.url}`; if (requestIdUrl in this.requestIdUrlSeen) { - return; + return false; } else { this.requestIdUrlSeen[requestIdUrl] = true; delay(300000).then(() => { @@ -36,11 +77,11 @@ class Request { this.mouseclick.beforeHandleRequest(request); - let returnVal; + let returnVal: OnBeforeRequestResult; try { returnVal = await this.handleRequest(request); } catch (error) { - debug( + this.debug( '[webRequestOnBeforeRequest] handling request failed', error.toString() ); @@ -55,67 +96,73 @@ class Request { } this.lastSeenRequestUrl[request.requestId] = request.url; - if (returnVal && returnVal.cancel) { + if (typeof returnVal === 'object' && returnVal.cancel) { this.cancelRequest(request); return returnVal; - } else if (!returnVal || (returnVal && !returnVal.clean)) { + } else if ( + !returnVal || + (typeof returnVal === 'object' && !returnVal.clean) + ) { this.container.markUnclean(request.tabId); } // make sure we shouldnt cancel anyway if (this.shouldCancelRequest(request)) { - debug('[webRequestOnBeforeRequest] canceling', request); + this.debug('[webRequestOnBeforeRequest] canceling', request); this.cancelRequest(request); return { cancel: true }; } return; } - async handleRequest(request) { + async handleRequest( + request: browser.webRequest.WebRequestOnBeforeRequestDetails + ): Promise { if (request.tabId === -1) { - debug( + this.debug( '[handleRequest] onBeforeRequest request doesnt belong to a tab, why are you main_frame?', request ); - return; + return false; } this.browseraction.removeBadge(request.tabId); if (this.shouldCancelRequest(request)) { - debug('[handleRequest] canceling', request); + this.debug('[handleRequest] canceling', request); return { cancel: true }; } if (this.container.noContainerTabs[request.tabId]) { - debug('[handleRequest] no container tab, we ignore that', tab); - return; + this.debug('[handleRequest] no container tab, we ignore that', request); + return false; } - let tab, openerTab; + let tab; + let openerTab; try { - tab = await browser.tabs.get(request.tabId); + tab = (await browser.tabs.get(request.tabId)) as Tab; if (tab && tab.openerTabId) { - openerTab = await browser.tabs.get(tab.openerTabId); + openerTab = (await browser.tabs.get(tab.openerTabId)) as Tab; } - debug( + this.debug( '[handleRequest] onbeforeRequest requested tab information', tab, openerTab ); } catch (error) { - debug( + this.debug( '[handleRequest] onbeforeRequest retrieving tab information failed, mac was probably faster', error ); } let macAssignment; - if (this.management.addons['@testpilot-containers'].enabled) { + if (this.management.addons.get('@testpilot-containers')?.enabled) { try { macAssignment = await this.mac.getAssignment(request.url); } catch (error) { - debug( + this.debug( '[handleRequest] contacting mac failed, probably old version', error ); @@ -123,15 +170,15 @@ class Request { } if (macAssignment) { if (macAssignment.neverAsk) { - debug('[handleRequest] mac neverask assigned', macAssignment); - return; + this.debug('[handleRequest] mac neverask assigned', macAssignment); + return false; } else { - debug('[handleRequest] mac assigned', macAssignment); + this.debug('[handleRequest] mac assigned', macAssignment); } } if (await this.externalAddonHasPrecedence({ request, tab, openerTab })) { - return; + return false; } this.history.maybeAddHistory(tab, request.url); @@ -142,16 +189,16 @@ class Request { return this.isolation.matchDomainPattern(request.url, ignorePattern); }) ) { - debug( + this.debug( '[handleRequest] request url is on the ignoreRequests list', request ); - return; + return false; } if (tab && this.container.isClean(tab.cookieStoreId)) { // removing this clean check can result in endless loops - debug( + this.debug( '[handleRequest] not isolating because the tmp container is still clean' ); if (!this.cleanRequests[request.requestId]) { @@ -164,7 +211,7 @@ class Request { } if (this.cleanRequests[request.requestId]) { - debug( + this.debug( '[handleRequest] not isolating because of clean redirect requests', request ); @@ -178,7 +225,7 @@ class Request { macAssignment, }); if (isolated) { - debug( + this.debug( '[handleRequest] we decided to isolate and open new tmpcontainer', request ); @@ -186,7 +233,7 @@ class Request { } if (!this.pref.automaticMode.active || !tab) { - return; + return false; } if ( @@ -194,16 +241,16 @@ class Request { tab.cookieStoreId === `${this.background.containerPrefix}-default` && openerTab ) { - debug('[handleRequest] default container and openerTab', openerTab); + this.debug('[handleRequest] default container and openerTab', openerTab); if ( !openerTab.url.startsWith('about:') && !openerTab.url.startsWith('moz-extension:') ) { - debug( + this.debug( '[handleRequest] request didnt came from about/moz-extension page', openerTab ); - return; + return false; } } @@ -211,16 +258,16 @@ class Request { tab && tab.cookieStoreId !== `${this.background.containerPrefix}-default` ) { - debug( + this.debug( '[handleRequest] onBeforeRequest tab belongs to a non-default container', tab, request ); - return; + return false; } if (macAssignment) { - debug( + this.debug( '[handleRequest] decided to reopen but mac assigned, maybe reopen confirmpage', request, tab, @@ -229,12 +276,12 @@ class Request { return this.mac.maybeReopenConfirmPage(macAssignment, request, tab); } - debug('[handleRequest] decided to reload in temp tab', tab, request); + this.debug('[handleRequest] decided to reload in temp tab', tab, request); if (this.cancelRequest(request)) { return { cancel: true }; } - debug('[handleRequest] reload in temp tab', tab, request); + this.debug('[handleRequest] reload in temp tab', tab, request); await this.container.reloadTabInTempContainer({ tab, url: request.url, @@ -246,21 +293,23 @@ class Request { return { cancel: true }; } - cancelRequest(request) { + cancelRequest( + request: browser.webRequest.WebRequestOnBeforeRequestDetails + ): boolean { if ( !request || typeof request.requestId === 'undefined' || typeof request.tabId === 'undefined' ) { - debug('[cancelRequest] invalid request', request); - return; + this.debug('[cancelRequest] invalid request', request); + return false; } if (!this.canceledRequests[request.requestId]) { this.canceledRequests[request.requestId] = true; // requestIds are unique per session, so we have no pressure to remove them setTimeout(() => { - debug( + this.debug( '[webRequestOnBeforeRequest] cleaning up canceledRequests', request ); @@ -269,7 +318,7 @@ class Request { } if (!this.canceledTabs[request.tabId]) { - debug('[cancelRequest] marked request as canceled', request); + this.debug('[cancelRequest] marked request as canceled', request); // workaround until https://bugzilla.mozilla.org/show_bug.cgi?id=1437748 is resolved this.canceledTabs[request.tabId] = { requestIds: { @@ -281,17 +330,24 @@ class Request { }; // cleanup canceledTabs later setTimeout(() => { - debug('[webRequestOnBeforeRequest] cleaning up canceledTabs', request); + this.debug( + '[webRequestOnBeforeRequest] cleaning up canceledTabs', + request + ); delete this.canceledTabs[request.tabId]; }, 2000); return false; } else { - debug('[cancelRequest] already canceled', request, this.canceledTabs); + this.debug( + '[cancelRequest] already canceled', + request, + this.canceledTabs + ); let cancel = false; if (this.shouldCancelRequest(request)) { // same requestId or url from the same tab, this is a redirect that we have to cancel to prevent opening two tabs cancel = true; - debug('[cancelRequest] probably redirect, aborting', request); + this.debug('[cancelRequest] probably redirect, aborting', request); } // we decided to cancel the request at this point, register canceled request this.canceledTabs[request.tabId].requestIds[request.requestId] = true; @@ -300,14 +356,16 @@ class Request { } } - shouldCancelRequest(request) { + shouldCancelRequest( + request: browser.webRequest.WebRequestOnBeforeRequestDetails + ): boolean { if ( !request || typeof request.requestId === 'undefined' || typeof request.tabId === 'undefined' ) { - debug('[shouldCancelRequest] invalid request', request); - return; + this.debug('[shouldCancelRequest] invalid request', request); + return false; } if ( @@ -321,16 +379,26 @@ class Request { return false; } - cleanupCanceled(request) { + cleanupCanceled( + request: browser.webRequest.WebRequestOnBeforeRequestDetails + ): void { if (this.canceledTabs[request.tabId]) { delete this.canceledTabs[request.tabId]; } } - async externalAddonHasPrecedence({ request, tab, openerTab }) { + async externalAddonHasPrecedence({ + request, + tab, + openerTab, + }: { + request: browser.webRequest.WebRequestOnBeforeRequestDetails; + tab?: Tab; + openerTab?: Tab; + }): Promise { const parsedUrl = new URL(request.url); - if (this.management.addons['containerise@kinte.sh'].enabled) { + if (this.management.addons.get('containerise@kinte.sh')?.enabled) { try { const hostmap = await browser.runtime.sendMessage( 'containerise@kinte.sh', @@ -344,16 +412,16 @@ class Request { hostmap.cookieStoreId && hostmap.enabled ) { - debug( + this.debug( '[handleRequest] assigned with containerise we do nothing', hostmap ); return true; } else { - debug('[handleRequest] not assigned with containerise', hostmap); + this.debug('[handleRequest] not assigned with containerise', hostmap); } } catch (error) { - debug( + this.debug( '[handleRequest] contacting containerise failed, probably old version', error ); @@ -361,10 +429,11 @@ class Request { } if ( - this.management.addons['block_outside_container@jspenguin.org'].enabled + this.management.addons.get('block_outside_container@jspengun.org') + ?.enabled ) { try { - let response = await browser.runtime.sendMessage( + const response = await browser.runtime.sendMessage( 'block_outside_container@jspenguin.org', { action: 'rule_exists', @@ -372,15 +441,17 @@ class Request { } ); if (response.rule_exists) { - debug( + this.debug( '[handleRequest] assigned with block_outside_container we do nothing' ); return true; } else { - debug('[handleRequest] not assigned with block_outside_container'); + this.debug( + '[handleRequest] not assigned with block_outside_container' + ); } } catch (error) { - debug( + this.debug( '[handleRequest] contacting block_outside_container failed', error ); @@ -397,16 +468,17 @@ class Request { '@contain-youtube', '@contain-amazon', ]) { - if (!this.management.addons[containWhat].enabled) { + const addon = this.management.addons.get(containWhat); + if (!addon || !addon.enabled || !addon.REs) { continue; } - for (const RE of this.management.addons[containWhat].REs) { + for (const RE of addon.REs) { if ( RE.test(parsedUrl.hostname) || (parsedTabUrl && RE.test(parsedTabUrl.hostname)) || (parsedOpenerTabUrl && RE.test(parsedOpenerTabUrl.hostname)) ) { - debug( + this.debug( '[handleRequest] handled by active container addon, ignoring', containWhat, RE, @@ -416,7 +488,7 @@ class Request { } } } + + return false; } } - -export default Request; diff --git a/src/background/runtime.js b/src/background/runtime.ts similarity index 68% rename from src/background/runtime.js rename to src/background/runtime.ts index 0a2c5eb3..e9835f49 100644 --- a/src/background/runtime.js +++ b/src/background/runtime.ts @@ -1,10 +1,38 @@ -class Runtime { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { BrowserAction } from './browseraction'; +import { Cleanup } from './cleanup'; +import { Container } from './container'; +import { ContextMenu } from './contextmenu'; +import { Convert } from './convert'; +import { Migration } from './migration'; +import { MouseClick } from './mouseclick'; +import { Preferences } from './preferences'; +import { Storage } from './storage'; +import { Utils } from './utils'; +import { PreferencesSchema, Tab, Debug, RuntimeMessage } from '~/types'; + +export class Runtime { + private background: TemporaryContainers; + private debug: Debug; + private storage: Storage; + private pref!: PreferencesSchema; + private preferences!: Preferences; + private container!: Container; + private mouseclick!: MouseClick; + private browseraction!: BrowserAction; + private migration!: Migration; + private contextmenu!: ContextMenu; + private cleanup!: Cleanup; + private convert!: Convert; + private utils!: Utils; + + constructor(background: TemporaryContainers) { this.background = background; + this.debug = background.debug; this.storage = background.storage; } - initialize() { + initialize(): void { this.pref = this.background.pref; this.preferences = this.background.preferences; this.container = this.background.container; @@ -17,20 +45,23 @@ class Runtime { this.utils = this.background.utils; } - async onMessage(message, sender) { - debug('[onMessage] message received', message, sender); + async onMessage( + message: RuntimeMessage, + sender: browser.runtime.MessageSender + ): Promise { + this.debug('[onMessage] message received', message, sender); if (typeof message !== 'object') { return; } switch (message.method) { case 'linkClicked': - debug('[onMessage] link clicked'); + this.debug('[onMessage] link clicked'); this.mouseclick.linkClicked(message.payload, sender); break; case 'savePreferences': - debug('[onMessage] saving preferences'); + this.debug('[onMessage] saving preferences'); await this.preferences.handleChanges({ oldPreferences: this.pref, newPreferences: message.payload.preferences, @@ -74,7 +105,7 @@ class Runtime { } case 'resetStatistics': - debug('[onMessage] resetting statistics'); + this.debug('[onMessage] resetting statistics'); this.storage.local.statistics = this.utils.clone( this.storage.defaults.statistics ); @@ -83,7 +114,7 @@ class Runtime { break; case 'resetStorage': - debug('[onMessage] resetting storage', message, sender); + this.debug('[onMessage] resetting storage', message, sender); this.browseraction.unsetPopup(); this.contextmenu.remove(); this.browseraction.setIcon('default'); @@ -91,7 +122,7 @@ class Runtime { return this.storage.install(); case 'resetContainerNumber': - debug('[onMessage] resetting container number', message, sender); + this.debug('[onMessage] resetting container number', message, sender); this.storage.local.tempContainerCounter = 0; await this.storage.persist(); break; @@ -109,21 +140,18 @@ class Runtime { cookieStoreId: message.payload.cookieStoreId, tabId: message.payload.tabId, name: message.payload.name, - url: message.payload.url, }); case 'convertTempContainerToRegular': return this.convert.convertTempContainerToRegular({ cookieStoreId: message.payload.cookieStoreId, tabId: message.payload.tabId, - url: message.payload.url, }); case 'convertPermanentToTempContainer': return this.convert.convertPermanentToTempContainer({ cookieStoreId: message.payload.cookieStoreId, tabId: message.payload.tabId, - url: message.payload.url, }); case 'lastFileExport': @@ -135,12 +163,20 @@ class Runtime { } } - async onMessageExternal(message, sender) { - debug('[onMessageExternal] got external message', message, sender); + async onMessageExternal( + message: { + method: string; + url?: string; + active?: boolean; + cookieStoreId?: string; + }, + sender: browser.runtime.MessageSender + ): Promise { + this.debug('[onMessageExternal] got external message', message, sender); switch (message.method) { case 'createTabInTempContainer': return this.container.createTabInTempContainer({ - url: message.url || null, + url: message.url || undefined, active: message.active, deletesHistory: this.pref.deletesHistory.automaticMode === 'automatic' @@ -148,7 +184,8 @@ class Runtime { : false, }); case 'isTempContainer': - return this.storage.local.tempContainers[message.cookieStoreId] + return message.cookieStoreId && + this.storage.local.tempContainers[message.cookieStoreId] ? true : false; default: @@ -156,7 +193,7 @@ class Runtime { } } - async onStartup() { + async onStartup(): Promise { this.cleanup.cleanup(true); if (this.pref.container.numberMode === 'keepuntilrestart') { @@ -165,5 +202,3 @@ class Runtime { } } } - -export default Runtime; diff --git a/src/background/statistics.js b/src/background/statistics.ts similarity index 64% rename from src/background/statistics.js rename to src/background/statistics.ts index 4322e04f..d831bf01 100644 --- a/src/background/statistics.js +++ b/src/background/statistics.ts @@ -1,22 +1,41 @@ -class Statistics { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { Cleanup } from './cleanup'; +import { Container } from './container'; +import { Storage } from './storage'; +import { formatBytes } from '../shared'; +import { PreferencesSchema, Tab, CookieStoreId, Debug } from '~/types'; + +export class Statistics { + private removedContainerCount = 0; + private removedContainerCookiesCount = 0; + private removedContainerHistoryCount = 0; + private removedContentLength = 0; + private requests: { + [key: string]: { contentLength: number }; + } = {}; + + private background: TemporaryContainers; + private debug: Debug; + private pref!: PreferencesSchema; + private storage!: Storage; + private container!: Container; + private cleanup!: Cleanup; + + constructor(background: TemporaryContainers) { this.background = background; - - this.removedContainerCount = 0; - this.removedContainerCookiesCount = 0; - this.removedContainerHistoryCount = 0; - this.removedContentLength = 0; - this.requests = {}; + this.debug = background.debug; } - initialize() { + initialize(): void { this.pref = this.background.pref; this.storage = this.background.storage; this.container = this.background.container; this.cleanup = this.background.cleanup; } - async collect(request) { + async collect( + request: browser.webRequest.WebRequestOnCompletedDetails + ): Promise { if (!this.pref.statistics && !this.pref.deletesHistory.statistics) { return; } @@ -27,7 +46,7 @@ class Statistics { let tab; try { - tab = await browser.tabs.get(request.tabId); + tab = (await browser.tabs.get(request.tabId)) as Tab; } catch (error) { return; } @@ -41,21 +60,33 @@ class Statistics { contentLength: 0, }; } - if (!request.fromCache) { + if (!request.fromCache && request.responseHeaders) { const contentLength = request.responseHeaders.find( header => header.name === 'content-length' ); - if (contentLength) { + if (contentLength && contentLength.value) { this.requests[tab.cookieStoreId].contentLength += parseInt( - contentLength.value + contentLength.value, + 10 ); } } } - update(historyClearedCount, cookieCount, cookieStoreId) { + async update( + historyClearedCount: number, + cookieStoreId: CookieStoreId + ): Promise { this.removedContainerCount++; + let cookieCount = 0; + try { + const cookies = await browser.cookies.getAll({ storeId: cookieStoreId }); + cookieCount = cookies.length; + } catch (error) { + this.debug('[tryToRemove] couldnt get cookies', cookieStoreId, error); + } + if (historyClearedCount) { this.removedContainerHistoryCount += historyClearedCount; } @@ -97,14 +128,14 @@ class Statistics { delete this.requests[cookieStoreId]; } - finish() { + finish(): void { if (this.removedContainerCount) { let notificationMessage = `Deleted Temporary Containers: ${this.removedContainerCount}`; if (this.removedContainerCookiesCount) { notificationMessage += `\nand ${this.removedContainerCookiesCount} Cookies`; } if (this.removedContentLength) { - notificationMessage += `\nand ~${this.formatBytes( + notificationMessage += `\nand ~${formatBytes( this.removedContentLength )} Cache`; } @@ -119,16 +150,4 @@ class Statistics { this.removedContainerHistoryCount = 0; this.removedContentLength = 0; } - - formatBytes(bytes, decimals) { - // https://stackoverflow.com/a/18650828 - if (bytes == 0) return '0 Bytes'; - const k = 1024, - dm = decimals || 2, - sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], - i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; - } } - -export default Statistics; diff --git a/src/background/storage.js b/src/background/storage.ts similarity index 65% rename from src/background/storage.js rename to src/background/storage.ts index 990653e1..718b01dc 100644 --- a/src/background/storage.js +++ b/src/background/storage.ts @@ -1,8 +1,18 @@ -class Storage { - constructor(background) { +import { TemporaryContainers } from './tmp'; +import { StorageLocal, Debug } from '~/types'; + +export class Storage { + public local!: StorageLocal; + public installed: boolean; + public defaults: StorageLocal; + + private background: TemporaryContainers; + private debug: Debug; + + constructor(background: TemporaryContainers) { this.background = background; + this.debug = background.debug; this.installed = false; - this.local = null; this.defaults = { containerPrefix: false, @@ -26,8 +36,8 @@ class Storage { }; } - async initialize() { - this.local = await browser.storage.local.get(); + async initialize(): Promise { + this.local = (await browser.storage.local.get()) as StorageLocal; // empty storage *should* mean new install if (!this.local || !Object.keys(this.local).length) { @@ -43,10 +53,13 @@ class Storage { await this.persist(); } } catch (error) { - debug('[initialize] accessing managed storage failed:', error.toString()); + this.debug( + '[initialize] accessing managed storage failed:', + error.toString() + ); } - debug('[initialize] storage initialized', this.local); + this.debug('[initialize] storage initialized', this.local); if ( this.background.utils.addMissingKeys({ defaults: this.defaults, @@ -57,31 +70,31 @@ class Storage { } // migrate if currently running version is different from version in storage - if (this.background.version !== this.local.version) { + if (this.local.version && this.background.version !== this.local.version) { try { await this.background.migration.migrate({ preferences: this.local.preferences, previousVersion: this.local.version, }); } catch (error) { - debug('[initialize] migration failed', error.toString()); + this.debug('[initialize] migration failed', error.toString()); } } return true; } - async persist() { + async persist(): Promise { try { if (!this.local || !Object.keys(this.local).length) { - debug('[persist] tried to persist corrupt storage', this.local); + this.debug('[persist] tried to persist corrupt storage', this.local); return false; } await browser.storage.local.set(this.local); - debug('[persist] storage persisted'); + this.debug('[persist] storage persisted'); return true; } catch (error) { - debug( + this.debug( '[persist] something went wrong while trying to persist the storage', error ); @@ -89,23 +102,17 @@ class Storage { } } - async install() { - debug('[install] installing storage'); + async install(): Promise { + this.debug('[install] installing storage'); this.local = this.background.utils.clone(this.defaults); this.local.version = this.background.version; - if (this.background.browserVersion < 67) { - this.local.preferences.container.color = 'red'; - } - if (!(await this.persist())) { throw new Error('[install] something went wrong while installing'); } - debug('[install] storage installed', this.local); + this.debug('[install] storage installed', this.local); this.installed = true; return true; } } - -export default Storage; diff --git a/src/background/tabs.js b/src/background/tabs.ts similarity index 64% rename from src/background/tabs.js rename to src/background/tabs.ts index e9f86f71..e0eab559 100644 --- a/src/background/tabs.js +++ b/src/background/tabs.ts @@ -1,29 +1,48 @@ -class Tabs { - constructor(background) { - this.background = background; +import { TemporaryContainers } from './tmp'; +import { BrowserAction } from './browseraction'; +import { Cleanup } from './cleanup'; +import { Container } from './container'; +import { History } from './history'; +import { delay } from './lib'; +import { MultiAccountContainers } from './mac'; +import { PageAction } from './pageaction'; +import { PreferencesSchema, Tab, Debug } from '~/types'; - this.containerMap = new Map(); - this.creatingInSameContainer = false; +export class Tabs { + public creatingInSameContainer = false; + public containerMap = new Map(); + + private background: TemporaryContainers; + private debug: Debug; + private pref!: PreferencesSchema; + private container!: Container; + private browseraction!: BrowserAction; + private pageaction!: PageAction; + private mac!: MultiAccountContainers; + private history!: History; + private cleanup!: Cleanup; + private tabs!: Tabs; + + constructor(background: TemporaryContainers) { + this.background = background; + this.debug = background.debug; } - initialize() { + initialize(): void { this.pref = this.background.pref; - this.storage = this.background.storage; this.container = this.background.container; this.browseraction = this.background.browseraction; this.pageaction = this.background.pageaction; - this.contextmenu = this.background.contextmenu; this.mac = this.background.mac; this.history = this.background.history; this.cleanup = this.background.cleanup; - - return this.handleAlreadyOpen(); + this.tabs = this.background.tabs; } // onUpdated sometimes (often) fires before onCreated // https://bugzilla.mozilla.org/show_bug.cgi?id=1586612 - async onCreated(tab) { - debug('[onCreated] tab created', tab); + async onCreated(tab: Tab): Promise { + this.debug('[onCreated] tab created', tab); this.containerMap.set(tab.id, tab.cookieStoreId); const reopened = await this.maybeReopenInTmpContainer(tab); if (!reopened) { @@ -31,12 +50,16 @@ class Tabs { } } - async onUpdated(tabId, changeInfo, tab) { - debug('[onUpdated] tab updated', tab, changeInfo); - this.maybeCloseRedirectorTab(tabId, tab, changeInfo); + async onUpdated( + tabId: number, + changeInfo: browser.tabs.TabsOnUpdatedEventChangeInfo, + tab: Tab + ): Promise { + this.debug('[onUpdated] tab updated', tab, changeInfo); + this.maybeCloseRedirectorTab(tab, changeInfo); if (changeInfo.url) { - debug('[onUpdated] url changed', changeInfo); + this.debug('[onUpdated] url changed', changeInfo); this.history.maybeAddHistory(tab, changeInfo.url); const reopened = await this.maybeReopenInTmpContainer(tab); if (!reopened) { @@ -45,19 +68,19 @@ class Tabs { } } - async onRemoved(tabId) { - debug('[onRemoved]', tabId); + async onRemoved(tabId: number): Promise { + this.debug('[onRemoved]', tabId); if (this.container.noContainerTabs[tabId]) { delete this.container.noContainerTabs[tabId]; } if (this.container.tabCreatedAsMacConfirmPage[tabId]) { - delete this.tabCreatedAsMacConfirmPage[tabId]; + delete this.container.tabCreatedAsMacConfirmPage[tabId]; } const cookieStoreId = this.containerMap.get(tabId); if (cookieStoreId && this.container.isTemporary(cookieStoreId)) { - debug( + this.debug( '[onRemoved] queuing container removal because of tab removal', tabId ); @@ -67,23 +90,25 @@ class Tabs { this.containerMap.delete(tabId); } - async onActivated(activeInfo) { - debug('[onActivated]', activeInfo); - this.container.lastCreatedInactiveTab[ + async onActivated( + activeInfo: browser.tabs.onActivatedActiveInfo + ): Promise { + this.debug('[onActivated]', activeInfo); + delete this.container.lastCreatedInactiveTab[ browser.windows.WINDOW_ID_CURRENT - ] = false; - const activatedTab = await browser.tabs.get(activeInfo.tabId); + ]; + const activatedTab = (await browser.tabs.get(activeInfo.tabId)) as Tab; this.pageaction.showOrHide(activatedTab); } - async handleAlreadyOpen() { - const tabs = await browser.tabs.query({}); + async handleAlreadyOpen(): Promise<(void | boolean)[]> { + const tabs = (await browser.tabs.query({})) as Tab[]; return Promise.all(tabs.map(tab => this.maybeReopenInTmpContainer(tab))); } - async maybeReopenInTmpContainer(tab) { + async maybeReopenInTmpContainer(tab: Tab): Promise { if (this.creatingInSameContainer) { - debug( + this.debug( '[maybeReopenInTmpContainer] we are in the process of creating a tab in same container, ignore', tab ); @@ -91,18 +116,18 @@ class Tabs { } if (this.container.noContainerTabs[tab.id]) { - debug('[maybeReopenInTmpContainer] nocontainer tab, ignore', tab); + this.debug('[maybeReopenInTmpContainer] nocontainer tab, ignore', tab); return; } if (tab.url.startsWith('moz-extension://')) { - debug('[maybeReopenInTmpContainer] moz-extension url', tab); + this.debug('[maybeReopenInTmpContainer] moz-extension url', tab); await this.mac.handleConfirmPage(tab); return; } if (!this.pref.automaticMode.active) { - debug( + this.debug( '[maybeReopenInTmpContainer] automatic mode not active, we ignore that', tab ); @@ -110,7 +135,7 @@ class Tabs { } if (tab.url !== 'about:home' && tab.url !== 'about:newtab') { - debug( + this.debug( '[maybeReopenInTmpContainer] not a home/new tab, we dont handle that', tab ); @@ -122,7 +147,7 @@ class Tabs { if (tab.cookieStoreId === `${this.background.containerPrefix}-default`) { if (this.pref.automaticMode.newTab === 'navigation' && !deletesHistory) { - debug( + this.debug( '[maybeReopenInTmpContainer] automatic mode on navigation, setting icon badge', tab ); @@ -131,7 +156,7 @@ class Tabs { } if (this.pref.automaticMode.newTab === 'created' || deletesHistory) { - debug( + this.debug( '[maybeReopenInTmpContainer] about:home/new tab in firefox-default container, reload in temp container', tab ); @@ -148,7 +173,7 @@ class Tabs { this.container.isTemporary(tab.cookieStoreId) && this.pref.automaticMode.newTab === 'navigation' ) { - debug( + this.debug( '[maybeReopenInTmpContainer] about:home and automatic mode on navigation but already in tmp container, open in default container', tab ); @@ -161,7 +186,10 @@ class Tabs { } } - maybeCloseRedirectorTab(tabId, tab, changeInfo) { + maybeCloseRedirectorTab( + tab: Tab, + changeInfo: browser.tabs.TabsOnUpdatedEventChangeInfo + ): void { if ( this.pref.closeRedirectorTabs.active && changeInfo.status && @@ -170,22 +198,14 @@ class Tabs { const url = new URL(tab.url); if (this.pref.closeRedirectorTabs.domains.includes(url.hostname)) { delay(this.pref.closeRedirectorTabs.delay).then(async () => { - try { - const tab = await browser.tabs.get(tabId); - const url = new URL(tab.url); - if (this.pref.closeRedirectorTabs.domains.includes(url.hostname)) { - debug('[onUpdated] removing redirector tab', changeInfo, tab); - browser.tabs.remove(tabId); - } - } catch (error) { - debug('[onUpdate] error while requesting tab info', error); - } + this.debug('[onUpdated] removing redirector tab', changeInfo, tab); + this.tabs.remove(tab); }); } } } - async maybeMoveTab(tab) { + async maybeMoveTab(tab: Tab): Promise { if ( !tab.active && this.container.lastCreatedInactiveTab[ @@ -202,19 +222,19 @@ class Tabs { ] ); if (lastCreatedInactiveTab.index > tab.index) { - debug('[onCreated] moving tab', lastCreatedInactiveTab, tab); + this.debug('[onCreated] moving tab', lastCreatedInactiveTab, tab); browser.tabs.move(tab.id, { index: lastCreatedInactiveTab.index }); this.container.lastCreatedInactiveTab[ browser.windows.WINDOW_ID_CURRENT ] = tab.id; } } catch (error) { - debug('[onCreated] getting lastCreatedInactiveTab failed', error); + this.debug('[onCreated] getting lastCreatedInactiveTab failed', error); } } } - async createInSameContainer() { + async createInSameContainer(): Promise { this.creatingInSameContainer = true; try { const tabs = await browser.tabs.query({ @@ -223,7 +243,10 @@ class Tabs { }); const activeTab = tabs[0]; if (!activeTab) { - debug('[createInSameContainer] couldnt find an active tab', activeTab); + this.debug( + '[createInSameContainer] couldnt find an active tab', + activeTab + ); return; } try { @@ -231,21 +254,21 @@ class Tabs { index: activeTab.index + 1, cookieStoreId: activeTab.cookieStoreId, }); - debug( + this.debug( '[createInSameContainer] new same container tab created', activeTab, newTab ); } catch (error) { - debug('[createInSameContainer] couldnt create tab', error); + this.debug('[createInSameContainer] couldnt create tab', error); } } catch (error) { - debug('[createInSameContainer] couldnt query tabs', error); + this.debug('[createInSameContainer] couldnt query tabs', error); } this.creatingInSameContainer = false; } - async remove(tab) { + async remove(tab: Tab): Promise { try { // make sure we dont close the window by removing this tab // TODO implement actual queue for removal, race-condition (and with that window-closing) is possible @@ -255,12 +278,12 @@ class Tabs { if (tabs.length > 1) { try { await browser.tabs.remove(tab.id); - debug('[removeTab] removed old tab', tab.id); + this.debug('[removeTab] removed old tab', tab.id); } catch (error) { - debug('[removeTab] error while removing old tab', tab, error); + this.debug('[removeTab] error while removing old tab', tab, error); } } else { - debug( + this.debug( '[removeTab] queuing removal of tab to prevent closing of window', tab, tabs @@ -270,9 +293,7 @@ class Tabs { }); } } catch (error) { - debug('[removeTab] couldnt query tabs', tab, error); + this.debug('[removeTab] couldnt query tabs', tab, error); } } } - -export default Tabs; diff --git a/src/background/tmp.ts b/src/background/tmp.ts new file mode 100644 index 00000000..52a122a9 --- /dev/null +++ b/src/background/tmp.ts @@ -0,0 +1,125 @@ +import { Log } from './log'; +import { EventListeners } from './event-listeners'; +import { BrowserAction } from './browseraction'; +import { Cleanup } from './cleanup'; +import { Commands } from './commands'; +import { Container } from './container'; +import { ContextMenu } from './contextmenu'; +import { Convert } from './convert'; +import { Cookies } from './cookies'; +import { History } from './history'; +import { Isolation } from './isolation'; +import { MultiAccountContainers } from './mac'; +import { Management } from './management'; +import { Migration } from './migration'; +import { MigrationLegacy } from './migration-legacy'; +import { MouseClick } from './mouseclick'; +import { PageAction } from './pageaction'; +import { Preferences } from './preferences'; +import { Request } from './request'; +import { Runtime } from './runtime'; +import { Statistics } from './statistics'; +import { Storage } from './storage'; +import { Tabs } from './tabs'; +import { Utils } from './utils'; +import { PreferencesSchema, Permissions } from '~/types'; + +export class TemporaryContainers { + public initialized = false; + public log = new Log(); + public debug = this.log.debug; + public utils = new Utils(this); + public preferences = new Preferences(this); + public storage = new Storage(this); + public runtime = new Runtime(this); + public management = new Management(this); + public request = new Request(this); + public container = new Container(this); + public mouseclick = new MouseClick(this); + public tabs = new Tabs(this); + public commands = new Commands(this); + public browseraction = new BrowserAction(this); + public pageaction = new PageAction(this); + public contextmenu = new ContextMenu(this); + public cookies = new Cookies(this); + public isolation = new Isolation(this); + public history = new History(this); + public cleanup = new Cleanup(this); + public convert = new Convert(this); + public statistics = new Statistics(this); + public mac = new MultiAccountContainers(this); + public migration = new Migration(this); + public migrationLegacy = new MigrationLegacy(this); + public eventlisteners = new EventListeners(this); + + public version!: string; + public containerPrefix = 'firefox'; + public permissions!: Permissions; + public pref!: PreferencesSchema; + + async initialize(): Promise { + if (this.initialized) { + throw new Error('already initialized'); + } + + this.debug('[tmp] initializing'); + browser.browserAction.disable(); + this.version = browser.runtime.getManifest().version; + const { permissions } = await browser.permissions.getAll(); + if (!permissions) { + throw new Error('permissions.getAll() failed'); + } + this.permissions = { + bookmarks: permissions.includes('bookmarks'), + history: permissions.includes('history'), + notifications: permissions.includes('notifications'), + downloads: permissions.includes('downloads'), + }; + + this.preferences.initialize(); + await this.storage.initialize(); + + this.pref = (new Proxy(this.storage, { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get(target, key): any { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (target.local.preferences as any)[key]; + }, + }) as unknown) as PreferencesSchema; + + if (!this.storage.local.containerPrefix) { + const browserInfo = await browser.runtime.getBrowserInfo(); + this.storage.local.containerPrefix = browserInfo.name.toLowerCase(); + await this.storage.persist(); + } + this.containerPrefix = this.storage.local.containerPrefix; + + this.request.initialize(); + this.runtime.initialize(); + this.container.initialize(); + this.mouseclick.initialize(); + this.commands.initialize(); + this.browseraction.initialize(); + this.pageaction.initialize(); + this.contextmenu.initialize(); + this.cookies.initialize(); + this.statistics.initialize(); + this.mac.initialize(); + this.isolation.initialize(); + this.history.initialize(); + this.cleanup.initialize(); + this.convert.initialize(); + this.tabs.initialize(); + + await this.management.initialize(); + + this.debug('[tmp] initialized'); + this.initialized = true; + this.eventlisteners.tmpInitialized(); + browser.browserAction.enable(); + + await this.tabs.handleAlreadyOpen(); + + return this; + } +} diff --git a/src/background/utils.js b/src/background/utils.ts similarity index 68% rename from src/background/utils.js rename to src/background/utils.ts index 3cd06ddf..827e6ee3 100644 --- a/src/background/utils.js +++ b/src/background/utils.ts @@ -1,24 +1,46 @@ -class Utils { - sameDomain(origin, target) { - return psl.parse(origin).domain === psl.parse(target).domain; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { psl } from './lib'; +import { TemporaryContainers } from './tmp'; +import { Debug } from '~/types'; + +export class Utils { + private debug: Debug; + + constructor(background: TemporaryContainers) { + this.debug = background.debug; + } + + sameDomain(origin: string, target: string): boolean { + const parsedOrigin = psl.parse(origin); + const parsedTarget = psl.parse(target); + if (parsedOrigin.error || parsedTarget.error) { + return false; + } + return parsedOrigin.domain === parsedTarget.domain; } - addMissingKeys({ defaults, source }) { + addMissingKeys({ + defaults, + source, + }: { + defaults: any; + source: any; + }): boolean { let addedMissing = false; - const addKeys = (_default, _source) => { - Object.keys(_default).map(key => { - if (_source[key] === undefined) { - debug( + const addKeys = (defaultsNode: any, sourceNode: any): void => { + Object.keys(defaultsNode).map(key => { + if (sourceNode[key] === undefined) { + this.debug( '[addMissingKeys] key not found, setting default', key, - _default[key] + defaultsNode[key] ); - _source[key] = _default[key]; + sourceNode[key] = defaultsNode[key]; addedMissing = true; - } else if (Array.isArray(_source[key])) { + } else if (Array.isArray(sourceNode[key])) { return; - } else if (typeof _source[key] === 'object') { - addKeys(_default[key], _source[key]); + } else if (typeof sourceNode[key] === 'object') { + addKeys(defaultsNode[key], sourceNode[key]); } }); }; @@ -27,11 +49,11 @@ class Utils { return addedMissing; } - clone(input) { + clone(input: any): any { return JSON.parse(JSON.stringify(input)); } - globToRegexp(glob) { + globToRegexp(glob: string): RegExp { // -------------------------------------------------------------------------------- // modified and simplified version of https://github.com/fitzgen/glob-to-regexp // version 0.4.0 @@ -52,16 +74,16 @@ class Utils { throw new TypeError('Expected a string'); } - var str = String(glob); + const str = String(glob); // The regexp we are building, as a string. - var reStr = ''; + let reStr = ''; // RegExp flags (eg "i" ) to pass in to RegExp constructor. - var flags = 'i'; + const flags = 'i'; - var c; - for (var i = 0, len = str.length; i < len; i++) { + let c; + for (let i = 0, len = str.length; i < len; i++) { c = str[i]; switch (c) { @@ -91,7 +113,7 @@ class Utils { return new RegExp('^' + reStr + '$', flags); } - versionCompare(a, b) { + versionCompare(a: string, b: string): 1 | 0 | -1 { // https://github.com/substack/semver-compare // https://github.com/substack/semver-compare/pull/4 @@ -114,18 +136,24 @@ class Utils { // 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. - var pa = a.split('.'); - var pb = b.split('.'); - for (var i = 0; i < Math.min(pa.length, pb.length); i++) { - var na = Number(pa[i]); - var nb = Number(pb[i]); - if (na > nb) return 1; - if (nb > na) return -1; - if (!isNaN(na) && isNaN(nb)) return 1; - if (isNaN(na) && !isNaN(nb)) return -1; + const pa = a.split('.'); + const pb = b.split('.'); + for (let i = 0; i < Math.min(pa.length, pb.length); i++) { + const na = Number(pa[i]); + const nb = Number(pb[i]); + if (na > nb) { + return 1; + } + if (nb > na) { + return -1; + } + if (!isNaN(na) && isNaN(nb)) { + return 1; + } + if (isNaN(na) && !isNaN(nb)) { + return -1; + } } return 0; } } - -export default Utils; diff --git a/src/contentscript.js b/src/contentscript.ts similarity index 78% rename from src/contentscript.js rename to src/contentscript.ts index 11d7ff59..557ea000 100644 --- a/src/contentscript.js +++ b/src/contentscript.ts @@ -14,11 +14,12 @@ document.body.addEventListener( // sometimes websites change links on click // so we wait for the next tick and with that increase // the chance that we actually see the correct link - await new Promise(setTimeout); + await new Promise(resolve => setTimeout(resolve)); // check for a element with href - const aElement = event.target.closest('a'); - if (aElement === null || typeof aElement !== 'object' || !aElement.href) { + const target = event?.target as HTMLElement | null; + const aElement = target?.closest('a'); + if (!aElement || typeof aElement !== 'object' || !aElement.href) { return; } diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..2e54c80c --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,14 @@ +import { TemporaryContainers } from './background/tmp'; +import { Log } from './background/log'; + +declare global { + interface Window { + tmp?: TemporaryContainers; + log: Log; + _mochaTest?: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + debug: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + migrationLegacy: any; + } +} diff --git a/src/options.html b/src/options.html index f6ab8379..cd0ddfb0 100644 --- a/src/options.html +++ b/src/options.html @@ -15,8 +15,7 @@ - - + diff --git a/src/popup.html b/src/popup.html index 90c7eac6..a5538f08 100644 --- a/src/popup.html +++ b/src/popup.html @@ -13,8 +13,7 @@ - - + diff --git a/src/shared.ts b/src/shared.ts new file mode 100644 index 00000000..a57e605f --- /dev/null +++ b/src/shared.ts @@ -0,0 +1,69 @@ +import { Permissions } from './types'; + +export const getPermissions = async (): Promise => { + const { permissions } = await browser.permissions.getAll(); + if (!permissions) { + throw new Error('permissions.getAll didnt return permissions'); + } + return { + bookmarks: permissions.includes('bookmarks'), + downloads: permissions.includes('downloads'), + history: permissions.includes('history'), + notifications: permissions.includes('notifications'), + }; +}; + +export const CONTAINER_COLORS = [ + 'blue', // #37ADFF + 'turquoise', // #00C79A + 'green', // #51CD00 + 'yellow', // #FFCB00 + 'orange', // #FF9F00 + 'red', // #FF613D + 'pink', // #FF4BDA + 'purple', // #AF51F5 + 'toolbar', +]; + +export const CONTAINER_ICONS = [ + 'fingerprint', + 'briefcase', + 'dollar', + 'cart', + 'circle', + 'gift', + 'vacation', + 'food', + 'fruit', + 'pet', + 'tree', + 'chill', + 'fence', +]; + +export const TOOLBAR_ICON_COLORS = [ + 'default', + 'black-simple', + 'blue-simple', + 'red-simple', + 'white-simple', +]; + +export const IGNORED_DOMAINS_DEFAULT = ['getpocket.com', 'addons.mozilla.org']; + +export const REDIRECTOR_DOMAINS_DEFAULT = [ + 't.co', + 'outgoing.prod.mozaws.net', + 'slack-redir.net', +]; + +export const formatBytes = (bytes: number, decimals = 2): string => { + // https://stackoverflow.com/a/18650828 + if (bytes == 0) { + return '0 Bytes'; + } + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + sizes[i]; +}; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..e043c7cf --- /dev/null +++ b/src/types.ts @@ -0,0 +1,259 @@ +import { + CONTAINER_COLORS, + CONTAINER_ICONS, + TOOLBAR_ICON_COLORS, + IGNORED_DOMAINS_DEFAULT, + REDIRECTOR_DOMAINS_DEFAULT, +} from './shared'; + +export type TabId = number; +export type WindowId = number; + +export type Milliseconds = number; +export type IgnoredDomain = typeof IGNORED_DOMAINS_DEFAULT[number] | string; +export type RedirectorDomain = + | typeof REDIRECTOR_DOMAINS_DEFAULT[number] + | string; + +export type CookieStoreId = string; + +export interface ContainerOptions { + name: string; + color: ContainerColor; + icon: ContainerIcon; + number: number; + clean: boolean; + deletesHistory?: boolean; + history?: { + [key: string]: { tabId: TabId }; + }; +} + +export interface CreateTabOptions { + cookieStoreId: CookieStoreId; + url?: string; + active?: boolean; + index?: number; + pinned?: boolean; + openerTabId?: number; +} + +export interface TmpTabOptions { + tab?: Tab; + url?: string; + active?: boolean; + request?: false | browser.webRequest.WebRequestOnBeforeRequestDetails; + dontPin?: boolean; + deletesHistory?: boolean; + macConfirmPage?: boolean; +} + +export type IsolationAction = + | 'never' + | 'notsamedomain' + | 'notsamedomainexact' + | 'always' + | 'global'; + +export interface IsolationGlobal { + navigation: { + action: IsolationAction; + }; + mouseClick: { + middle: { + action: IsolationAction; + container: 'default' | 'deleteshistory'; + }; + ctrlleft: { + action: IsolationAction; + container: 'default' | 'deleteshistory'; + }; + left: { + action: IsolationAction; + container: 'default' | 'deleteshistory'; + }; + }; + excluded: { + [key: string]: object; + }; + excludedContainers: { + [key: string]: object; + }; +} + +export interface IsolationDomain extends IsolationGlobal { + pattern: string; + always: { + action: 'enabled' | 'disabled'; + allowedInPermanent: boolean; + allowedInTemporary: boolean; + }; +} + +export interface Cookie { + [key: string]: string; + domain: string; + expirationDate: string; + firstPartyDomain: string; + httpOnly: '' | 'true' | 'false'; + name: string; + path: string; + sameSite: '' | browser.cookies.SameSiteStatus; + secure: '' | 'true' | 'false'; + url: string; + value: string; +} + +export type ToolbarIconColor = + | 'default' + | 'black-simple' + | 'blue-simple' + | 'red-simple' + | 'white-simple'; + +export interface StorageLocal { + containerPrefix: string | false; + tempContainerCounter: number; + tempContainers: { + [key: string]: ContainerOptions; + }; + tempContainersNumbers: number[]; + statistics: { + startTime: Date; + containersDeleted: number; + cookiesDeleted: number; + cacheDeleted: number; + deletesHistory: { + containersDeleted: number; + cookiesDeleted: number; + urlsDeleted: number; + }; + }; + preferences: PreferencesSchema; + lastFileExport: false; + version: false | string; +} + +export interface PreferencesSchema { + automaticMode: { + active: boolean; + newTab: 'created' | 'navigation'; + }; + notifications: boolean; + container: { + namePrefix: string; + color: ContainerColor; + colorRandom: boolean; + colorRandomExcluded: ContainerColor[]; + icon: ContainerIcon; + iconRandom: boolean; + iconRandomExcluded: ContainerIcon[]; + numberMode: 'keep' | 'keepuntilrestart' | 'reuse' | 'hide'; + removal: Milliseconds; + }; + iconColor: ToolbarIconColor; + isolation: { + active: boolean; + global: IsolationGlobal; + domain: IsolationDomain[]; + mac: { + action: 'enabled' | 'disabled'; + }; + }; + browserActionPopup: boolean; + pageAction: boolean; + contextMenu: boolean; + contextMenuBookmarks: boolean; + keyboardShortcuts: { + AltC: boolean; + AltP: boolean; + AltN: boolean; + AltShiftC: boolean; + AltX: boolean; + AltO: boolean; + AltI: boolean; + }; + replaceTabs: boolean; + closeRedirectorTabs: { + active: boolean; + delay: number; + domains: RedirectorDomain[]; + }; + ignoreRequests: IgnoredDomain[]; + cookies: { + domain: { + [key: string]: Cookie[]; + }; + }; + deletesHistory: { + active: boolean; + automaticMode: 'never' | 'automatic'; + contextMenu: boolean; + contextMenuBookmarks: boolean; + containerAlwaysPerDomain: 'never' | 'automatic'; + containerIsolation: 'never' | 'automatic'; + containerRemoval: Milliseconds; + containerMouseClicks: 'never' | 'automatic'; + statistics: boolean; + }; + statistics: boolean; + ui: { + expandPreferences: boolean; + popupDefaultTab: + | 'isolation-global' + | 'isolation-per-domain' + | 'isolation-mac' + | 'actions' + | 'statistics'; + }; +} + +export interface Tab extends browser.tabs.Tab { + id: number; + url: string; + windowId: number; + cookieStoreId: string; +} + +export interface Permissions { + bookmarks: boolean; + downloads: boolean; + history: boolean; + notifications: boolean; +} + +export type ContainerColor = typeof CONTAINER_COLORS[number]; +export type ContainerIcon = typeof CONTAINER_ICONS[number]; +export type ToolbarIconColors = typeof TOOLBAR_ICON_COLORS[number]; + +export interface MacAssignment { + userContextId: string; + cookieStoreId: string; + neverAsk: boolean; +} + +export type OnBeforeRequestResult = + | undefined + | boolean + | { clean?: boolean; cancel?: boolean }; + +export type ClickType = 'middle' | 'left' | 'ctrlleft'; +export interface ClickEvent { + button: number; + ctrlKey: boolean; + metaKey: boolean; +} + +export interface ClickMessage { + href: string; + event: ClickEvent; +} + +export interface RuntimeMessage { + method: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + payload: ClickMessage | any; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Debug = (...args: any[]) => Promise; diff --git a/src/ui/.eslintrc.json b/src/ui/.eslintrc.json index ef2b077c..bc66312c 100644 --- a/src/ui/.eslintrc.json +++ b/src/ui/.eslintrc.json @@ -3,7 +3,6 @@ "sourceType": "module" }, "globals": { - "$": "readonly", - "Vue": "readonly" + "$": "readonly" } } diff --git a/src/ui/components/actions.ts b/src/ui/components/actions.ts new file mode 100644 index 00000000..544a2c5f --- /dev/null +++ b/src/ui/components/actions.ts @@ -0,0 +1,74 @@ +import Vue from 'vue'; +import { Popup } from '../root'; + +export default Vue.extend({ + props: { + app: { + type: Object as () => Popup, + required: true, + }, + }, + data() { + return { + preferences: this.app.preferences, + permissions: this.app.permissions, + activeTab: this.app.activeTab, + isHttpTab: this.app.activeTab.url.startsWith('http'), + }; + }, + methods: { + openInTmp(): void { + browser.runtime.sendMessage({ + method: 'createTabInTempContainer', + payload: { + url: this.activeTab.url, + }, + }); + window.close(); + }, + openInDeletesHistoryTmp(): void { + browser.runtime.sendMessage({ + method: 'createTabInTempContainer', + payload: { + url: this.activeTab.url, + deletesHistory: true, + }, + }); + window.close(); + }, + convertToRegular(): void { + browser.runtime.sendMessage({ + method: 'convertTempContainerToRegular', + payload: { + cookieStoreId: this.activeTab.cookieStoreId, + tabId: this.activeTab.id, + url: this.activeTab.url, + }, + }); + window.close(); + }, + convertToPermanent(): void { + browser.runtime.sendMessage({ + method: 'convertTempContainerToPermanent', + payload: { + cookieStoreId: this.activeTab.cookieStoreId, + tabId: this.activeTab.id, + name: this.activeTab.parsedUrl.hostname, + url: this.activeTab.url, + }, + }); + window.close(); + }, + convertToTemporary(): void { + browser.runtime.sendMessage({ + method: 'convertPermanentToTempContainer', + payload: { + cookieStoreId: this.activeTab.cookieStoreId, + tabId: this.activeTab.id, + url: this.activeTab.url, + }, + }); + window.close(); + }, + }, +}); diff --git a/src/ui/components/actions.vue b/src/ui/components/actions.vue index 349777a9..5a3166d6 100644 --- a/src/ui/components/actions.vue +++ b/src/ui/components/actions.vue @@ -1,75 +1,5 @@