aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ext/js/accessibility/accessibility-controller.js17
-rw-r--r--ext/js/background/backend.js8
-rw-r--r--ext/js/background/script-manager.js508
-rw-r--r--types/ext/script-manager.d.ts14
4 files changed, 122 insertions, 425 deletions
diff --git a/ext/js/accessibility/accessibility-controller.js b/ext/js/accessibility/accessibility-controller.js
index 8250b369..2b352948 100644
--- a/ext/js/accessibility/accessibility-controller.js
+++ b/ext/js/accessibility/accessibility-controller.js
@@ -16,19 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+import {isContentScriptRegistered, registerContentScript, unregisterContentScript} from '../background/script-manager.js';
import {log} from '../core.js';
/**
* This class controls the registration of accessibility handlers.
*/
export class AccessibilityController {
- /**
- * Creates a new instance.
- * @param {import('../background/script-manager.js').ScriptManager} scriptManager An instance of the `ScriptManager` class.
- */
- constructor(scriptManager) {
- /** @type {import('../background/script-manager.js').ScriptManager} */
- this._scriptManager = scriptManager;
+ constructor() {
/** @type {?import('core').TokenObject} */
this._updateGoogleDocsAccessibilityToken = null;
/** @type {?Promise<void>} */
@@ -90,19 +85,17 @@ export class AccessibilityController {
const id = 'googleDocsAccessibility';
try {
if (forceGoogleDocsHtmlRenderingAny) {
- if (await this._scriptManager.isContentScriptRegistered(id)) { return; }
+ if (await isContentScriptRegistered(id)) { return; }
/** @type {import('script-manager').RegistrationDetails} */
const details = {
allFrames: true,
- matchAboutBlank: true,
matches: ['*://docs.google.com/*'],
- urlMatches: '^[^:]*://docs\\.google\\.com/[\\w\\W]*$',
runAt: 'document_start',
js: ['js/accessibility/google-docs.js']
};
- await this._scriptManager.registerContentScript(id, details);
+ await registerContentScript(id, details);
} else {
- await this._scriptManager.unregisterContentScript(id);
+ await unregisterContentScript(id);
}
} catch (e) {
log.error(e);
diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js
index 5ef3c3be..0604fe8b 100644
--- a/ext/js/background/backend.js
+++ b/ext/js/background/backend.js
@@ -40,7 +40,7 @@ import {MediaUtil} from '../media/media-util.js';
import {ClipboardReaderProxy, DictionaryDatabaseProxy, OffscreenProxy, TranslatorProxy} from './offscreen-proxy.js';
import {ProfileConditionsUtil} from './profile-conditions-util.js';
import {RequestBuilder} from './request-builder.js';
-import {ScriptManager} from './script-manager.js';
+import {injectStylesheet} from './script-manager.js';
/**
* This class controls the core logic of the extension, including API calls
@@ -110,10 +110,8 @@ export class Backend {
});
/** @type {OptionsUtil} */
this._optionsUtil = new OptionsUtil();
- /** @type {ScriptManager} */
- this._scriptManager = new ScriptManager();
/** @type {AccessibilityController} */
- this._accessibilityController = new AccessibilityController(this._scriptManager);
+ this._accessibilityController = new AccessibilityController();
/** @type {?number} */
this._searchPopupTabId = null;
@@ -650,7 +648,7 @@ export class Backend {
async _onApiInjectStylesheet({type, value}, sender) {
const {frameId, tab} = sender;
if (typeof tab !== 'object' || tab === null || typeof tab.id !== 'number') { throw new Error('Invalid tab'); }
- return await this._scriptManager.injectStylesheet(type, value, tab.id, frameId, false);
+ return await injectStylesheet(type, value, tab.id, frameId, false);
}
/** @type {import('api').ApiHandler<'getStylesheetContent'>} */
diff --git a/ext/js/background/script-manager.js b/ext/js/background/script-manager.js
index 98f67bb0..1142121f 100644
--- a/ext/js/background/script-manager.js
+++ b/ext/js/background/script-manager.js
@@ -16,419 +16,139 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {isObject} from '../core.js';
-
/**
- * This class is used to manage script injection into content tabs.
+ * Injects a stylesheet into a tab.
+ * @param {'file'|'code'} type The type of content to inject; either 'file' or 'code'.
+ * @param {string} content The content to inject.
+ * - If type is `'file'`, this argument should be a path to a file.
+ * - If type is `'code'`, this argument should be the CSS content.
+ * @param {number} tabId The id of the tab to inject into.
+ * @param {number|undefined} frameId The id of the frame to inject into.
+ * @param {boolean} allFrames Whether or not the stylesheet should be injected into all frames.
+ * @returns {Promise<void>}
*/
-export class ScriptManager {
- /**
- * Creates a new instance of the class.
- */
- constructor() {
- /** @type {Map<string, ?browser.contentScripts.RegisteredContentScript>} */
- this._contentScriptRegistrations = new Map();
- }
-
- /**
- * Injects a stylesheet into a tab.
- * @param {'file'|'code'} type The type of content to inject; either 'file' or 'code'.
- * @param {string} content The content to inject.
- * If type is 'file', this argument should be a path to a file.
- * If type is 'code', this argument should be the CSS content.
- * @param {number} tabId The id of the tab to inject into.
- * @param {number|undefined} frameId The id of the frame to inject into.
- * @param {boolean} allFrames Whether or not the stylesheet should be injected into all frames.
- * @returns {Promise<void>}
- */
- injectStylesheet(type, content, tabId, frameId, allFrames) {
- if (isObject(chrome.scripting) && typeof chrome.scripting.insertCSS === 'function') {
- return this._injectStylesheetMV3(type, content, tabId, frameId, allFrames);
- } else {
- return Promise.reject(new Error('Stylesheet injection not supported'));
- }
- }
-
- /**
- * Injects a script into a tab.
- * @param {string} file The path to a file to inject.
- * @param {number} tabId The id of the tab to inject into.
- * @param {number|undefined} frameId The id of the frame to inject into.
- * @param {boolean} allFrames Whether or not the script should be injected into all frames.
- * @returns {Promise<{frameId: number|undefined, result: unknown}>} The id of the frame and the result of the script injection.
- */
- injectScript(file, tabId, frameId, allFrames) {
- if (isObject(chrome.scripting) && typeof chrome.scripting.executeScript === 'function') {
- return this._injectScriptMV3(file, tabId, frameId, allFrames);
- } else {
- return Promise.reject(new Error('Script injection not supported'));
- }
- }
-
- /**
- * Checks whether or not a content script is registered.
- * @param {string} id The identifier used with a call to `registerContentScript`.
- * @returns {Promise<boolean>} `true` if a script is registered, `false` otherwise.
- */
- async isContentScriptRegistered(id) {
- if (this._contentScriptRegistrations.has(id)) {
- return true;
- }
- if (isObject(chrome.scripting) && typeof chrome.scripting.getRegisteredContentScripts === 'function') {
- const scripts = await new Promise((resolve, reject) => {
- chrome.scripting.getRegisteredContentScripts({ids: [id]}, (result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(result);
- }
- });
- });
- for (const script of scripts) {
- if (script.id === id) {
- return true;
- }
+export function injectStylesheet(type, content, tabId, frameId, allFrames) {
+ return new Promise((resolve, reject) => {
+ /** @type {chrome.scripting.InjectionTarget} */
+ const target = {
+ tabId,
+ allFrames
+ };
+ /** @type {chrome.scripting.CSSInjection} */
+ const details = (
+ type === 'file' ?
+ {origin: 'AUTHOR', files: [content], target} :
+ {origin: 'USER', css: content, target}
+ );
+ if (!allFrames && typeof frameId === 'number') {
+ details.target.frameIds = [frameId];
+ }
+ chrome.scripting.insertCSS(details, () => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve();
}
- }
- return false;
- }
-
- /**
- * Registers a dynamic content script.
- * Note: if the fallback handler is used and the 'webNavigation' permission isn't granted,
- * there is a possibility that the script can be injected more than once due to the events used.
- * Therefore, a reentrant check may need to be performed by the content script.
- * @param {string} id A unique identifier for the registration.
- * @param {import('script-manager').RegistrationDetails} details The script registration details.
- * @throws An error is thrown if the id is already in use.
- */
- async registerContentScript(id, details) {
- if (await this.isContentScriptRegistered(id)) {
- throw new Error('Registration already exists');
- }
-
- if (isObject(chrome.scripting) && typeof chrome.scripting.registerContentScripts === 'function') {
- const details2 = this._createContentScriptRegistrationOptionsChrome(details, id);
- await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
- chrome.scripting.registerContentScripts([details2], () => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve();
- }
- });
- }));
- this._contentScriptRegistrations.set(id, null);
- return;
- }
-
- // Fallback
- this._registerContentScriptFallback(id, details);
- }
+ });
+ });
+}
- /**
- * Unregisters a previously registered content script.
- * @param {string} id The identifier passed to a previous call to `registerContentScript`.
- * @returns {Promise<boolean>} `true` if the content script was unregistered, `false` otherwise.
- */
- async unregisterContentScript(id) {
- if (isObject(chrome.scripting) && typeof chrome.scripting.unregisterContentScripts === 'function') {
- this._contentScriptRegistrations.delete(id);
- try {
- await this._unregisterContentScriptMV3(id);
- return true;
- } catch (e) {
- return false;
+/**
+ * Checks whether or not a content script is registered.
+ * @param {string} id The identifier used with a call to `registerContentScript`.
+ * @returns {Promise<boolean>} `true` if a script is registered, `false` otherwise.
+ */
+export async function isContentScriptRegistered(id) {
+ const scripts = await new Promise((resolve, reject) => {
+ chrome.scripting.getRegisteredContentScripts({ids: [id]}, (result) => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve(result);
}
+ });
+ });
+ for (const script of scripts) {
+ if (script.id === id) {
+ return true;
}
-
- // Fallback
- const registration = this._contentScriptRegistrations.get(id);
- if (typeof registration === 'undefined') { return false; }
- this._contentScriptRegistrations.delete(id);
- if (registration !== null && typeof registration.unregister === 'function') {
- await registration.unregister();
- }
- return true;
}
+ return false;
+}
- /**
- * Gets the optional permissions required to register a content script.
- * @returns {string[]} An array of the required permissions, which may be empty.
- */
- getRequiredContentScriptRegistrationPermissions() {
- if (isObject(chrome.scripting) && typeof chrome.scripting.registerContentScripts === 'function') {
- return [];
- }
-
- // Fallback
- return ['webNavigation'];
+/**
+ * Registers a dynamic content script.
+ * Note: if the fallback handler is used and the 'webNavigation' permission isn't granted,
+ * there is a possibility that the script can be injected more than once due to the events used.
+ * Therefore, a reentrant check may need to be performed by the content script.
+ * @param {string} id A unique identifier for the registration.
+ * @param {import('script-manager').RegistrationDetails} details The script registration details.
+ * @throws An error is thrown if the id is already in use.
+ */
+export async function registerContentScript(id, details) {
+ if (await isContentScriptRegistered(id)) {
+ throw new Error('Registration already exists');
}
- // Private
-
- /**
- * @param {'file'|'code'} type
- * @param {string} content
- * @param {number} tabId
- * @param {number|undefined} frameId
- * @param {boolean} allFrames
- * @returns {Promise<void>}
- */
- _injectStylesheetMV3(type, content, tabId, frameId, allFrames) {
- return new Promise((resolve, reject) => {
- /** @type {chrome.scripting.InjectionTarget} */
- const target = {
- tabId,
- allFrames
- };
- /** @type {chrome.scripting.CSSInjection} */
- const details = (
- type === 'file' ?
- {origin: 'AUTHOR', files: [content], target} :
- {origin: 'USER', css: content, target}
- );
- if (!allFrames && typeof frameId === 'number') {
- details.target.frameIds = [frameId];
+ const details2 = createContentScriptRegistrationOptions(details, id);
+ await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
+ chrome.scripting.registerContentScripts([details2], () => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve();
}
- chrome.scripting.insertCSS(details, () => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve();
- }
- });
});
- }
+ }));
+}
- /**
- * @param {string} file
- * @param {number} tabId
- * @param {number|undefined} frameId
- * @param {boolean} allFrames
- * @returns {Promise<{frameId: number|undefined, result: unknown}>} The id of the frame and the result of the script injection.
- */
- _injectScriptMV3(file, tabId, frameId, allFrames) {
- return new Promise((resolve, reject) => {
- /** @type {chrome.scripting.ScriptInjection<unknown[], unknown>} */
- const details = {
- injectImmediately: true,
- files: [file],
- target: {tabId, allFrames}
- };
- if (!allFrames && typeof frameId === 'number') {
- details.target.frameIds = [frameId];
+/**
+ * Unregisters a previously registered content script.
+ * @param {string} id The identifier passed to a previous call to `registerContentScript`.
+ * @returns {Promise<void>}
+ */
+export async function unregisterContentScript(id) {
+ return new Promise((resolve, reject) => {
+ chrome.scripting.unregisterContentScripts({ids: [id]}, () => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve();
}
- chrome.scripting.executeScript(details, (results) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- const {frameId: frameId2, result} = results[0];
- resolve({frameId: frameId2, result});
- }
- });
});
- }
-
- /**
- * @param {string} id
- * @returns {Promise<void>}
- */
- _unregisterContentScriptMV3(id) {
- return new Promise((resolve, reject) => {
- chrome.scripting.unregisterContentScripts({ids: [id]}, () => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve();
- }
- });
- });
- }
-
- /**
- * @param {import('script-manager').RegistrationDetails} details
- * @returns {browser.contentScripts.RegisteredContentScriptOptions}
- */
- _createContentScriptRegistrationOptionsFirefox(details) {
- const {css, js, matchAboutBlank} = details;
- /** @type {browser.contentScripts.RegisteredContentScriptOptions} */
- const options = {};
- if (typeof matchAboutBlank !== 'undefined') {
- options.matchAboutBlank = matchAboutBlank;
- }
- if (Array.isArray(css)) {
- options.css = css.map((file) => ({file}));
- }
- if (Array.isArray(js)) {
- options.js = js.map((file) => ({file}));
- }
- this._initializeContentScriptRegistrationOptionsGeneric(details, options);
- return options;
- }
-
- /**
- * @param {import('script-manager').RegistrationDetails} details
- * @param {string} id
- * @returns {chrome.scripting.RegisteredContentScript}
- */
- _createContentScriptRegistrationOptionsChrome(details, id) {
- const {css, js} = details;
- /** @type {chrome.scripting.RegisteredContentScript} */
- const options = {
- id: id,
- persistAcrossSessions: true
- };
- if (Array.isArray(css)) {
- options.css = [...css];
- }
- if (Array.isArray(js)) {
- options.js = [...js];
- }
- this._initializeContentScriptRegistrationOptionsGeneric(details, options);
- return options;
- }
+ });
+}
- /**
- * @param {import('script-manager').RegistrationDetails} details
- * @param {chrome.scripting.RegisteredContentScript|browser.contentScripts.RegisteredContentScriptOptions} options
- */
- _initializeContentScriptRegistrationOptionsGeneric(details, options) {
- const {allFrames, excludeMatches, matches, runAt} = details;
- if (typeof allFrames !== 'undefined') {
- options.allFrames = allFrames;
- }
- if (Array.isArray(excludeMatches)) {
- options.excludeMatches = [...excludeMatches];
- }
- if (Array.isArray(matches)) {
- options.matches = [...matches];
- }
- if (typeof runAt !== 'undefined') {
- options.runAt = runAt;
- }
+/**
+ * @param {import('script-manager').RegistrationDetails} details
+ * @param {string} id
+ * @returns {chrome.scripting.RegisteredContentScript}
+ */
+function createContentScriptRegistrationOptions(details, id) {
+ const {css, js, allFrames, matches, runAt} = details;
+ /** @type {chrome.scripting.RegisteredContentScript} */
+ const options = {
+ id: id,
+ persistAcrossSessions: true
+ };
+ if (Array.isArray(css)) {
+ options.css = [...css];
}
-
- /**
- * @param {string[]} array
- * @param {boolean} firefoxConvention
- * @returns {string[]|browser.extensionTypes.ExtensionFileOrCode[]}
- */
- _convertFileArray(array, firefoxConvention) {
- return firefoxConvention ? array.map((file) => ({file})) : [...array];
+ if (Array.isArray(js)) {
+ options.js = [...js];
}
-
- /**
- * @param {string} id
- * @param {import('script-manager').RegistrationDetails} details
- */
- _registerContentScriptFallback(id, details) {
- const {allFrames, css, js, matchAboutBlank, runAt, urlMatches} = details;
- /** @type {import('script-manager').ContentScriptInjectionDetails} */
- const details2 = {allFrames, css, js, matchAboutBlank, runAt, urlRegex: /** @type {?RegExp} */ (null)};
- /** @type {() => Promise<void>} */
- let unregister;
- const webNavigationEvent = this._getWebNavigationEvent(runAt);
- if (typeof webNavigationEvent === 'object' && webNavigationEvent !== null) {
- /**
- * @param {chrome.webNavigation.WebNavigationFramedCallbackDetails} details
- */
- const onTabCommitted = ({url, tabId, frameId}) => {
- this._injectContentScript(true, details2, null, url, tabId, frameId);
- };
- const filter = {url: [{urlMatches}]};
- webNavigationEvent.addListener(onTabCommitted, filter);
- unregister = async () => webNavigationEvent.removeListener(onTabCommitted);
- } else {
- /**
- * @param {number} tabId
- * @param {chrome.tabs.TabChangeInfo} changeInfo
- * @param {chrome.tabs.Tab} tab
- */
- const onTabUpdated = (tabId, {status}, {url}) => {
- if (typeof status === 'string' && typeof url === 'string') {
- this._injectContentScript(false, details2, status, url, tabId, void 0);
- }
- };
- try {
- // Firefox
- /** @type {browser.tabs.UpdateFilter} */
- const extraParameters = {urls: [urlMatches], properties: ['status']};
- browser.tabs.onUpdated.addListener(
- /** @type {(tabId: number, changeInfo: browser.tabs._OnUpdatedChangeInfo, tab: browser.tabs.Tab) => void} */ (onTabUpdated),
- extraParameters
- );
- } catch (e) {
- // Chrome
- details2.urlRegex = new RegExp(urlMatches);
- chrome.tabs.onUpdated.addListener(onTabUpdated);
- }
- unregister = async () => chrome.tabs.onUpdated.removeListener(onTabUpdated);
- }
- this._contentScriptRegistrations.set(id, {unregister});
+ if (typeof allFrames !== 'undefined') {
+ options.allFrames = allFrames;
}
-
- /**
- * @param {import('script-manager').RunAt} runAt
- * @returns {?(chrome.webNavigation.WebNavigationFramedEvent|chrome.webNavigation.WebNavigationTransitionalEvent)}
- */
- _getWebNavigationEvent(runAt) {
- const {webNavigation} = chrome;
- if (!isObject(webNavigation)) { return null; }
- switch (runAt) {
- case 'document_start':
- return webNavigation.onCommitted;
- case 'document_end':
- return webNavigation.onDOMContentLoaded;
- default: // 'document_idle':
- return webNavigation.onCompleted;
- }
+ if (Array.isArray(matches)) {
+ options.matches = [...matches];
}
-
- /**
- * @param {boolean} isWebNavigation
- * @param {import('script-manager').ContentScriptInjectionDetails} details
- * @param {?string} status
- * @param {string} url
- * @param {number} tabId
- * @param {number|undefined} frameId
- */
- async _injectContentScript(isWebNavigation, details, status, url, tabId, frameId) {
- const {urlRegex} = details;
- if (urlRegex !== null && !urlRegex.test(url)) { return; }
-
- let {allFrames, css, js, runAt} = details;
-
- if (isWebNavigation) {
- if (allFrames) {
- allFrames = false;
- } else {
- if (frameId !== 0) { return; }
- }
- } else {
- if (runAt === 'document_start') {
- if (status !== 'loading') { return; }
- } else { // 'document_end', 'document_idle'
- if (status !== 'complete') { return; }
- }
- }
-
- const promises = [];
- if (Array.isArray(css)) {
- for (const file of css) {
- promises.push(this.injectStylesheet('file', file, tabId, frameId, allFrames));
- }
- }
- if (Array.isArray(js)) {
- for (const file of js) {
- promises.push(this.injectScript(file, tabId, frameId, allFrames));
- }
- }
- await Promise.all(promises);
+ if (typeof runAt !== 'undefined') {
+ options.runAt = runAt;
}
+ return options;
}
diff --git a/types/ext/script-manager.d.ts b/types/ext/script-manager.d.ts
index 57b9ee06..66a5c20f 100644
--- a/types/ext/script-manager.d.ts
+++ b/types/ext/script-manager.d.ts
@@ -21,26 +21,12 @@ export type RunAt = 'document_start' | 'document_end' | 'document_idle';
export type RegistrationDetails = {
/** Same as `matches` in the `content_scripts` manifest key. */
matches: string[];
-
- /** Regex match pattern to use as a fallback when native content script registration isn't supported. */
- /** Should be equivalent to `matches`. */
- urlMatches: string;
-
/** Same as `run_at` in the `content_scripts` manifest key. */
runAt: RunAt;
-
- /** Same as `exclude_matches` in the `content_scripts` manifest key. */
- excludeMatches?: string[];
-
- /** Same as `match_about_blank` in the `content_scripts` manifest key. */
- matchAboutBlank: boolean;
-
/** Same as `all_frames` in the `content_scripts` manifest key. */
allFrames: boolean;
-
/** List of CSS paths. */
css?: string[];
-
/** List of script paths. */
js?: string[];
};