Skip to content

Commit

Permalink
Extract tab lifecycle events (duckduckgo#2123)
Browse files Browse the repository at this point in the history
* Rename 'features' to 'components'.

* Move all tabManager tracking code to a separate component

* Fix lint

* Fix import

* Remove onCreatedNavigationTarget listener from events.js
  • Loading branch information
sammacbeth authored Aug 31, 2023
1 parent 44feb69 commit 21b1b0c
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 112 deletions.
2 changes: 1 addition & 1 deletion integration-test/fire-button.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async function requestBrowsingDataPermissions (backgroundPage) {
* @returns {Promise<import('@playwright/test').JSHandle>}
*/
function getFireButtonHandle (backgroundPage) {
return backgroundPage.evaluateHandle(() => globalThis.features.fireButton)
return backgroundPage.evaluateHandle(() => globalThis.components.fireButton)
}

async function waitForAllResults (page) {
Expand Down
13 changes: 8 additions & 5 deletions shared/js/background/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
/* global DEBUG, RELOADER, BUILD_TARGET */

import { onStartup } from './startup'
import FireButton from './features/fire-button'
import FireButton from './components/fire-button'
import TabTracker from './components/tab-tracking'
import initDebugBuild from './devbuild'
import initReloader from './devbuild-reloader'
import tabManager from './tab-manager'
Expand All @@ -38,14 +39,16 @@ settings.ready().then(() => {
* fireButton?: FireButton;
* }}
*/
const features = {}
const components = {
tabTracking: new TabTracker({ tabManager })
}

if (BUILD_TARGET === 'chrome' || BUILD_TARGET === 'chrome-mv3') {
features.fireButton = new FireButton({ settings, tabManager })
components.fireButton = new FireButton({ settings, tabManager })
}
console.log('Loaded features:', features)
console.log('Loaded components:', components)
// @ts-ignore
self.features = features
self.components = components

// Optional features controlled by build flags.
// If these flags are set to false, the whole function is tree-shaked from the build.
Expand Down
120 changes: 120 additions & 0 deletions shared/js/background/components/tab-tracking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/* global BUILD_TARGET */
import browser from 'webextension-polyfill'
import { restoreDefaultClickToLoadRuleActions } from '../dnr-click-to-load'
import Companies from '../companies'
import { isRedirect } from '../utils'
// eslint-disable-next-line no-restricted-syntax
import * as devtools from '../devtools'

/**
* @typedef {import('../tab-manager.js')} TabManager
* @typedef {import('../devtools')} Devtools
*/

export default class TabTracker {
/**
* @param {{
* tabManager: TabManager;
* }} options
*/
constructor ({ tabManager }) {
this.tabManager = tabManager
this.createdTargets = new Map()

browser.webRequest.onHeadersReceived.addListener((request) => {
this.tabManager.updateTabUrl(request)
const tab = tabManager.get({ tabId: request.tabId })
// SERP ad click detection
if (
isRedirect(request.statusCode)
) {
tab.setAdClickIfValidRedirect(request.url)
} else if (tab && tab.adClick && tab.adClick.adClickRedirect && !isRedirect(request.statusCode)) {
tab.adClick.setAdBaseDomain(tab.site.baseDomain || '')
}
}, { urls: ['<all_urls>'], types: ['main_frame'] })

// Store the created tab id for when onBeforeNavigate is called so data can be copied across from the source tab
browser.webNavigation.onCreatedNavigationTarget.addListener(details => {
this.createdTargets.set(details.tabId, details.sourceTabId)
})

// keep track of URLs that the browser navigates to.
//
// this is supplemented by tabManager.updateTabUrl() on headersReceived:
// tabManager.updateTabUrl only fires when a tab has finished loading with a 200,
// which misses a couple of edge cases like browser special pages
// and Gmail's weird redirect which returns a 200 via a service worker
browser.webNavigation.onBeforeNavigate.addListener((details) => {
// ignore navigation on iframes
if (details.frameId !== 0) return

const currentTab = tabManager.get({ tabId: details.tabId })
const newTab = tabManager.create({ tabId: details.tabId, url: details.url })

if (BUILD_TARGET === 'chrome-mv3') {
// Ensure that the correct declarativeNetRequest allowing rules are
// added for this tab.
// Note: The webNavigation.onBeforeCommitted event would be better,
// since onBeforeNavigate can be fired for a navigation that is
// not later committed. But since there is a race-condition
// between the page loading and the rules being added, let's use
// onBeforeNavigate for now as it fires sooner.
restoreDefaultClickToLoadRuleActions(newTab)
}

// persist the last URL the tab was trying to upgrade to HTTPS
if (currentTab && currentTab.httpsRedirects) {
newTab.httpsRedirects.persistMainFrameRedirect(currentTab.httpsRedirects.getMainFrameRedirect())
}
if (this.createdTargets.has(details.tabId)) {
const sourceTabId = this.createdTargets.get(details.tabId)
this.createdTargets.delete(details.tabId)

const sourceTab = tabManager.get({ tabId: sourceTabId })
if (sourceTab && sourceTab.adClick) {
this.createdTargets.set(details.tabId, sourceTabId)
if (sourceTab.adClick.shouldPropagateAdClickForNewTab(newTab)) {
newTab.adClick = sourceTab.adClick.propagate(newTab.id)
}
}
}

newTab.updateSite(details.url)
devtools.postMessage(details.tabId, 'tabChange', devtools.serializeTab(newTab))
})

browser.tabs.onCreated.addListener((info) => {
if (info.id) {
tabManager.createOrUpdateTab(info.id, info)
}
})

browser.tabs.onUpdated.addListener((id, info) => {
// sync company data to storage when a tab finishes loading
if (info.status === 'complete') {
Companies.syncToStorage()
}
tabManager.createOrUpdateTab(id, info)
})

browser.tabs.onRemoved.addListener((id, info) => {
// remove the tab object
tabManager.delete(id)
})

this.restoreOrCreateTabs()
}

async restoreOrCreateTabs () {
const savedTabs = await browser.tabs.query({ status: 'complete' })
for (let i = 0; i < savedTabs.length; i++) {
const tab = savedTabs[i]

if (tab.url) {
// On reinstall we wish to create the tab again
await this.tabManager.restoreOrCreate(tab)
}
}
}
}
92 changes: 0 additions & 92 deletions shared/js/background/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import browser from 'webextension-polyfill'
import messageHandlers from './message-handlers'
import { updateActionIcon } from './events/privacy-icon-indicator'
import { flushSessionRules } from './dnr-session-rule-id'
import { restoreDefaultClickToLoadRuleActions } from './dnr-click-to-load'
import {
clearInvalidDynamicRules
} from './dnr-utils'
Expand Down Expand Up @@ -244,20 +243,6 @@ const isTopicsEnabled = manifestVersion === 2 && 'browsingTopics' in document &&

browser.webRequest.onHeadersReceived.addListener(
request => {
if (request.type === 'main_frame') {
tabManager.updateTabUrl(request)

const tab = tabManager.get({ tabId: request.tabId })
// SERP ad click detection
if (
utils.isRedirect(request.statusCode)
) {
tab.setAdClickIfValidRedirect(request.url)
} else if (tab && tab.adClick && tab.adClick.adClickRedirect && !utils.isRedirect(request.statusCode)) {
tab.adClick.setAdBaseDomain(tab.site.baseDomain)
}
}

if (ATB.shouldUpdateSetAtb(request)) {
// returns a promise
return ATB.updateSetAtb()
Expand All @@ -278,86 +263,9 @@ browser.webRequest.onHeadersReceived.addListener(
extraInfoSpec
)

// Store the created tab id for when onBeforeNavigate is called so data can be copied across from the source tab
const createdTargets = new Map()
browser.webNavigation.onCreatedNavigationTarget.addListener(details => {
createdTargets.set(details.tabId, details.sourceTabId)
})

/**
* Web Navigation
*/
// keep track of URLs that the browser navigates to.
//
// this is supplemented by tabManager.updateTabUrl() on headersReceived:
// tabManager.updateTabUrl only fires when a tab has finished loading with a 200,
// which misses a couple of edge cases like browser special pages
// and Gmail's weird redirect which returns a 200 via a service worker
browser.webNavigation.onBeforeNavigate.addListener(details => {
// ignore navigation on iframes
if (details.frameId !== 0) return

const currentTab = tabManager.get({ tabId: details.tabId })
const newTab = tabManager.create({ tabId: details.tabId, url: details.url })

if (manifestVersion === 3) {
// Ensure that the correct declarativeNetRequest allowing rules are
// added for this tab.
// Note: The webNavigation.onBeforeCommitted event would be better,
// since onBeforeNavigate can be fired for a navigation that is
// not later committed. But since there is a race-condition
// between the page loading and the rules being added, let's use
// onBeforeNavigate for now as it fires sooner.
restoreDefaultClickToLoadRuleActions(newTab)
}

// persist the last URL the tab was trying to upgrade to HTTPS
if (currentTab && currentTab.httpsRedirects) {
newTab.httpsRedirects.persistMainFrameRedirect(currentTab.httpsRedirects.getMainFrameRedirect())
}
if (createdTargets.has(details.tabId)) {
const sourceTabId = createdTargets.get(details.tabId)
createdTargets.delete(details.tabId)

const sourceTab = tabManager.get({ tabId: sourceTabId })
if (sourceTab && sourceTab.adClick) {
createdTargets.set(details.tabId, sourceTabId)
if (sourceTab.adClick.shouldPropagateAdClickForNewTab(newTab)) {
newTab.adClick = sourceTab.adClick.propagate(newTab.id)
}
}
}

newTab.updateSite(details.url)
devtools.postMessage(details.tabId, 'tabChange', devtools.serializeTab(newTab))
})

/**
* TABS
*/

const Companies = require('./companies')

browser.tabs.onCreated.addListener((info) => {
if (info.id) {
tabManager.createOrUpdateTab(info.id, info)
}
})

browser.tabs.onUpdated.addListener((id, info) => {
// sync company data to storage when a tab finishes loading
if (info.status === 'complete') {
Companies.syncToStorage()
}

tabManager.createOrUpdateTab(id, info)
})

browser.tabs.onRemoved.addListener((id, info) => {
// remove the tab object
tabManager.delete(id)
})

// message popup to close when the active tab changes.
browser.tabs.onActivated.addListener(() => {
browserWrapper.notifyPopup({ closePopup: true })
Expand Down
2 changes: 1 addition & 1 deletion shared/js/background/message-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { isFeatureEnabled, reloadCurrentTab } from './utils'
import { ensureClickToLoadRuleActionDisabled } from './dnr-click-to-load'
import tdsStorage from './storage/tds'
import { getArgumentsObject } from './helpers/arguments-object'
import { isFireButtonEnabled } from './features/fire-button'
import { isFireButtonEnabled } from './components/fire-button'
const { getDomain } = require('tldts')
const utils = require('./utils')
const settings = require('./settings')
Expand Down
12 changes: 0 additions & 12 deletions shared/js/background/startup.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* global BUILD_TARGET */
import browser from 'webextension-polyfill'
import { NewTabTrackerStats } from './newtab-tracker-stats'
import { TrackerStats } from './classes/tracker-stats.js'
import httpsStorage from './storage/https'
Expand All @@ -9,7 +8,6 @@ const Companies = require('./companies')
const experiment = require('./experiments')
const https = require('./https')
const settings = require('./settings')
const tabManager = require('./tab-manager')
const trackers = require('./trackers')
const dnrSessionId = require('./dnr-session-rule-id')
const { fetchAlias, showContextMenuAction } = require('./email-utils')
Expand Down Expand Up @@ -71,16 +69,6 @@ export async function onStartup () {
showContextMenuAction()
}

const savedTabs = await browser.tabs.query({ status: 'complete' })
for (let i = 0; i < savedTabs.length; i++) {
const tab = savedTabs[i]

if (tab.url) {
// On reinstall we wish to create the tab again
await tabManager.restoreOrCreate(tab)
}
}

if (resolveReadyPromise) {
resolveReadyPromise()
resolveReadyPromise = null
Expand Down
2 changes: 1 addition & 1 deletion unit-test/background/fire-button.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getOriginsForUrl, tabMatchesHostFilter } from '../../shared/js/background/features/fire-button'
import { getOriginsForUrl, tabMatchesHostFilter } from '../../shared/js/background/components/fire-button'

describe('fire button utils', () => {
describe('tabMatchesOriginFilter', () => {
Expand Down

0 comments on commit 21b1b0c

Please sign in to comment.