From 3ab19d50d0aecba5f6dc4944d20ce125a4e82f7d Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Tue, 28 May 2024 10:51:39 -0700 Subject: [PATCH 1/4] Re-inject site scripts when background process wakes up from idle --- src/background.ts | 27 ++++++++++++++++++++------- src/permissions-helper.ts | 2 +- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/background.ts b/src/background.ts index 1dbc286..90aa589 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,11 +1,10 @@ -import type { WebNavigation } from 'webextension-polyfill'; import { runtime, scripting, tabs, webNavigation } from 'webextension-polyfill'; import { fetchUser } from './gkApi'; import { injectionScope as inject_azureDevops } from './hosts/azureDevops'; import { injectionScope as inject_bitbucket } from './hosts/bitbucket'; import { injectionScope as inject_github } from './hosts/github'; import { injectionScope as inject_gitlab } from './hosts/gitlab'; -import { refreshPermissions } from './permissions-helper'; +import { domainToMatchPattern, refreshPermissions } from './permissions-helper'; import { getEnterpriseConnections, GKDotDevUrl, PermissionsGrantedMessage, PopupInitMessage } from './shared'; import type { CacheContext } from './types'; @@ -68,18 +67,32 @@ async function addInjectionListener(context: CacheContext) { const allDomains = Object.values(injectionDomains as any).flat(); // note: This is a closure over injectionDomains - const injectScript = (details: WebNavigation.OnDOMContentLoadedDetailsType) => { + const injectScript = (tabId: number, tabUrl: string) => { void scripting.executeScript({ - target: { tabId: details.tabId }, + target: { tabId: tabId }, // injectImmediately: true, - func: getInjectionFn(details.url, injectionDomains), - args: [details.url, GKDotDevUrl], + func: getInjectionFn(tabUrl, injectionDomains), + args: [tabUrl, GKDotDevUrl], }); }; - webNavigation.onDOMContentLoaded.addListener(injectScript, { + webNavigation.onDOMContentLoaded.addListener(details => injectScript(details.tabId, details.url), { url: allDomains.map(domain => ({ hostContains: domain })), }); + + // Immediately inject into the currently open compatible tabs. This is needed because when the background + // script is idle, its event listeners are not active. Opening a compatible tab will cause the background + // script to awaken and setup the event listeners again, but the tab will load before that happens. + const currentTabs = await tabs.query({ + url: allDomains.map(domainToMatchPattern), + status: 'complete', // only query tabs that have finished loading + discarded: false, // discarded tabs will reload when focused so we don't need to inject into them now + }); + currentTabs.forEach(tab => { + if (tab.id && tab.url) { + injectScript(tab.id, tab.url); + } + }); } function urlHostHasDomain(url: URL, domains: string[]): boolean { diff --git a/src/permissions-helper.ts b/src/permissions-helper.ts index 38b73b6..c7ec795 100644 --- a/src/permissions-helper.ts +++ b/src/permissions-helper.ts @@ -3,7 +3,7 @@ import { permissions } from 'webextension-polyfill'; import { arrayDifference, CloudProviders, getEnterpriseConnections } from './shared'; import type { CacheContext } from './types'; -function domainToMatchPattern(domain: string): string { +export function domainToMatchPattern(domain: string): string { return `*://*.${domain}/*`; } From f87146d9b6ccb18ab277737beaafe97b7755f2e1 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Wed, 29 May 2024 16:48:07 -0700 Subject: [PATCH 2/4] Make the onDOMContentLoaded even listener a global listener so it persists --- src/background.ts | 66 +++++++++++++++------------------------ src/permissions-helper.ts | 2 +- 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/src/background.ts b/src/background.ts index 90aa589..56a7f0a 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,10 +1,10 @@ -import { runtime, scripting, tabs, webNavigation } from 'webextension-polyfill'; +import { runtime, scripting, storage, tabs, webNavigation } from 'webextension-polyfill'; import { fetchUser } from './gkApi'; import { injectionScope as inject_azureDevops } from './hosts/azureDevops'; import { injectionScope as inject_bitbucket } from './hosts/bitbucket'; import { injectionScope as inject_github } from './hosts/github'; import { injectionScope as inject_gitlab } from './hosts/gitlab'; -import { domainToMatchPattern, refreshPermissions } from './permissions-helper'; +import { refreshPermissions } from './permissions-helper'; import { getEnterpriseConnections, GKDotDevUrl, PermissionsGrantedMessage, PopupInitMessage } from './shared'; import type { CacheContext } from './types'; @@ -22,6 +22,24 @@ const DefaultInjectionDomains: InjectionDomains = { azureDevops: ['dev.azure.com'], }; +webNavigation.onDOMContentLoaded.addListener(async details => { + const { injectionDomains } = (await storage.session.get('injectionDomains')) as { + injectionDomains?: InjectionDomains; + }; + if (!injectionDomains) { + return; + } + + const injectionFn = getInjectionFn(details.url, injectionDomains); + if (injectionFn) { + void scripting.executeScript({ + target: { tabId: details.tabId }, + func: injectionFn, + args: [details.url, GKDotDevUrl], + }); + } +}); + webNavigation.onHistoryStateUpdated.addListener(details => { // used to detect when the user navigates to a different page in the same tab const url = new URL(details.url); @@ -62,39 +80,6 @@ async function computeInjectionDomains(context: CacheContext) { return injectionDomains; } -async function addInjectionListener(context: CacheContext) { - const injectionDomains = await computeInjectionDomains(context); - const allDomains = Object.values(injectionDomains as any).flat(); - - // note: This is a closure over injectionDomains - const injectScript = (tabId: number, tabUrl: string) => { - void scripting.executeScript({ - target: { tabId: tabId }, - // injectImmediately: true, - func: getInjectionFn(tabUrl, injectionDomains), - args: [tabUrl, GKDotDevUrl], - }); - }; - - webNavigation.onDOMContentLoaded.addListener(details => injectScript(details.tabId, details.url), { - url: allDomains.map(domain => ({ hostContains: domain })), - }); - - // Immediately inject into the currently open compatible tabs. This is needed because when the background - // script is idle, its event listeners are not active. Opening a compatible tab will cause the background - // script to awaken and setup the event listeners again, but the tab will load before that happens. - const currentTabs = await tabs.query({ - url: allDomains.map(domainToMatchPattern), - status: 'complete', // only query tabs that have finished loading - discarded: false, // discarded tabs will reload when focused so we don't need to inject into them now - }); - currentTabs.forEach(tab => { - if (tab.id && tab.url) { - injectScript(tab.id, tab.url); - } - }); -} - function urlHostHasDomain(url: URL, domains: string[]): boolean { return domains.some(domain => url.hostname.endsWith(domain)); } @@ -102,7 +87,7 @@ function urlHostHasDomain(url: URL, domains: string[]): boolean { function getInjectionFn( rawUrl: string, injectionDomains: InjectionDomains, -): (url: string, gkDotDevUrl: string) => void { +): ((url: string, gkDotDevUrl: string) => void) | null { const url = new URL(rawUrl); if (urlHostHasDomain(url, injectionDomains.github)) { return inject_github; @@ -120,8 +105,7 @@ function getInjectionFn( return inject_azureDevops; } - console.error('Unsupported host'); - throw new Error('Unsupported host'); + return null; } async function main() { @@ -131,9 +115,9 @@ async function main() { const context: CacheContext = {}; // This removes unneded permissions await refreshPermissions(context); - // NOTE: This may request hosts that we may not have permissions for, which will log errors for the extension - // This does not cause any issues, and eliminating the errors requires more logic - await addInjectionListener(context); + + const injectionDomains = await computeInjectionDomains(context); + void storage.session.set({ injectionDomains: injectionDomains }); } void main(); diff --git a/src/permissions-helper.ts b/src/permissions-helper.ts index c7ec795..38b73b6 100644 --- a/src/permissions-helper.ts +++ b/src/permissions-helper.ts @@ -3,7 +3,7 @@ import { permissions } from 'webextension-polyfill'; import { arrayDifference, CloudProviders, getEnterpriseConnections } from './shared'; import type { CacheContext } from './types'; -export function domainToMatchPattern(domain: string): string { +function domainToMatchPattern(domain: string): string { return `*://*.${domain}/*`; } From d111cdc7623c412ebb27efa37550f376573c8c56 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Wed, 29 May 2024 18:04:26 -0700 Subject: [PATCH 3/4] Inject GK buttons into current tabs on startup and on install --- src/background.ts | 58 +++++++++++++++++++++++++++++---------- src/permissions-helper.ts | 2 +- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/background.ts b/src/background.ts index 56a7f0a..9fe5a49 100644 --- a/src/background.ts +++ b/src/background.ts @@ -4,7 +4,7 @@ import { injectionScope as inject_azureDevops } from './hosts/azureDevops'; import { injectionScope as inject_bitbucket } from './hosts/bitbucket'; import { injectionScope as inject_github } from './hosts/github'; import { injectionScope as inject_gitlab } from './hosts/gitlab'; -import { refreshPermissions } from './permissions-helper'; +import { domainToMatchPattern, refreshPermissions } from './permissions-helper'; import { getEnterpriseConnections, GKDotDevUrl, PermissionsGrantedMessage, PopupInitMessage } from './shared'; import type { CacheContext } from './types'; @@ -23,12 +23,7 @@ const DefaultInjectionDomains: InjectionDomains = { }; webNavigation.onDOMContentLoaded.addListener(async details => { - const { injectionDomains } = (await storage.session.get('injectionDomains')) as { - injectionDomains?: InjectionDomains; - }; - if (!injectionDomains) { - return; - } + const injectionDomains = await getInjectionDomains(); const injectionFn = getInjectionFn(details.url, injectionDomains); if (injectionFn) { @@ -64,6 +59,48 @@ runtime.onMessage.addListener(async msg => { return undefined; }); +runtime.onInstalled.addListener(injectIntoCurrentTabs); +runtime.onStartup.addListener(injectIntoCurrentTabs); + +async function injectIntoCurrentTabs() { + const injectionDomains = await getInjectionDomains(); + const allDomains = Object.values(injectionDomains as any).flat(); + + const currentTabs = await tabs.query({ + url: allDomains.map(domainToMatchPattern), + status: 'complete', + discarded: false, + }); + currentTabs.forEach(tab => { + if (tab.id && tab.url) { + const injectionFn = getInjectionFn(tab.url, injectionDomains); + if (injectionFn) { + void scripting.executeScript({ + target: { tabId: tab.id }, + func: injectionFn, + args: [tab.url, GKDotDevUrl], + }); + } + } + }); +} + +async function getInjectionDomains() { + let { injectionDomains } = (await storage.session.get('injectionDomains')) as { + injectionDomains?: InjectionDomains; + }; + if (!injectionDomains) { + const context: CacheContext = {}; + // This removes unneded permissions + await refreshPermissions(context); + + injectionDomains = await computeInjectionDomains(context); + await storage.session.set({ injectionDomains: injectionDomains }); + } + + return injectionDomains; +} + async function computeInjectionDomains(context: CacheContext) { const injectionDomains = structuredClone(DefaultInjectionDomains); const enterpriseConnections = await getEnterpriseConnections(context); @@ -111,13 +148,6 @@ function getInjectionFn( async function main() { // The fetchUser function also updates the extension icon if the user is logged in await fetchUser(); - - const context: CacheContext = {}; - // This removes unneded permissions - await refreshPermissions(context); - - const injectionDomains = await computeInjectionDomains(context); - void storage.session.set({ injectionDomains: injectionDomains }); } void main(); diff --git a/src/permissions-helper.ts b/src/permissions-helper.ts index 38b73b6..c7ec795 100644 --- a/src/permissions-helper.ts +++ b/src/permissions-helper.ts @@ -3,7 +3,7 @@ import { permissions } from 'webextension-polyfill'; import { arrayDifference, CloudProviders, getEnterpriseConnections } from './shared'; import type { CacheContext } from './types'; -function domainToMatchPattern(domain: string): string { +export function domainToMatchPattern(domain: string): string { return `*://*.${domain}/*`; } From 841d83cf5e5bcc38d6455dab6557d17ece9df202 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Wed, 29 May 2024 18:45:54 -0700 Subject: [PATCH 4/4] Clear injectionDomains cache instead of reloading extension when permissions are granted --- src/background.ts | 3 +-- src/popup/components/RequestPermissionsBanner.tsx | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/background.ts b/src/background.ts index 9fe5a49..4f45b37 100644 --- a/src/background.ts +++ b/src/background.ts @@ -51,8 +51,7 @@ runtime.onMessage.addListener(async msg => { const context: CacheContext = {}; return refreshPermissions(context); } else if (msg === PermissionsGrantedMessage) { - // Reload extension to update injection listener - runtime.reload(); + await storage.session.remove('injectionDomains'); return undefined; } console.error('Recevied unknown runtime message', msg); diff --git a/src/popup/components/RequestPermissionsBanner.tsx b/src/popup/components/RequestPermissionsBanner.tsx index 52a00c8..54a9008 100644 --- a/src/popup/components/RequestPermissionsBanner.tsx +++ b/src/popup/components/RequestPermissionsBanner.tsx @@ -33,6 +33,7 @@ export const RequestPermissionsBanner = ({ permissionsRequest }: { permissionsRe const granted = await permissions.request(permissionsRequest.request); if (granted) { await sendPermissionsGranted(); + window.close(); } }} >