Skip to content

Commit 583e200

Browse files
authored
[DevTools] Enable minimal support in pages with sandbox Content-Security-Policy (facebook#35208)
1 parent 8a83073 commit 583e200

File tree

11 files changed

+390
-54
lines changed

11 files changed

+390
-54
lines changed

packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ const contentScriptsToInject = [
1717
runAt: 'document_end',
1818
world: chrome.scripting.ExecutionWorld.ISOLATED,
1919
},
20+
{
21+
id: '@react-devtools/fallback-eval-context',
22+
js: ['build/fallbackEvalContext.js'],
23+
matches: ['<all_urls>'],
24+
persistAcrossSessions: true,
25+
runAt: 'document_start',
26+
world: chrome.scripting.ExecutionWorld.MAIN,
27+
},
2028
{
2129
id: '@react-devtools/hook',
2230
js: ['build/installHook.js'],

packages/react-devtools-extensions/src/background/messageHandlers.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,58 @@ export function handleDevToolsPageMessage(message) {
9797

9898
break;
9999
}
100+
101+
case 'eval-in-inspected-window': {
102+
const {
103+
payload: {tabId, requestId, scriptId, args},
104+
} = message;
105+
106+
chrome.tabs
107+
.sendMessage(tabId, {
108+
source: 'devtools-page-eval',
109+
payload: {
110+
scriptId,
111+
args,
112+
},
113+
})
114+
.then(response => {
115+
if (!response) {
116+
chrome.runtime.sendMessage({
117+
source: 'react-devtools-background',
118+
payload: {
119+
type: 'eval-in-inspected-window-response',
120+
requestId,
121+
result: null,
122+
error: 'No response from content script',
123+
},
124+
});
125+
return;
126+
}
127+
const {result, error} = response;
128+
chrome.runtime.sendMessage({
129+
source: 'react-devtools-background',
130+
payload: {
131+
type: 'eval-in-inspected-window-response',
132+
requestId,
133+
result,
134+
error,
135+
},
136+
});
137+
})
138+
.catch(error => {
139+
chrome.runtime.sendMessage({
140+
source: 'react-devtools-background',
141+
payload: {
142+
type: 'eval-in-inspected-window-response',
143+
requestId,
144+
result: null,
145+
error: error?.message || String(error),
146+
},
147+
});
148+
});
149+
150+
break;
151+
}
100152
}
101153
}
102154

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import {evalScripts} from '../evalScripts';
11+
12+
window.addEventListener('message', event => {
13+
if (event.data?.source === 'react-devtools-content-script-eval') {
14+
const {scriptId, args, requestId} = event.data.payload;
15+
const response = {result: null, error: null};
16+
try {
17+
if (!evalScripts[scriptId]) {
18+
throw new Error(`No eval script with id "${scriptId}" exists.`);
19+
}
20+
response.result = evalScripts[scriptId].fn.apply(null, args);
21+
} catch (err) {
22+
response.error = err.message;
23+
}
24+
window.postMessage(
25+
{
26+
source: 'react-devtools-content-script-eval-response',
27+
payload: {
28+
requestId,
29+
response,
30+
},
31+
},
32+
'*',
33+
);
34+
}
35+
});

