From 72c49d731b0fef8b32c6f59eaba38b13c116263e Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Thu, 8 Aug 2024 19:50:43 +0100 Subject: [PATCH 1/2] feat[react-devtools]: support Manifest v3 for Firefox extension --- .../firefox/manifest.json | 40 +++++---- .../dynamicallyInjectContentScripts.js | 90 +++++++------------ .../src/background/executeScript.js | 39 -------- .../background/setExtensionIconAndPopup.js | 6 +- .../src/background/tabsManager.js | 24 ++--- .../src/contentScripts/prepareInjection.js | 40 --------- .../react-devtools-shared/babel.config.js | 2 +- 7 files changed, 66 insertions(+), 175 deletions(-) diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index ffa48634e0e0d..3c2d417d585af 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -1,12 +1,12 @@ { - "manifest_version": 2, + "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Firefox Developer Tools.", "version": "5.3.1", - "applications": { + "browser_specific_settings": { "gecko": { "id": "@react-devtools", - "strict_min_version": "102.0" + "strict_min_version": "128.0" } }, "icons": { @@ -15,22 +15,32 @@ "48": "icons/48-production.png", "128": "icons/128-production.png" }, - "browser_action": { + "action": { "default_icon": { "16": "icons/16-disabled.png", "32": "icons/32-disabled.png", "48": "icons/48-disabled.png", "128": "icons/128-disabled.png" }, - "default_popup": "popups/disabled.html", - "browser_style": true + "default_popup": "popups/disabled.html" }, "devtools_page": "main.html", - "content_security_policy": "script-src 'self' 'unsafe-eval' blob:; object-src 'self'", + "content_security_policy": { + "extension_pages": "script-src 'self'; object-src 'self'" + }, "web_accessible_resources": [ - "main.html", - "panel.html", - "build/*.js" + { + "resources": [ + "main.html", + "panel.html", + "build/*.js", + "build/*.js.map" + ], + "matches": [ + "" + ], + "extension_ids": [] + } ], "background": { "scripts": [ @@ -38,12 +48,10 @@ ] }, "permissions": [ - "file:///*", - "http://*/*", - "https://*/*", - "clipboardWrite", - "scripting", - "devtools" + "scripting" + ], + "host_permissions": [ + "" ], "content_scripts": [ { diff --git a/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js b/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js index e19030457a89c..b1888b4e7cc59 100644 --- a/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js +++ b/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js @@ -1,58 +1,39 @@ /* global chrome */ -// Firefox doesn't support ExecutionWorld.MAIN yet -// equivalent logic for Firefox is in prepareInjection.js -const contentScriptsToInject = __IS_FIREFOX__ - ? [ - { - id: '@react-devtools/proxy', - js: ['build/proxy.js'], - matches: [''], - persistAcrossSessions: true, - runAt: 'document_end', - }, - { - id: '@react-devtools/file-fetcher', - js: ['build/fileFetcher.js'], - matches: [''], - persistAcrossSessions: true, - runAt: 'document_end', - }, - ] - : [ - { - id: '@react-devtools/proxy', - js: ['build/proxy.js'], - matches: [''], - persistAcrossSessions: true, - runAt: 'document_end', - world: chrome.scripting.ExecutionWorld.ISOLATED, - }, - { - id: '@react-devtools/file-fetcher', - js: ['build/fileFetcher.js'], - matches: [''], - persistAcrossSessions: true, - runAt: 'document_end', - world: chrome.scripting.ExecutionWorld.ISOLATED, - }, - { - id: '@react-devtools/hook', - js: ['build/installHook.js'], - matches: [''], - persistAcrossSessions: true, - runAt: 'document_start', - world: chrome.scripting.ExecutionWorld.MAIN, - }, - { - id: '@react-devtools/renderer', - js: ['build/renderer.js'], - matches: [''], - persistAcrossSessions: true, - runAt: 'document_start', - world: chrome.scripting.ExecutionWorld.MAIN, - }, - ]; +const contentScriptsToInject = [ + { + id: '@react-devtools/proxy', + js: ['build/proxy.js'], + matches: [''], + persistAcrossSessions: true, + runAt: 'document_end', + world: chrome.scripting.ExecutionWorld.ISOLATED, + }, + { + id: '@react-devtools/file-fetcher', + js: ['build/fileFetcher.js'], + matches: [''], + persistAcrossSessions: true, + runAt: 'document_end', + world: chrome.scripting.ExecutionWorld.ISOLATED, + }, + { + id: '@react-devtools/hook', + js: ['build/installHook.js'], + matches: [''], + persistAcrossSessions: true, + runAt: 'document_start', + world: chrome.scripting.ExecutionWorld.MAIN, + }, + { + id: '@react-devtools/renderer', + js: ['build/renderer.js'], + matches: [''], + persistAcrossSessions: true, + runAt: 'document_start', + world: chrome.scripting.ExecutionWorld.MAIN, + }, +]; async function dynamicallyInjectContentScripts() { try { @@ -61,9 +42,6 @@ async function dynamicallyInjectContentScripts() { // This fixes registering proxy content script in incognito mode await chrome.scripting.unregisterContentScripts(); - // equivalent logic for Firefox is in prepareInjection.js - // Manifest V3 method of injecting content script - // TODO(hoxyq): migrate Firefox to V3 manifests // Note: the "world" option in registerContentScripts is only available in Chrome v102+ // It's critical since it allows us to directly run scripts on the "main" world on the page // "document_start" allows it to run before the page's scripts diff --git a/packages/react-devtools-extensions/src/background/executeScript.js b/packages/react-devtools-extensions/src/background/executeScript.js index efe73229ecff5..8b80095d33c2e 100644 --- a/packages/react-devtools-extensions/src/background/executeScript.js +++ b/packages/react-devtools-extensions/src/background/executeScript.js @@ -1,40 +1,5 @@ /* global chrome */ -// Firefox doesn't support ExecutionWorld.MAIN yet -// https://bugzilla.mozilla.org/show_bug.cgi?id=1736575 -function executeScriptForFirefoxInMainWorld({target, files}) { - return chrome.scripting.executeScript({ - target, - func: fileNames => { - function injectScriptSync(src) { - let code = ''; - const request = new XMLHttpRequest(); - request.addEventListener('load', function () { - code = this.responseText; - }); - request.open('GET', src, false); - request.send(); - - const script = document.createElement('script'); - script.textContent = code; - - // This script runs before the element is created, - // so we add the script to instead. - if (document.documentElement) { - document.documentElement.appendChild(script); - } - - if (script.parentNode) { - script.parentNode.removeChild(script); - } - } - - fileNames.forEach(file => injectScriptSync(chrome.runtime.getURL(file))); - }, - args: [files], - }); -} - export function executeScriptInIsolatedWorld({target, files}) { return chrome.scripting.executeScript({ target, @@ -44,10 +9,6 @@ export function executeScriptInIsolatedWorld({target, files}) { } export function executeScriptInMainWorld({target, files}) { - if (__IS_FIREFOX__) { - return executeScriptForFirefoxInMainWorld({target, files}); - } - return chrome.scripting.executeScript({ target, files, diff --git a/packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js b/packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js index 5c6e011114014..51f233e284f0e 100644 --- a/packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js +++ b/packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js @@ -3,9 +3,7 @@ 'use strict'; function setExtensionIconAndPopup(reactBuildType, tabId) { - const action = __IS_FIREFOX__ ? chrome.browserAction : chrome.action; - - action.setIcon({ + chrome.action.setIcon({ tabId, path: { '16': chrome.runtime.getURL(`icons/16-${reactBuildType}.png`), @@ -15,7 +13,7 @@ function setExtensionIconAndPopup(reactBuildType, tabId) { }, }); - action.setPopup({ + chrome.action.setPopup({ tabId, popup: chrome.runtime.getURL(`popups/${reactBuildType}.html`), }); diff --git a/packages/react-devtools-extensions/src/background/tabsManager.js b/packages/react-devtools-extensions/src/background/tabsManager.js index 23b566502a269..192a6ce42ce28 100644 --- a/packages/react-devtools-extensions/src/background/tabsManager.js +++ b/packages/react-devtools-extensions/src/background/tabsManager.js @@ -18,26 +18,12 @@ function checkAndHandleRestrictedPageIfSo(tab) { // we can't update for any other types (prod,dev,outdated etc) // as the content script needs to be injected at document_start itself for those kinds of detection // TODO: Show a different popup page(to reload current page probably) for old tabs, opened before the extension is installed -if (__IS_CHROME__ || __IS_EDGE__) { - chrome.tabs.query({}, tabs => tabs.forEach(checkAndHandleRestrictedPageIfSo)); - chrome.tabs.onCreated.addListener((tabId, changeInfo, tab) => - checkAndHandleRestrictedPageIfSo(tab), - ); -} +chrome.tabs.query({}, tabs => tabs.forEach(checkAndHandleRestrictedPageIfSo)); +chrome.tabs.onCreated.addListener((tabId, changeInfo, tab) => + checkAndHandleRestrictedPageIfSo(tab), +); // Listen to URL changes on the active tab and update the DevTools icon. chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { - if (__IS_FIREFOX__) { - // We don't properly detect protected URLs in Firefox at the moment. - // However, we can reset the DevTools icon to its loading state when the URL changes. - // It will be updated to the correct icon by the onMessage callback below. - if (tab.active && changeInfo.status === 'loading') { - setExtensionIconAndPopup('disabled', tabId); - } - } else { - // Don't reset the icon to the loading state for Chrome or Edge. - // The onUpdated callback fires more frequently for these browsers, - // often after onMessage has been called. - checkAndHandleRestrictedPageIfSo(tab); - } + checkAndHandleRestrictedPageIfSo(tab); }); diff --git a/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js b/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js index d67ea7c405a1e..1b9962a9a826f 100644 --- a/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js +++ b/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js @@ -1,31 +1,5 @@ /* global chrome */ -import nullthrows from 'nullthrows'; - -// We run scripts on the page via the service worker (background/index.js) for -// Manifest V3 extensions (Chrome & Edge). -// We need to inject this code for Firefox only because it does not support ExecutionWorld.MAIN -// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld -// In this content script we have access to DOM, but don't have access to the webpage's window, -// so we inject this inline script tag into the webpage (allowed in Manifest V2). -function injectScriptSync(src) { - let code = ''; - const request = new XMLHttpRequest(); - request.addEventListener('load', function () { - code = this.responseText; - }); - request.open('GET', src, false); - request.send(); - - const script = document.createElement('script'); - script.textContent = code; - - // This script runs before the element is created, - // so we add the script to instead. - nullthrows(document.documentElement).appendChild(script); - nullthrows(script.parentNode).removeChild(script); -} - let lastSentDevToolsHookMessage; // We want to detect when a renderer attaches, and notify the "background page" @@ -60,17 +34,3 @@ window.addEventListener('pageshow', function ({target}) { chrome.runtime.sendMessage(lastSentDevToolsHookMessage); }); - -if (__IS_FIREFOX__) { - injectScriptSync(chrome.runtime.getURL('build/renderer.js')); - - // Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with. - // Only do this for HTML documents though, to avoid e.g. breaking syntax highlighting for XML docs. - switch (document.contentType) { - case 'text/html': - case 'application/xhtml+xml': { - injectScriptSync(chrome.runtime.getURL('build/installHook.js')); - break; - } - } -} diff --git a/packages/react-devtools-shared/babel.config.js b/packages/react-devtools-shared/babel.config.js index ca877aa683afd..78af34817e0a9 100644 --- a/packages/react-devtools-shared/babel.config.js +++ b/packages/react-devtools-shared/babel.config.js @@ -3,7 +3,7 @@ const firefoxManifest = require('../react-devtools-extensions/firefox/manifest.j const minChromeVersion = parseInt(chromeManifest.minimum_chrome_version, 10); const minFirefoxVersion = parseInt( - firefoxManifest.applications.gecko.strict_min_version, + firefoxManifest.browser_specific_settings.gecko.strict_min_version, 10, ); validateVersion(minChromeVersion); From c9830d64749cf8fd592ea30a1cd65842cf83f6df Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Thu, 8 Aug 2024 20:04:26 +0100 Subject: [PATCH 2/2] fix[react-devtools/extensions]: fixed tabs API calls and displaying restricted access popup --- .../chrome/manifest.json | 3 ++- .../edge/manifest.json | 3 ++- .../firefox/manifest.json | 3 ++- .../src/background/tabsManager.js | 20 ++++++++++--------- .../src/main/registerEventsLogger.js | 10 ++-------- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index 4dcd951a480ac..e7b9b19b6a9df 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -42,7 +42,8 @@ }, "permissions": [ "storage", - "scripting" + "scripting", + "tabs" ], "host_permissions": [ "" diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index fd19f1c5df532..496d4e92c3b63 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -42,7 +42,8 @@ }, "permissions": [ "storage", - "scripting" + "scripting", + "tabs" ], "host_permissions": [ "" diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 3c2d417d585af..930c1ab11083e 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -48,7 +48,8 @@ ] }, "permissions": [ - "scripting" + "scripting", + "tabs" ], "host_permissions": [ "" diff --git a/packages/react-devtools-extensions/src/background/tabsManager.js b/packages/react-devtools-extensions/src/background/tabsManager.js index 192a6ce42ce28..d46c14c6ea7c3 100644 --- a/packages/react-devtools-extensions/src/background/tabsManager.js +++ b/packages/react-devtools-extensions/src/background/tabsManager.js @@ -5,7 +5,12 @@ import setExtensionIconAndPopup from './setExtensionIconAndPopup'; function isRestrictedBrowserPage(url) { - return !url || new URL(url).protocol === 'chrome:'; + if (!url) { + return true; + } + + const urlProtocol = new URL(url).protocol; + return urlProtocol === 'chrome:' || urlProtocol === 'about:'; } function checkAndHandleRestrictedPageIfSo(tab) { @@ -14,16 +19,13 @@ function checkAndHandleRestrictedPageIfSo(tab) { } } -// update popup page of any existing open tabs, if they are restricted browser pages. -// we can't update for any other types (prod,dev,outdated etc) -// as the content script needs to be injected at document_start itself for those kinds of detection -// TODO: Show a different popup page(to reload current page probably) for old tabs, opened before the extension is installed +// Update popup page of any existing open tabs, if they are restricted browser pages chrome.tabs.query({}, tabs => tabs.forEach(checkAndHandleRestrictedPageIfSo)); -chrome.tabs.onCreated.addListener((tabId, changeInfo, tab) => - checkAndHandleRestrictedPageIfSo(tab), -); +chrome.tabs.onCreated.addListener(tab => checkAndHandleRestrictedPageIfSo(tab)); // Listen to URL changes on the active tab and update the DevTools icon. chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { - checkAndHandleRestrictedPageIfSo(tab); + if (changeInfo.url && isRestrictedBrowserPage(changeInfo.url)) { + setExtensionIconAndPopup('restricted', tabId); + } }); diff --git a/packages/react-devtools-extensions/src/main/registerEventsLogger.js b/packages/react-devtools-extensions/src/main/registerEventsLogger.js index 5234866fd546c..ec57d173e42ca 100644 --- a/packages/react-devtools-extensions/src/main/registerEventsLogger.js +++ b/packages/react-devtools-extensions/src/main/registerEventsLogger.js @@ -4,14 +4,8 @@ import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDev function registerEventsLogger() { registerDevToolsEventLogger('extension', async () => { - // TODO: after we upgrade to Firefox Manifest V3, chrome.tabs.query returns a Promise without the callback. - return new Promise(resolve => { - chrome.tabs.query({active: true}, tabs => { - resolve({ - page_url: tabs[0]?.url, - }); - }); - }); + const tabs = await chrome.tabs.query({active: true}); + return {page_url: tabs[0]?.url}; }); }