-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
#579: Deduplicate injection and make it more straightforward #654
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,12 +17,15 @@ | |
|
||
import { isBackgroundPage } from "webext-detect-page"; | ||
import { browser } from "webextension-polyfill-ts"; | ||
import { getAdditionalPermissions } from "webext-additional-permissions"; | ||
import { patternToRegex } from "webext-patterns"; | ||
import * as contentScriptProtocol from "@/contentScript/devTools"; | ||
import { sleep } from "@/utils"; | ||
import { isErrorObject, sleep } from "@/utils"; | ||
|
||
export type Target = { | ||
tabId: number; | ||
frameId: number; | ||
url: string; | ||
}; | ||
|
||
export async function testTabPermissions(target: Target): Promise<boolean> { | ||
|
@@ -41,11 +44,9 @@ export async function testTabPermissions(target: Target): Promise<boolean> { | |
runAt: "document_start", | ||
}); | ||
return true; | ||
} catch (error) { | ||
if ((error.message as string)?.includes("Cannot access contents")) { | ||
// no permissions | ||
} else { | ||
console.warn("testTabPermissions failed", { reason: error }); | ||
} catch (error: unknown) { | ||
if (!isErrorObject(error) || !/Cannot access/.test(error.message)) { | ||
console.warn("testTabPermissions failed", { error }); | ||
} | ||
} | ||
return false; | ||
|
@@ -66,36 +67,68 @@ export async function waitReady( | |
throw new Error(`contentScript not ready in ${maxWaitMillis}ms`); | ||
} | ||
|
||
/** Checks whether a URL has permanent permissions and therefore whether `webext-dynamic-content-scripts` already registered the scripts */ | ||
export async function isContentScriptRegistered(url: string): Promise<boolean> { | ||
const { permissions } = await getAdditionalPermissions({ | ||
strictOrigins: false, | ||
}); | ||
|
||
return patternToRegex(...permissions).test(url); | ||
} | ||
|
||
/** Checks whether a tab has the activeTab permission to inject scripts and CSS */ | ||
export async function isActiveTab( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a confusing method name. At first glance, it looks as if it should be whether the tab is the active one in the browser. I would recommend hasActiveTabPermission, as it's wordier, but instant to understand Additionally, this will also work if the page has permanent permissions How does this method relate to testPagePermissions? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It probably replaces it. The PR is still a WIP, trying things out |
||
tabId: number, | ||
frameId = 0 | ||
): Promise<boolean> { | ||
return browser.tabs.insertCSS(tabId, { code: "@media {}", frameId }).then( | ||
() => true, | ||
() => false | ||
); | ||
} | ||
|
||
/** | ||
* Inject a contentScript into a page if the contentScript is not already available on the page | ||
* @param target the tab frame to inject the contentScript into | ||
* @param file the contentScript file | ||
* @return true if the content script was injected | ||
*/ | ||
export async function injectContentScript( | ||
target: Target, | ||
file = "contentScript.js" | ||
): Promise<boolean> { | ||
export async function injectContentScript(target: Target): Promise<boolean> { | ||
if (!isBackgroundPage()) { | ||
throw new Error( | ||
"injectContentScript can only be called from the background page" | ||
); | ||
} | ||
|
||
// Already has permanent access | ||
if (await isContentScriptRegistered(target.url)) { | ||
return; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why isn’t this caught by TS? 🤔 |
||
} | ||
|
||
// Has temporary access | ||
if (!(await isActiveTab(target.tabId, target.frameId))) { | ||
console.debug( | ||
`Skipping content script injection because no activeTab permissions for tab: ${target.tabId}` | ||
); | ||
return; | ||
} | ||
|
||
const { installed } = await contentScriptProtocol.isInstalled(target); | ||
|
||
if (!installed) { | ||
console.debug( | ||
`Injecting devtools contentScript for tab ${target.tabId}, frame ${ | ||
target.frameId ?? 0 | ||
}: ${file}` | ||
}` | ||
); | ||
|
||
// inject in the top-level frame | ||
console.log("injectContentScript"); | ||
|
||
await browser.tabs.executeScript(target.tabId, { | ||
frameId: target.frameId ?? 0, | ||
allFrames: false, | ||
file, | ||
file: "contentScript.js", | ||
runAt: "document_end", | ||
}); | ||
|
||
|
@@ -109,3 +142,22 @@ export async function injectContentScript( | |
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Some pages are off-limits to extension. This function can find out if an error is due to this limitation. | ||
* | ||
* Example error messages: | ||
* Cannot access a chrome:// URL | ||
* Cannot access a chrome-extension:// URL of different extension | ||
* Cannot access contents of url "chrome-extension://mpjjildhmpddojocokjkgmlkkkfjnepo/options.html#/". Extension manifest must request permission to access this host. | ||
* The extensions gallery cannot be scripted. | ||
* | ||
* @param error | ||
* @returns | ||
*/ | ||
export async function isPrivatePageError(error: unknown): Promise<boolean> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this should be async? |
||
return ( | ||
isErrorObject(error) && | ||
/cannot be scripted|(chrome|about|extension):\/\//.test(error.message) | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to pass the URL around, when available, but I'm having trouble following the flow of this lifted function so I thought it's better to ask you.
Is it possible to get the URL of the tab here? It's used to exclude pages that have permanent permissions, see the new
isContentScriptRegistered
function.Currently this is undefined
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the "lift" functions, there's always two sides to it, the calling function and the listening function. For this one, the calling side is here: http://github.com/pixiebrix/pixiebrix-extension/blob/8302a92f9ed90a4e48624a62e119e193fe2b278c/src/background/devtools/internal.ts#L184-L184
With that method setting the "meta" properties of the request here: http://github.com/pixiebrix/pixiebrix-extension/blob/8302a92f9ed90a4e48624a62e119e193fe2b278c/src/background/devtools/external.ts#L89-L89
What's interesting about that is it actually leaves off one of the expected fields: http://github.com/pixiebrix/pixiebrix-extension/blob/8302a92f9ed90a4e48624a62e119e193fe2b278c/src/background/devtools/contract.ts#L56-L56
I'm not sure if URL is available at that call site, as I don't see it on the type declaration for
browser.devtools.inspectedWindow
Historic context: for the messaging protocol, I just copied the Flux Standard Actions approach used by Redux: https://github.com/redux-utilities/flux-standard-action
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure passing the URL around simplifies things due to the additional book keeping. I guess one benefit it we can sort of enforce this: w3c/webextensions#8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I’ll look into it again today. The types are indeed failing me here, likely due to to an assertion in the callBackground call. These kind of functions have complex types so it’s hard/impossible to get them right. I regularly run into TS limitations when trying to type type-preserving functions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah the URL call was actually just in the temporary injection function, and I think it was available there, but as I’m working to have a “single injector” I moved it here without luck.
As for that issue, the only way to avoid it for us would be to do a URL check in the pre-contentScript we had discussed before