Skip to content

Commit

Permalink
Inject content scripts dynamically
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelgomesxyz committed May 4, 2021
1 parent 74311ca commit fdf5abd
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 16 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"react-router-dom": "^5.2.0",
"rollbar": "^2.19.3",
"typeface-roboto": "^0.0.75",
"webextension-polyfill": "^0.6.0"
"webextension-polyfill": "^0.8.0"
},
"devDependencies": {
"@babel/core": "^7.11.4",
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

132 changes: 131 additions & 1 deletion src/modules/background/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ export interface SaveCorrectionSuggestionMessage {
url: string;
}

export interface NavigationCommittedParams {
transitionType: browser.webNavigation.TransitionType;
tabId: number;
url: string;
}

const injectedTabs = new Set();
let streamingServiceScripts: browser.runtime.Manifest['content_scripts'] | null = null;

const init = async () => {
Shared.pageType = 'background';
await BrowserStorage.sync();
Expand All @@ -116,16 +125,45 @@ const init = async () => {
}
browser.tabs.onRemoved.addListener((tabId) => void onTabRemoved(tabId));
browser.storage.onChanged.addListener(onStorageChanged);
if (storage.options?.streamingServices) {
const scrobblerEnabled = (Object.entries(storage.options.streamingServices) as [
StreamingServiceId,
boolean
][]).some(
([streamingServiceId, value]) => value && streamingServices[streamingServiceId].hasScrobbler
);
if (scrobblerEnabled) {
addWebNavigationListener(storage.options);
}
}
if (storage.options?.grantCookies) {
addWebRequestListener();
}
browser.runtime.onMessage.addListener((onMessage as unknown) as browser.runtime.onMessageEvent);
};

const onTabUpdated = (_: unknown, __: unknown, tab: browser.tabs.Tab) => {
void injectScript(tab);
};

/**
* Checks if the tab that was closed was the tab that was scrobbling and, if that's the case, stops the scrobble.
*/
const onTabRemoved = async (tabId: number) => {
try {
/**
* Some single-page apps trigger the onTabRemoved event when navigating through pages,
* so we double check here to make sure that the tab was actually removed.
* If the tab was removed, this will throw an error.
*/
await browser.tabs.get(tabId);
return;
} catch (err) {
// Do nothing
}
if (injectedTabs.has(tabId)) {
injectedTabs.delete(tabId);
}
const { scrobblingTabId } = await BrowserStorage.get('scrobblingTabId');
if (tabId !== scrobblingTabId) {
return;
Expand All @@ -139,6 +177,32 @@ const onTabRemoved = async (tabId: number) => {
await BrowserAction.setInactiveIcon();
};

const injectScript = async (tab: Partial<browser.tabs.Tab>, reload = false) => {
if (
!streamingServiceScripts ||
tab.status !== 'complete' ||
!tab.id ||
!tab.url ||
!tab.url.startsWith('http') ||
(injectedTabs.has(tab.id) && !reload)
) {
return;
}
for (const { matches, js, run_at: runAt } of streamingServiceScripts) {
if (!js || !runAt) {
continue;
}
const isMatch = matches.find((match) => tab.url?.match(match));
if (isMatch) {
injectedTabs.add(tab.id);
for (const file of js) {
await browser.tabs.executeScript(tab.id, { file, runAt });
}
break;
}
}
};

const onStorageChanged = (
changes: browser.storage.ChangeDict,
areaName: browser.storage.StorageName
Expand All @@ -149,13 +213,79 @@ const onStorageChanged = (
if (!changes.options) {
return;
}
if ((changes.options.newValue as StorageValuesOptions)?.grantCookies) {
const newValue = changes.options.newValue as StorageValuesOptions | undefined;
if (!newValue) {
return;
}
if (newValue.streamingServices) {
const scrobblerEnabled = (Object.entries(newValue.streamingServices) as [
StreamingServiceId,
boolean
][]).some(
([streamingServiceId, value]) => value && streamingServices[streamingServiceId].hasScrobbler
);
if (scrobblerEnabled) {
addWebNavigationListener(newValue);
} else {
removeWebNavigationListener();
}
}
if (newValue.grantCookies) {
addWebRequestListener();
} else {
removeWebRequestListener();
}
};

const addWebNavigationListener = (options: StorageValuesOptions) => {
streamingServiceScripts = Object.values(streamingServices)
.filter((service) => options.streamingServices[service.id] && service.hasScrobbler)
.map((service) => ({
matches: service.hostPatterns.map((hostPattern) =>
hostPattern.replace(/^\*:\/\/\*\./, 'https?://(www.)?').replace(/\/\*$/, '')
),
js: ['js/lib/browser-polyfill.js', `js/${service.id}.js`],
run_at: 'document_idle',
}));
if (!browser.tabs.onUpdated.hasListener(onTabUpdated)) {
browser.tabs.onUpdated.addListener(onTabUpdated);
}
if (
!browser.webNavigation ||
browser.webNavigation.onCommitted.hasListener(onNavigationCommitted)
) {
return;
}
browser.webNavigation.onCommitted.addListener(onNavigationCommitted);
};

const removeWebNavigationListener = () => {
if (browser.tabs.onUpdated.hasListener(onTabUpdated)) {
browser.tabs.onUpdated.removeListener(onTabUpdated);
}
if (
!browser.webNavigation ||
!browser.webNavigation.onCommitted.hasListener(onNavigationCommitted)
) {
return;
}
browser.webNavigation.onCommitted.removeListener(onNavigationCommitted);
};

const onNavigationCommitted = ({ transitionType, tabId, url }: NavigationCommittedParams) => {
if (transitionType !== 'reload') {
return;
}
void injectScript(
{
status: 'complete',
id: tabId,
url: url,
},
true
);
};

const addWebRequestListener = () => {
if (
!browser.webRequest ||
Expand Down
10 changes: 8 additions & 2 deletions src/modules/options/OptionsApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,25 @@ export const OptionsApp: React.FC = () => {
for (const option of Object.values(options) as Option<keyof StorageValuesOptions>[]) {
addOptionToSave(optionsToSave, option);
}
const scrobblerEnabled = (Object.entries(optionsToSave.streamingServices) as [
StreamingServiceId,
boolean
][]).some(
([streamingServiceId, value]) => value && streamingServices[streamingServiceId].hasScrobbler
);
const permissionPromises: Promise<boolean>[] = [];
if (originsToAdd.length > 0) {
permissionPromises.push(
browser.permissions.request({
permissions: [],
permissions: scrobblerEnabled ? ['webNavigation'] : [],
origins: originsToAdd,
})
);
}
if (originsToRemove.length > 0) {
permissionPromises.push(
browser.permissions.remove({
permissions: [],
permissions: scrobblerEnabled ? [] : ['webNavigation'],
origins: originsToRemove,
})
);
Expand Down
9 changes: 1 addition & 8 deletions webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,6 @@ const getWebpackConfig = (env: Environment) => {
};

const getManifest = (config: Config, browserName: string): string => {
const streamingServiceScripts: Manifest['content_scripts'] = Object.values(streamingServices)
.filter((service) => service.hasScrobbler)
.map((service) => ({
js: ['js/lib/browser-polyfill.js', `js/${service.id}.js`],
matches: service.hostPatterns,
run_at: 'document_idle',
}));
const manifest: Manifest = {
manifest_version: 2,
name: 'Universal Trakt Scrobbler',
Expand All @@ -203,12 +196,12 @@ const getManifest = (config: Config, browserName: string): string => {
matches: ['*://*.trakt.tv/apps*'],
run_at: 'document_start',
},
...streamingServiceScripts,
],
default_locale: 'en',
optional_permissions: [
'cookies',
'notifications',
'webNavigation',
'webRequest',
'webRequestBlocking',
'*://api.rollbar.com/*',
Expand Down

0 comments on commit fdf5abd

Please sign in to comment.