main
js 285 lines 7.33 KB
Raw
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 /* global chrome, ExtensionRuntimePort */
10
11 'use strict';
12
13 import './dynamicallyInjectContentScripts';
14 import './tabsManager';
15
16 import {
17 handleDevToolsPageMessage,
18 handleBackendManagerMessage,
19 handleReactDevToolsHookMessage,
20 handleFetchResourceContentScriptMessage,
21 } from './messageHandlers';
22 import {
23 EXTENSION_BRIDGE_CONNECTION_DISCONNECTED,
24 EXTENSION_BRIDGE_CONNECTION_READY,
25 } from '../constants';
26 import type {ExtensionBridgeConnectionType} from '../constants';
27
28 const ports: {
29 // TODO: Check why we convert tab IDs to strings, and if we can avoid it
30 [tabId: string]: {
31 extension: ExtensionRuntimePort | null,
32 proxy: ExtensionRuntimePort | null,
33 disconnectPipe: Function | null,
34 },
35 } = {};
36
37 function registerTab(tabId: number) {
38 // $FlowFixMe[incompatible-type]
39 if (!ports[tabId]) {
40 // $FlowFixMe[incompatible-type]
41 ports[tabId] = {
42 extension: null,
43 proxy: null,
44 disconnectPipe: null,
45 };
46 }
47 }
48
49 function registerExtensionPort(port: ExtensionRuntimePort, tabId: number) {
50 // $FlowFixMe[incompatible-type]
51 ports[tabId].extension = port;
52
53 port.onDisconnect.addListener(() => {
54 // This should delete disconnectPipe from ports dictionary
55 // $FlowFixMe[incompatible-type]
56 ports[tabId].disconnectPipe?.();
57
58 // $FlowFixMe[incompatible-type]
59 ports[tabId].extension = null;
60 });
61 }
62
63 function registerProxyPort(port: ExtensionRuntimePort, tabId: string) {
64 ports[tabId].proxy = port;
65
66 // In case proxy port was disconnected from the other end, from content script
67 // This can happen if content script was detached, when user does in-tab navigation
68 // This listener should never be called when we call port.disconnect() from this (background/index.js) script
69 port.onDisconnect.addListener(() => {
70 ports[tabId].disconnectPipe?.();
71
72 ports[tabId].proxy = null;
73 });
74 }
75
76 function isNumeric(str: string): boolean {
77 return +str + '' === str;
78 }
79
80 chrome.runtime.onConnect.addListener(port => {
81 if (port.name === 'proxy') {
82 // Might not be present for restricted pages in Firefox
83 if (port.sender?.tab?.id == null) {
84 // Not disconnecting it, so it would not reconnect
85 return;
86 }
87
88 // Proxy content script is executed in tab, so it should have it specified.
89 const tabId = port.sender.tab.id;
90
91 // $FlowFixMe[incompatible-type]
92 const registeredPort = ports[tabId];
93 const proxy = registeredPort?.proxy;
94 if (proxy) {
95 registeredPort.disconnectPipe?.();
96 proxy.disconnect();
97 }
98
99 registerTab(tabId);
100 registerProxyPort(
101 port,
102 // $FlowFixMe[incompatible-type]
103 tabId,
104 );
105
106 // $FlowFixMe[incompatible-type]
107 if (ports[tabId].extension) {
108 connectExtensionAndProxyPorts(
109 ports[tabId].extension,
110 ports[tabId].proxy,
111 tabId,
112 );
113 }
114
115 return;
116 }
117
118 if (isNumeric(port.name)) {
119 // DevTools page port doesn't have tab id specified, because its sender is the extension.
120 const tabId = +port.name;
121
122 registerTab(tabId);
123 registerExtensionPort(
124 port,
125 // $FlowFixMe[incompatible-call]
126 tabId,
127 );
128
129 // $FlowFixMe[incompatible-type]
130 if (ports[tabId].proxy) {
131 connectExtensionAndProxyPorts(
132 ports[tabId].extension,
133 ports[tabId].proxy,
134 tabId,
135 );
136 }
137
138 return;
139 }
140
141 // I am not sure if we should throw here
142 console.warn(`Unknown port ${port.name} connected`);
143 });
144
145 function connectExtensionAndProxyPorts(
146 maybeExtensionPort: ExtensionRuntimePort | null,
147 maybeProxyPort: ExtensionRuntimePort | null,
148 tabId: number,
149 ) {
150 if (!maybeExtensionPort) {
151 throw new Error(
152 `Attempted to connect ports, when extension port is not present`,
153 );
154 }
155 const extensionPort = maybeExtensionPort;
156
157 if (!maybeProxyPort) {
158 throw new Error(
159 `Attempted to connect ports, when proxy port is not present`,
160 );
161 }
162 const proxyPort = maybeProxyPort;
163
164 function sendBridgeConnectionMessage(
165 port: ExtensionRuntimePort,
166 type: ExtensionBridgeConnectionType,
167 ) {
168 try {
169 port.postMessage({
170 source: 'react-devtools-background',
171 payload: {type},
172 });
173 } catch (error) {
174 // The port disconnected before the status update could be delivered.
175 }
176 }
177
178 // $FlowFixMe[incompatible-type]
179 if (ports[tabId].disconnectPipe) {
180 throw new Error(
181 `Attempted to connect already connected ports for tab with id ${tabId}`,
182 );
183 }
184
185 let didDisconnect = false;
186
187 function extensionPortMessageListener(message: mixed) {
188 try {
189 proxyPort.postMessage(message);
190 } catch (e) {
191 if (__DEV__) {
192 console.log(`Broken pipe ${tabId}: `, e);
193 }
194
195 disconnectListener();
196 }
197 }
198
199 function proxyPortMessageListener(message: mixed) {
200 try {
201 extensionPort.postMessage(message);
202 } catch (e) {
203 if (__DEV__) {
204 console.log(`Broken pipe ${tabId}: `, e);
205 }
206
207 disconnectListener();
208 }
209 }
210
211 function disconnectListener() {
212 if (didDisconnect) {
213 return;
214 }
215 didDisconnect = true;
216
217 extensionPort.onMessage.removeListener(extensionPortMessageListener);
218 proxyPort.onMessage.removeListener(proxyPortMessageListener);
219
220 sendBridgeConnectionMessage(
221 extensionPort,
222 EXTENSION_BRIDGE_CONNECTION_DISCONNECTED,
223 );
224 sendBridgeConnectionMessage(
225 proxyPort,
226 EXTENSION_BRIDGE_CONNECTION_DISCONNECTED,
227 );
228
229 // We handle disconnect() calls manually, based on each specific case
230 // No need to disconnect other port here
231
232 // $FlowFixMe[incompatible-type]
233 delete ports[tabId].disconnectPipe;
234 }
235
236 ports[tabId].disconnectPipe = disconnectListener;
237
238 extensionPort.onMessage.addListener(extensionPortMessageListener);
239 proxyPort.onMessage.addListener(proxyPortMessageListener);
240
241 extensionPort.onDisconnect.addListener(disconnectListener);
242 proxyPort.onDisconnect.addListener(disconnectListener);
243
244 // The proxy owns the backend message queue. Once both forwarding listeners
245 // are installed, tell it to flush that queue through this pipe. It echoes the
246 // message to the frontend after the queued backend messages have been sent.
247 sendBridgeConnectionMessage(proxyPort, EXTENSION_BRIDGE_CONNECTION_READY);
248 }
249
250 chrome.runtime.onMessage.addListener((message, sender) => {
251 switch (message?.source) {
252 case 'devtools-page': {
253 handleDevToolsPageMessage(message);
254 break;
255 }
256 case 'react-devtools-fetch-resource-content-script': {
257 handleFetchResourceContentScriptMessage(message);
258 break;
259 }
260 case 'react-devtools-backend-manager': {
261 handleBackendManagerMessage(message, sender);
262 break;
263 }
264 case 'react-devtools-hook': {
265 handleReactDevToolsHookMessage(message, sender);
266 }
267 }
268 });
269
270 chrome.tabs.onActivated.addListener(({tabId: activeTabId}) => {
271 for (const registeredTabId in ports) {
272 if (
273 ports[registeredTabId].proxy != null &&
274 ports[registeredTabId].extension != null
275 ) {
276 const numericRegisteredTabId = +registeredTabId;
277 const event =
278 activeTabId === numericRegisteredTabId
279 ? 'resumeElementPolling'
280 : 'pauseElementPolling';
281
282 ports[registeredTabId].extension.postMessage({event});
283 }
284 }
285 });