packages/react-devtools-extensions/src/contentScripts/proxy.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,49 @@ function connectPort() {
117117
// $FlowFixMe[incompatible-use]
118118
port.onDisconnect.addListener(handleDisconnect);
119119
}
120+
121+
let evalRequestId = 0;
122+
const evalRequestCallbacks = new Map<number, Function>();
123+
124+
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
125+
switch (msg?.source) {
126+
case 'devtools-page-eval': {
127+
const {scriptId, args} = msg.payload;
128+
const requestId = evalRequestId++;
129+
window.postMessage(
130+
{
131+
source: 'react-devtools-content-script-eval',
132+
payload: {
133+
requestId,
134+
scriptId,
135+
args,
136+
},
137+
},
138+
'*',
139+
);
140+
evalRequestCallbacks.set(requestId, sendResponse);
141+
return true; // Indicate we will respond asynchronously
142+
}
143+
}
144+
});
145+
146+
window.addEventListener('message', event => {
147+
if (event.data?.source === 'react-devtools-content-script-eval-response') {
148+
const {requestId, response} = event.data.payload;
149+
const callback = evalRequestCallbacks.get(requestId);
150+
try {
151+
if (!callback)
152+
throw new Error(
153+
`No eval request callback for id "${requestId}" exists.`,
154+
);
155+
callback(response);
156+
} catch (e) {
157+
console.warn(
158+
'React DevTools Content Script eval response error occurred:',
159+
e,
160+
);
161+
} finally {
162+
evalRequestCallbacks.delete(requestId);
163+
}
164+
}
165+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
export type EvalScriptIds =
11+
| 'checkIfReactPresentInInspectedWindow'
12+
| 'reload'
13+
| 'setBrowserSelectionFromReact'
14+
| 'setReactSelectionFromBrowser'
15+
| 'viewAttributeSource'
16+
| 'viewElementSource';
17+
18+
/*
19+
.fn for fallback in Content Script context
20+
.code for chrome.devtools.inspectedWindow.eval()
21+
*/
22+
type EvalScriptEntry = {
23+
fn: (...args: any[]) => any,
24+
code: (...args: any[]) => string,
25+
};
26+
27+
/*
28+
Can not access `Developer Tools Console API` (e.g., inspect(), $0) in this context.
29+
So some fallback functions are no-op or throw error.
30+
*/
31+
export const evalScripts: {[key: EvalScriptIds]: EvalScriptEntry} = {
32+
checkIfReactPresentInInspectedWindow: {
33+
fn: () =>
34+
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ &&
35+
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0,
36+
code: () =>
37+
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ &&' +
38+
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
39+
},
40+
reload: {
41+
fn: () => window.location.reload(),
42+
code: () => 'window.location.reload();',
43+
},
44+
setBrowserSelectionFromReact: {
45+
fn: () => {
46+
throw new Error('Not supported in fallback eval context');
47+
},
48+
code: () =>
49+
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
50+
'(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
51+
'false',
52+
},
53+
setReactSelectionFromBrowser: {
54+
fn: () => {
55+
throw new Error('Not supported in fallback eval context');
56+
},
57+
code: () =>
58+
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
59+
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
60+
'false',
61+
},
62+
viewAttributeSource: {
63+
fn: ({rendererID, elementID, path}) => {
64+
return false; // Not supported in fallback eval context
65+
},
66+
code: ({rendererID, elementID, path}) =>
67+
'{' + // The outer block is important because it means we can declare local variables.
68+
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
69+
JSON.stringify(rendererID) +
70+
');' +
71+
'if (renderer) {' +
72+
' const value = renderer.getElementAttributeByPath(' +
73+
JSON.stringify(elementID) +
74+
',' +
75+
JSON.stringify(path) +
76+
');' +
77+
' if (value) {' +
78+
' inspect(value);' +
79+
' true;' +
80+
' } else {' +
81+
' false;' +
82+
' }' +
83+
'} else {' +
84+
' false;' +
85+
'}' +
86+
'}',
87+
},
88+
viewElementSource: {
89+
fn: ({rendererID, elementID}) => {
90+
return false; // Not supported in fallback eval context
91+
},
92+
code: ({rendererID, elementID}) =>
93+
'{' + // The outer block is important because it means we can declare local variables.
94+
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
95+
JSON.stringify(rendererID) +
96+
');' +
97+
'if (renderer) {' +
98+
' const value = renderer.getElementSourceFunctionById(' +
99+
JSON.stringify(elementID) +
100+
');' +
101+
' if (value) {' +
102+
' inspect(value);' +
103+
' true;' +
104+
' } else {' +
105+
' false;' +
106+
' }' +
107+
'} else {' +
108+
' false;' +
109+
'}' +
110+
'}',
111+
},
112+
};

packages/react-devtools-extensions/src/main/elementSelection.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
/* global chrome */
1+
import {evalInInspectedWindow} from './evalInInspectedWindow';
22

33
export function setBrowserSelectionFromReact() {
44
// This is currently only called on demand when you press "view DOM".
55
// In the future, if Chrome adds an inspect() that doesn't switch tabs,
66
// we could make this happen automatically when you select another component.
7-
chrome.devtools.inspectedWindow.eval(
8-
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
9-
'(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
10-
'false',
7+
evalInInspectedWindow(
8+
'setBrowserSelectionFromReact',
9+
[],
1110
(didSelectionChange, evalError) => {
1211
if (evalError) {
1312
console.error(evalError);
@@ -19,10 +18,9 @@ export function setBrowserSelectionFromReact() {
1918
export function setReactSelectionFromBrowser(bridge) {
2019
// When the user chooses a different node in the browser Elements tab,
2120
// copy it over to the hook object so that we can sync the selection.
22-
chrome.devtools.inspectedWindow.eval(
23-
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
24-
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
25-
'false',
21+
evalInInspectedWindow(
22+
'setReactSelectionFromBrowser',
23+
[],
2624
(didSelectionChange, evalError) => {
2725
if (evalError) {
2826
console.error(evalError);

0 commit comments

Comments
 (0)