123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- /* eslint-disable max-classes-per-file */
- /* eslint-disable prefer-rest-params */
- import objectPath from 'object-path';
- import Browser from 'webextension-polyfill';
- import { MessageListener } from '@/utils/message';
- import BrowserAPIEventHandler from './BrowserAPIEventHandler';
- import { browserAPIMap } from './browser-api-map';
- /**
- * @typedef {Object} ScriptInjectTarget
- * @property {number} tabId
- * @property {number=} frameId
- * @property {boolean=} allFrames
- */
- // Maybe there's a better way?
- export const IS_BROWSER_API_AVAILABLE = 'tabs' in Browser;
- function sendBrowserApiMessage(name, ...args) {
- return MessageListener.sendMessage(
- 'browser-api',
- {
- name,
- args,
- },
- 'background'
- );
- }
- class BrowserConentScript {
- /**
- * Inject content script into targeted tab
- * @param {Object} script
- * @param {ScriptInjectTarget} script.target
- * @param {string} script.file
- * @param {boolean=} script.injectImmediately
- * @param {(boolean|{timeoutMs?: number, maxTry?: number, messageId?: string})=} script.waitUntilInjected
- * @returns {Promise<boolean>}
- */
- static async inject({ file, target, injectImmediately, waitUntilInjected }) {
- if (!IS_BROWSER_API_AVAILABLE) {
- return sendBrowserApiMessage('contentScript.inject', ...arguments);
- }
- const frameId =
- Object.hasOwn(target, 'frameId') && !target.allFrames
- ? target.frameId
- : undefined;
- if (Browser.tabs.injectContentScript) {
- await Browser.tabs.executeScript(target.tabId, {
- file,
- frameId,
- allFrames: target.allFrames,
- });
- } else {
- await Browser.scripting.executeScript({
- target: {
- tabId: target.tabId,
- allFrames: target.allFrames,
- frameIds: typeof frameId === 'number' ? [frameId] : undefined,
- },
- files: [file],
- injectImmediately,
- });
- }
- if (!waitUntilInjected) return true;
- const maxTryCount = waitUntilInjected.maxTry ?? 3;
- const timeoutMs = waitUntilInjected.timeoutMs ?? 1000;
- let tryCount = 0;
- return new Promise((resolve) => {
- const checkIfInjected = async () => {
- try {
- if (tryCount > maxTryCount) {
- resolve(false);
- return;
- }
- tryCount += 1;
- const isInjected = await this.isContentScriptInjected(
- target,
- waitUntilInjected.messageId
- );
- if (isInjected) {
- resolve(true);
- return;
- }
- setTimeout(() => checkIfInjected(), timeoutMs);
- } catch (error) {
- console.error(error);
- setTimeout(() => checkIfInjected(), timeoutMs);
- }
- };
- checkIfInjected();
- });
- }
- /**
- * Check if content script injected
- * @param {ScriptInjectTarget} target
- * @param {string=} messageId
- */
- static async isInjected({ tabId, allFrames, frameId }, messageId) {
- if (!IS_BROWSER_API_AVAILABLE) {
- return sendBrowserApiMessage('contentScript.isInjected', ...arguments);
- }
- try {
- await Browser.tabs.sendMessage(
- tabId,
- { type: messageId || 'content-script-exists' },
- { frameId: allFrames ? undefined : frameId }
- );
- return true;
- } catch (error) {
- return false;
- }
- }
- }
- class BrowserAPIService {
- /**
- * Handle runtime message that send by BrowserAPIService when API is not available
- * @param {{ name: string; args: any[] }} payload;
- */
- static runtimeMessageHandler({ args, name }) {
- const apiHandler = objectPath.get(this, name);
- if (!apiHandler) throw new Error(`"${name}" is invalid method`);
- return apiHandler(...args);
- }
- static runtime = Browser.runtime;
- /** @type {typeof Browser.tabs} */
- static tabs;
- /** @type {typeof Browser.proxy} */
- static proxy;
- /** @type {typeof Browser.storage} */
- static storage;
- /** @type {typeof Browser.windows} */
- static windows;
- /** @type {typeof chrome.debugger} */
- static debugger;
- /** @type {typeof Browser.webNavigation} */
- static webNavigation;
- /** @type {typeof Browser.permissions} */
- static permissions;
- /** @type {typeof Browser.downloads} */
- static downloads;
- /** @type {typeof Browser.notifications} */
- static notifications;
- /** @type {typeof Browser.browserAction} */
- static browserAction;
- /** @type {typeof Browser.extension} */
- static extension;
- static contentScript = BrowserConentScript;
- }
- (() => {
- browserAPIMap.forEach((item) => {
- let value;
- if (IS_BROWSER_API_AVAILABLE) {
- value = item.api();
- } else {
- value = item.isEvent
- ? BrowserAPIEventHandler.instance.createEventListener(item.path)
- : (...args) => sendBrowserApiMessage(item.path, ...args);
- }
- objectPath.set(BrowserAPIService, item.path, value);
- });
- })();
- export default BrowserAPIService;
|