@samitouri / QOS-React / commits / 09285d5a7f

refactor[devtools/extension]: refactored messaging logic across different parts of the extension (#27417)

1. https://github.com/bvaughn/react/commit/9fc04eaf3fb701cdc14f57d5aed48f3126af6c94#diff-2c5e1f5e80e74154e65b2813cf1c3638f85034530e99dae24809ab4ad70d0143 introduced a vulnerability: we listen to `'fetch-file-with-cache'` event from `window` to fetch sources of the file, in which we want to parse hook names. We send this event via `window`, which means any page can also use this and manipulate the extension to perform some `fetch()` calls. With these changes, instead of transporting message via `window`, we have a distinct content script, which is responsible for fetching sources. It is notified via `chrome.runtime.sendMessage` api, so it can't be manipulated. 2. Consistent structure of messages `{source: string, payload: object}` in different parts of the extension 3. Added some wrappers around `chrome.scripting.executeScript` API in `packages/react-devtools-extensions/src/background/executeScript.js`, which support custom flow for Firefox, to simulate support of `ExecutionWorld.MAIN`.

Ruslan Lesiutin committed Sep 25, 2023 at 12:02 UTC 09285d5a7f1c08bec09f44cec3d0518a603597fc
14 files changed +406 -291
packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js
+15
@@ -13,6 +13,13 @@ const contentScriptsToInject = IS_FIREFOX
13 persistAcrossSessions: true,
14 runAt: 'document_end',
15 },
16 + {
17 + id: '@react-devtools/file-fetcher',
18 + js: ['build/fileFetcher.js'],
19 + matches: ['<all_urls>'],
20 + persistAcrossSessions: true,
21 + runAt: 'document_end',
22 + },
23 ]
24 : [
25 {
@@ -23,6 +30,14 @@ const contentScriptsToInject = IS_FIREFOX
30 runAt: 'document_end',
31 world: chrome.scripting.ExecutionWorld.ISOLATED,
32 },
33 + {
34 + id: '@react-devtools/file-fetcher',
35 + js: ['build/fileFetcher.js'],
36 + matches: ['<all_urls>'],
37 + persistAcrossSessions: true,
38 + runAt: 'document_end',
39 + world: chrome.scripting.ExecutionWorld.ISOLATED,
40 + },
41 {
42 id: '@react-devtools/hook',
43 js: ['build/installHook.js'],
packages/react-devtools-extensions/src/background/executeScript.js new
+58
@@ -0,0 +1,58 @@
1 +/* global chrome */
2 +
3 +import {IS_FIREFOX} from '../utils';
4 +
5 +// Firefox doesn't support ExecutionWorld.MAIN yet
6 +// https://bugzilla.mozilla.org/show_bug.cgi?id=1736575
7 +function executeScriptForFirefoxInMainWorld({target, files}) {
8 + return chrome.scripting.executeScript({
9 + target,
10 + func: fileNames => {
11 + function injectScriptSync(src) {
12 + let code = '';
13 + const request = new XMLHttpRequest();
14 + request.addEventListener('load', function () {
15 + code = this.responseText;
16 + });
17 + request.open('GET', src, false);
18 + request.send();
19 +
20 + const script = document.createElement('script');
21 + script.textContent = code;
22 +
23 + // This script runs before the <head> element is created,
24 + // so we add the script to <html> instead.
25 + if (document.documentElement) {
26 + document.documentElement.appendChild(script);
27 + }
28 +
29 + if (script.parentNode) {
30 + script.parentNode.removeChild(script);
31 + }
32 + }
33 +
34 + fileNames.forEach(file => injectScriptSync(chrome.runtime.getURL(file)));
35 + },
36 + args: [files],
37 + });
38 +}
39 +
40 +export function executeScriptInIsolatedWorld({target, files}) {
41 + return chrome.scripting.executeScript({
42 + target,
43 + files,
44 + world: chrome.scripting.ExecutionWorld.ISOLATED,
45 + });
46 +}
47 +
48 +export function executeScriptInMainWorld({target, files}) {
49 + if (IS_FIREFOX) {
50 + return executeScriptForFirefoxInMainWorld({target, files});
51 + }
52 +
53 + return chrome.scripting.executeScript({
54 + target,
55 + files,
56 + world: chrome.scripting.ExecutionWorld.MAIN,
57 + });
58 +}
packages/react-devtools-extensions/src/background/index.js
+21 -63
@@ -2,11 +2,15 @@
2
3 'use strict';
4
5 -import {IS_FIREFOX, EXTENSION_CONTAINED_VERSIONS} from '../utils';
6 -
5 import './dynamicallyInjectContentScripts';
6 import './tabsManager';
9 -import setExtensionIconAndPopup from './setExtensionIconAndPopup';
7 +
8 +import {
9 + handleDevToolsPageMessage,
10 + handleBackendManagerMessage,
11 + handleReactDevToolsHookMessage,
12 + handleFetchResourceContentScriptMessage,
13 +} from './messageHandlers';
14
15 /*
16 {
@@ -173,67 +177,21 @@ function connectExtensionAndProxyPorts(extensionPort, proxyPort, tabId) {
177 }
178
179 chrome.runtime.onMessage.addListener((message, sender) => {
176 - const tab = sender.tab;
177 - // sender.tab.id from content script points to the tab that injected the content script
178 - if (tab) {
179 - const id = tab.id;
180 - // This is sent from the hook content script.
181 - // It tells us a renderer has attached.
182 - if (message.hasDetectedReact) {
183 - setExtensionIconAndPopup(message.reactBuildType, id);
184 - } else {
185 - const extensionPort = ports[id]?.extension;
186 -
187 - switch (message.payload?.type) {
188 - case 'fetch-file-with-cache-complete':
189 - case 'fetch-file-with-cache-error':
190 - // Forward the result of fetch-in-page requests back to the extension.
191 - extensionPort?.postMessage(message);
192 - break;
193 - // This is sent from the backend manager running on a page
194 - case 'react-devtools-required-backends':
195 - const backendsToDownload = [];
196 - message.payload.versions.forEach(version => {
197 - if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
198 - if (!IS_FIREFOX) {
199 - // equivalent logic for Firefox is in prepareInjection.js
200 - chrome.scripting.executeScript({
201 - target: {tabId: id},
202 - files: [`/build/react_devtools_backend_${version}.js`],
203 - world: chrome.scripting.ExecutionWorld.MAIN,
204 - });
205 - }
206 - } else {
207 - backendsToDownload.push(version);
208 - }
209 - });
210 -
211 - // Request the necessary backends in the extension DevTools UI
212 - // TODO: handle this message in index.js to build the UI
213 - extensionPort?.postMessage({
214 - payload: {
215 - type: 'react-devtools-additional-backends',
216 - versions: backendsToDownload,
217 - },
218 - });
219 - break;
220 - }
180 + switch (message?.source) {
181 + case 'devtools-page': {
182 + handleDevToolsPageMessage(message);
183 + break;
184 }
222 - }
223 -
224 - // This is sent from the devtools page when it is ready for injecting the backend
225 - if (message?.payload?.type === 'react-devtools-inject-backend-manager') {
226 - // sender.tab.id from devtools page may not exist, or point to the undocked devtools window
227 - // so we use the payload to get the tab id
228 - const tabId = message.payload.tabId;
229 -
230 - if (tabId && !IS_FIREFOX) {
231 - // equivalent logic for Firefox is in prepareInjection.js
232 - chrome.scripting.executeScript({
233 - target: {tabId},
234 - files: ['/build/backendManager.js'],
235 - world: chrome.scripting.ExecutionWorld.MAIN,
236 - });
185 + case 'react-devtools-fetch-resource-content-script': {
186 + handleFetchResourceContentScriptMessage(message);
187 + break;
188 + }
189 + case 'react-devtools-backend-manager': {
190 + handleBackendManagerMessage(message, sender);
191 + break;
192 + }
193 + case 'react-devtools-hook': {
194 + handleReactDevToolsHookMessage(message, sender);
195 }
196 }
197 });
packages/react-devtools-extensions/src/background/injectProxy.js deleted
-12
@@ -1,12 +0,0 @@
1 -/* global chrome */
2 -
3 -// We keep this logic in background, because Firefox doesn't allow using these APIs
4 -// from extension page script
5 -function injectProxy(tabId: number) {
6 - chrome.scripting.executeScript({
7 - target: {tabId},
8 - files: ['/build/proxy.js'],
9 - });
10 -}
11 -
12 -export default injectProxy;
packages/react-devtools-extensions/src/background/messageHandlers.js new
+103
@@ -0,0 +1,103 @@
1 +/* global chrome */
2 +
3 +import setExtensionIconAndPopup from './setExtensionIconAndPopup';
4 +import {executeScriptInMainWorld} from './executeScript';
5 +
6 +import {EXTENSION_CONTAINED_VERSIONS} from '../utils';
7 +
8 +export function handleReactDevToolsHookMessage(message, sender) {
9 + const {payload} = message;
10 +
11 + switch (payload?.type) {
12 + case 'react-renderer-attached': {
13 + setExtensionIconAndPopup(payload.reactBuildType, sender.tab.id);
14 +
15 + break;
16 + }
17 + }
18 +}
19 +
20 +export function handleBackendManagerMessage(message, sender) {
21 + const {payload} = message;
22 +
23 + switch (payload?.type) {
24 + case 'require-backends': {
25 + payload.versions.forEach(version => {
26 + if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
27 + executeScriptInMainWorld({
28 + target: {tabId: sender.tab.id},
29 + files: [`/build/react_devtools_backend_${version}.js`],
30 + });
31 + }
32 + });
33 +
34 + break;
35 + }
36 + }
37 +}
38 +
39 +export function handleDevToolsPageMessage(message) {
40 + const {payload} = message;
41 +
42 + switch (payload?.type) {
43 + // Proxy this message from DevTools page to content script via chrome.tabs.sendMessage
44 + case 'fetch-file-with-cache': {
45 + const {
46 + payload: {tabId, url},
47 + } = message;
48 +
49 + if (!tabId) {
50 + throw new Error("Couldn't fetch file sources: tabId not specified");
51 + }
52 +
53 + if (!url) {
54 + throw new Error("Couldn't fetch file sources: url not specified");
55 + }
56 +
57 + chrome.tabs.sendMessage(tabId, {
58 + source: 'devtools-page',
59 + payload: {
60 + type: 'fetch-file-with-cache',
61 + url,
62 + },
63 + });
64 +
65 + break;
66 + }
67 +
68 + case 'inject-backend-manager': {
69 + const {
70 + payload: {tabId},
71 + } = message;
72 +
73 + if (!tabId) {
74 + throw new Error("Couldn't inject backend manager: tabId not specified");
75 + }
76 +
77 + executeScriptInMainWorld({
78 + target: {tabId},
79 + files: ['/build/backendManager.js'],
80 + });
81 +
82 + break;
83 + }
84 + }
85 +}
86 +
87 +export function handleFetchResourceContentScriptMessage(message) {
88 + const {payload} = message;
89 +
90 + switch (payload?.type) {
91 + case 'fetch-file-with-cache-complete':
92 + case 'fetch-file-with-cache-error':
93 + // Forward the result of fetch-in-page requests back to the DevTools page.
94 + // We switch the source here because of inconsistency between Firefox and Chrome
95 + // In Chromium this message will be propagated from content script to DevTools page
96 + // For Firefox, only background script will get this message, so we need to forward it to DevTools page
97 + chrome.runtime.sendMessage({
98 + source: 'react-devtools-background',
99 + payload,
100 + });
101 + break;
102 + }
103 +}
packages/react-devtools-extensions/src/contentScripts/backendManager.js
+1 -1
@@ -170,7 +170,7 @@ function updateRequiredBackends() {
170 {
171 source: 'react-devtools-backend-manager',
172 payload: {
173 - type: 'react-devtools-required-backends',
173 + type: 'require-backends',
174 versions: Array.from(requiredBackends),
175 },
176 },
packages/react-devtools-extensions/src/contentScripts/fileFetcher.js new
+48
@@ -0,0 +1,48 @@
1 +/* global chrome */
2 +
3 +function fetchResource(url) {
4 + const reject = value => {
5 + chrome.runtime.sendMessage({
6 + source: 'react-devtools-fetch-resource-content-script',
7 + payload: {
8 + type: 'fetch-file-with-cache-error',
9 + url,
10 + value,
11 + },
12 + });
13 + };
14 +
15 + const resolve = value => {
16 + chrome.runtime.sendMessage({
17 + source: 'react-devtools-fetch-resource-content-script',
18 + payload: {
19 + type: 'fetch-file-with-cache-complete',
20 + url,
21 + value,
22 + },
23 + });
24 + };
25 +
26 + fetch(url, {cache: 'force-cache'}).then(
27 + response => {
28 + if (response.ok) {
29 + response
30 + .text()
31 + .then(text => resolve(text))
32 + .catch(error => reject(null));
33 + } else {
34 + reject(null);
35 + }
36 + },
37 + error => reject(null),
38 + );
39 +}
40 +
41 +chrome.runtime.onMessage.addListener(message => {
42 + if (
43 + message?.source === 'devtools-page' &&
44 + message?.payload?.type === 'fetch-file-with-cache'
45 + ) {
46 + fetchResource(message.payload.url);
47 + }
48 +});
packages/react-devtools-extensions/src/contentScripts/installHook.js
+5 -2
@@ -10,8 +10,11 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
10 function ({reactBuildType}) {
11 window.postMessage(
12 {
13 - source: 'react-devtools-detector',
14 - reactBuildType,
13 + source: 'react-devtools-hook',
14 + payload: {
15 + type: 'react-renderer-attached',
16 + reactBuildType,
17 + },
18 },
19 '*',
20 );
packages/react-devtools-extensions/src/contentScripts/prepareInjection.js
+13 -77
@@ -1,9 +1,7 @@
1 /* global chrome */
2
3 import nullthrows from 'nullthrows';
4 -import {SESSION_STORAGE_RELOAD_AND_PROFILE_KEY} from 'react-devtools-shared/src/constants';
5 -import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
6 -import {IS_FIREFOX, EXTENSION_CONTAINED_VERSIONS} from '../utils';
4 +import {IS_FIREFOX} from '../utils';
5
6 // We run scripts on the page via the service worker (background/index.js) for
7 // Manifest V3 extensions (Chrome & Edge).
@@ -29,7 +27,7 @@ function injectScriptSync(src) {
27 nullthrows(script.parentNode).removeChild(script);
28 }
29
32 -let lastDetectionResult;
30 +let lastSentDevToolsHookMessage;
31
32 // We want to detect when a renderer attaches, and notify the "background page"
33 // (which is shared between tabs and can highlight the React icon).
@@ -41,73 +39,14 @@ window.addEventListener('message', function onMessage({data, source}) {
39 if (source !== window || !data) {
40 return;
41 }
44 - switch (data.source) {
45 - case 'react-devtools-detector':
46 - lastDetectionResult = {
47 - hasDetectedReact: true,
48 - reactBuildType: data.reactBuildType,
49 - };
50 - chrome.runtime.sendMessage(lastDetectionResult);
51 - break;
52 - case 'react-devtools-extension':
53 - if (data.payload?.type === 'fetch-file-with-cache') {
54 - const url = data.payload.url;
55 -
56 - const reject = value => {
57 - chrome.runtime.sendMessage({
58 - source: 'react-devtools-content-script',
59 - payload: {
60 - type: 'fetch-file-with-cache-error',
61 - url,
62 - value,
63 - },
64 - });
65 - };
42
67 - const resolve = value => {
68 - chrome.runtime.sendMessage({
69 - source: 'react-devtools-content-script',
70 - payload: {
71 - type: 'fetch-file-with-cache-complete',
72 - url,
73 - value,
74 - },
75 - });
76 - };
43 + // We keep this logic here and not in `proxy.js`, because proxy content script is injected later at `document_end`
44 + if (data.source === 'react-devtools-hook') {
45 + const {source: messageSource, payload} = data;
46 + const message = {source: messageSource, payload};
47
78 - fetch(url, {cache: 'force-cache'}).then(
79 - response => {
80 - if (response.ok) {
81 - response
82 - .text()
83 - .then(text => resolve(text))
84 - .catch(error => reject(null));
85 - } else {
86 - reject(null);
87 - }
88 - },
89 - error => reject(null),
90 - );
91 - }
92 - break;
93 - case 'react-devtools-inject-backend-manager':
94 - if (IS_FIREFOX) {
95 - injectScriptSync(chrome.runtime.getURL('build/backendManager.js'));
96 - }
97 - break;
98 - case 'react-devtools-backend-manager':
99 - if (IS_FIREFOX) {
100 - data.payload?.versions?.forEach(version => {
101 - if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
102 - injectScriptSync(
103 - chrome.runtime.getURL(
104 - `/build/react_devtools_backend_${version}.js`,
105 - ),
106 - );
107 - }
108 - });
109 - }
110 - break;
48 + lastSentDevToolsHookMessage = message;
49 + chrome.runtime.sendMessage(message);
50 }
51 });
52
@@ -116,19 +55,16 @@ window.addEventListener('message', function onMessage({data, source}) {
55 // replay the last detection result if the content script is active and the
56 // document has been hidden and shown again.
57 window.addEventListener('pageshow', function ({target}) {
119 - if (!lastDetectionResult || target !== window.document) {
58 + if (!lastSentDevToolsHookMessage || target !== window.document) {
59 return;
60 }
122 - chrome.runtime.sendMessage(lastDetectionResult);
61 +
62 + chrome.runtime.sendMessage(lastSentDevToolsHookMessage);
63 });
64
65 if (IS_FIREFOX) {
126 - // If we have just reloaded to profile, we need to inject the renderer interface before the app loads.
127 - if (
128 - sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true'
129 - ) {
130 - injectScriptSync(chrome.runtime.getURL('build/renderer.js'));
131 - }
66 + injectScriptSync(chrome.runtime.getURL('build/renderer.js'));
67 +
68 // Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with.
69 // Only do this for HTML documents though, to avoid e.g. breaking syntax highlighting for XML docs.
70 switch (document.contentType) {
packages/react-devtools-extensions/src/contentScripts/proxy.js
+15 -5
@@ -56,19 +56,29 @@ function handleMessageFromDevtools(message) {
56 }
57
58 function handleMessageFromPage(event) {
59 - if (event.source === window && event.data) {
59 + if (event.source !== window || !event.data) {
60 + return;
61 + }
62 +
63 + switch (event.data.source) {
64 // This is a message from a bridge (initialized by a devtools backend)
61 - if (event.data.source === 'react-devtools-bridge') {
65 + case 'react-devtools-bridge': {
66 backendInitialized = true;
67
68 port.postMessage(event.data.payload);
69 + break;
70 }
71
67 - // This is a message from the backend manager
68 - if (event.data.source === 'react-devtools-backend-manager') {
72 + // This is a message from the backend manager, which runs in ExecutionWorld.MAIN
73 + // and can't use `chrome.runtime.sendMessage`
74 + case 'react-devtools-backend-manager': {
75 + const {source, payload} = event.data;
76 +
77 chrome.runtime.sendMessage({
70 - payload: event.data.payload,
78 + source,
79 + payload,
80 });
81 + break;
82 }
83 }
84 }
packages/react-devtools-extensions/src/main/fetchFileWithCaching.js new
+123
@@ -0,0 +1,123 @@
1 +/* global chrome */
2 +
3 +import {__DEBUG__} from 'react-devtools-shared/src/constants';
4 +
5 +let debugIDCounter = 0;
6 +
7 +const debugLog = (...args) => {
8 + if (__DEBUG__) {
9 + console.log(...args);
10 + }
11 +};
12 +
13 +const fetchFromNetworkCache = (url, resolve, reject) => {
14 + // Debug ID allows us to avoid re-logging (potentially long) URL strings below,
15 + // while also still associating (potentially) interleaved logs with the original request.
16 + let debugID = null;
17 +
18 + if (__DEBUG__) {
19 + debugID = debugIDCounter++;
20 + debugLog(`[main] fetchFromNetworkCache(${debugID})`, url);
21 + }
22 +
23 + chrome.devtools.network.getHAR(harLog => {
24 + for (let i = 0; i < harLog.entries.length; i++) {
25 + const entry = harLog.entries[i];
26 + if (url !== entry.request.url) {
27 + continue;
28 + }
29 +
30 + debugLog(
31 + `[main] fetchFromNetworkCache(${debugID}) Found matching URL in HAR`,
32 + url,
33 + );
34 +
35 + if (entry.getContent != null) {
36 + entry.getContent(content => {
37 + if (content) {
38 + debugLog(
39 + `[main] fetchFromNetworkCache(${debugID}) Content retrieved`,
40 + );
41 +
42 + resolve(content);
43 + } else {
44 + debugLog(
45 + `[main] fetchFromNetworkCache(${debugID}) Invalid content returned by getContent()`,
46 + content,
47 + );
48 +
49 + // Edge case where getContent() returned null; fall back to fetch.
50 + fetchFromPage(url, resolve, reject);
51 + }
52 + });
53 + } else {
54 + const content = entry.response.content.text;
55 +
56 + if (content != null) {
57 + debugLog(
58 + `[main] fetchFromNetworkCache(${debugID}) Content retrieved`,
59 + );
60 + resolve(content);
61 + } else {
62 + debugLog(
63 + `[main] fetchFromNetworkCache(${debugID}) Invalid content returned from entry.response.content`,
64 + content,
65 + );
66 + fetchFromPage(url, resolve, reject);
67 + }
68 + }
69 + }
70 +
71 + debugLog(
72 + `[main] fetchFromNetworkCache(${debugID}) No cached request found in getHAR()`,
73 + );
74 +
75 + // No matching URL found; fall back to fetch.
76 + fetchFromPage(url, resolve, reject);
77 + });
78 +};
79 +
80 +const fetchFromPage = async (url, resolve, reject) => {
81 + debugLog('[main] fetchFromPage()', url);
82 +
83 + function onPortMessage({payload, source}) {
84 + if (source === 'react-devtools-background') {
85 + switch (payload?.type) {
86 + case 'fetch-file-with-cache-complete':
87 + chrome.runtime.onMessage.removeListener(onPortMessage);
88 + resolve(payload.value);
89 + break;
90 + case 'fetch-file-with-cache-error':
91 + chrome.runtime.onMessage.removeListener(onPortMessage);
92 + reject(payload.value);
93 + break;
94 + }
95 + }
96 + }
97 +
98 + chrome.runtime.onMessage.addListener(onPortMessage);
99 +
100 + chrome.runtime.sendMessage({
101 + source: 'devtools-page',
102 + payload: {
103 + type: 'fetch-file-with-cache',
104 + tabId: chrome.devtools.inspectedWindow.tabId,
105 + url,
106 + },
107 + });
108 +};
109 +
110 +// Fetching files from the extension won't make use of the network cache
111 +// for resources that have already been loaded by the page.
112 +// This helper function allows the extension to request files to be fetched
113 +// by the content script (running in the page) to increase the likelihood of a cache hit.
114 +const fetchFileWithCaching = url => {
115 + return new Promise((resolve, reject) => {
116 + // Try fetching from the Network cache first.
117 + // If DevTools was opened after the page started loading, we may have missed some requests.
118 + // So fall back to a fetch() from the page and hope we get a cached response that way.
119 + fetchFromNetworkCache(url, resolve, reject);
120 + });
121 +};
122 +
123 +export default fetchFileWithCaching;
packages/react-devtools-extensions/src/main/index.js
+1 -112
@@ -12,7 +12,6 @@ import {
12 } from 'react-devtools-shared/src/storage';
13 import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
14 import {
15 - __DEBUG__,
15 LOCAL_STORAGE_SUPPORTS_PROFILING_KEY,
16 LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
17 } from 'react-devtools-shared/src/constants';
@@ -24,6 +23,7 @@ import {
23 } from './elementSelection';
24 import {startReactPolling} from './reactPolling';
25 import cloneStyleTags from './cloneStyleTags';
26 +import fetchFileWithCaching from './fetchFileWithCaching';
27 import injectBackendManager from './injectBackendManager';
28 import syncSavedPreferences from './syncSavedPreferences';
29 import registerEventsLogger from './registerEventsLogger';
@@ -158,117 +158,6 @@ function createBridgeAndStore() {
158 }
159 };
160
161 - let debugIDCounter = 0;
162 -
163 - // For some reason in Firefox, chrome.runtime.sendMessage() from a content script
164 - // never reaches the chrome.runtime.onMessage event listener.
165 - let fetchFileWithCaching = null;
166 - if (IS_CHROME) {
167 - const fetchFromNetworkCache = (url, resolve, reject) => {
168 - // Debug ID allows us to avoid re-logging (potentially long) URL strings below,
169 - // while also still associating (potentially) interleaved logs with the original request.
170 - let debugID = null;
171 -
172 - if (__DEBUG__) {
173 - debugID = debugIDCounter++;
174 - console.log(`[main] fetchFromNetworkCache(${debugID})`, url);
175 - }
176 -
177 - chrome.devtools.network.getHAR(harLog => {
178 - for (let i = 0; i < harLog.entries.length; i++) {
179 - const entry = harLog.entries[i];
180 - if (url === entry.request.url) {
181 - if (__DEBUG__) {
182 - console.log(
183 - `[main] fetchFromNetworkCache(${debugID}) Found matching URL in HAR`,
184 - url,
185 - );
186 - }
187 -
188 - entry.getContent(content => {
189 - if (content) {
190 - if (__DEBUG__) {
191 - console.log(
192 - `[main] fetchFromNetworkCache(${debugID}) Content retrieved`,
193 - );
194 - }
195 -
196 - resolve(content);
197 - } else {
198 - if (__DEBUG__) {
199 - console.log(
200 - `[main] fetchFromNetworkCache(${debugID}) Invalid content returned by getContent()`,
201 - content,
202 - );
203 - }
204 -
205 - // Edge case where getContent() returned null; fall back to fetch.
206 - fetchFromPage(url, resolve, reject);
207 - }
208 - });
209 -
210 - return;
211 - }
212 - }
213 -
214 - if (__DEBUG__) {
215 - console.log(
216 - `[main] fetchFromNetworkCache(${debugID}) No cached request found in getHAR()`,
217 - );
218 - }
219 -
220 - // No matching URL found; fall back to fetch.
221 - fetchFromPage(url, resolve, reject);
222 - });
223 - };
224 -
225 - const fetchFromPage = (url, resolve, reject) => {
226 - if (__DEBUG__) {
227 - console.log('[main] fetchFromPage()', url);
228 - }
229 -
230 - function onPortMessage({payload, source}) {
231 - if (source === 'react-devtools-content-script') {
232 - switch (payload?.type) {
233 - case 'fetch-file-with-cache-complete':
234 - chrome.runtime.onMessage.removeListener(onPortMessage);
235 - resolve(payload.value);
236 - break;
237 - case 'fetch-file-with-cache-error':
238 - chrome.runtime.onMessage.removeListener(onPortMessage);
239 - reject(payload.value);
240 - break;
241 - }
242 - }
243 - }
244 -
245 - chrome.runtime.onMessage.addListener(onPortMessage);
246 -
247 - chrome.devtools.inspectedWindow.eval(`
248 - window.postMessage({
249 - source: 'react-devtools-extension',
250 - payload: {
251 - type: 'fetch-file-with-cache',
252 - url: "${url}",
253 - },
254 - });
255 - `);
256 - };
257 -
258 - // Fetching files from the extension won't make use of the network cache
259 - // for resources that have already been loaded by the page.
260 - // This helper function allows the extension to request files to be fetched
261 - // by the content script (running in the page) to increase the likelihood of a cache hit.
262 - fetchFileWithCaching = url => {
263 - return new Promise((resolve, reject) => {
264 - // Try fetching from the Network cache first.
265 - // If DevTools was opened after the page started loading, we may have missed some requests.
266 - // So fall back to a fetch() from the page and hope we get a cached response that way.
267 - fetchFromNetworkCache(url, resolve, reject);
268 - });
269 - };
270 - }
271 -
161 // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
162 const hookNamesModuleLoaderFunction = () =>
163 import(
packages/react-devtools-extensions/src/main/injectBackendManager.js
+2 -19
@@ -1,27 +1,10 @@
1 /* global chrome */
2
3 -import {IS_FIREFOX} from '../utils';
4 -
3 function injectBackendManager(tabId) {
6 - if (IS_FIREFOX) {
7 - // Firefox does not support executing script in ExecutionWorld.MAIN from content script.
8 - // see prepareInjection.js
9 - chrome.devtools.inspectedWindow.eval(
10 - `window.postMessage({ source: 'react-devtools-inject-backend-manager' }, '*');`,
11 - function (response, evalError) {
12 - if (evalError) {
13 - console.error(evalError);
14 - }
15 - },
16 - );
17 -
18 - return;
19 - }
20 -
4 chrome.runtime.sendMessage({
22 - source: 'react-devtools-main',
5 + source: 'devtools-page',
6 payload: {
24 - type: 'react-devtools-inject-backend-manager',
7 + type: 'inject-backend-manager',
8 tabId,
9 },
10 });
packages/react-devtools-extensions/webpack.config.js
+1
@@ -57,6 +57,7 @@ module.exports = {
57 entry: {
58 background: './src/background/index.js',
59 backendManager: './src/contentScripts/backendManager.js',
60 + fileFetcher: './src/contentScripts/fileFetcher.js',
61 main: './src/main/index.js',
62 panel: './src/panel.js',
63 proxy: './src/contentScripts/proxy.js',