Skip to content

Commit 221f300

Browse files
authored
chore[DevTools]: make clipboardWrite optional for chromium (#32262)
Addresses #32244. ### Chromium We will use [chrome.permissions](https://developer.chrome.com/docs/extensions/reference/api/permissions) for checking / requesting `clipboardWrite` permission before copying something to the clipboard. ### Firefox We will keep `clipboardWrite` as a required permission, because there is no reliable and working API for requesting optional permissions for extensions that are extending browser DevTools: - `chrome.permissions` is unavailable for devtools pages - https://bugzilla.mozilla.org/show_bug.cgi?id=1796933 - You can't call `chrome.permissions.request` from background, because this instruction has to be executed inside user-event callback, basically only initiated by user. I don't really want to come up with solutions like opening a new tab with a button that user has to click.
1 parent 55b54b0 commit 221f300

File tree

16 files changed

+136
-33
lines changed

16 files changed

+136
-33
lines changed

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,13 +500,15 @@ module.exports = {
500500
'packages/react-devtools-shared/src/hook.js',
501501
'packages/react-devtools-shared/src/backend/console.js',
502502
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
503+
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
503504
],
504505
globals: {
505506
__IS_CHROME__: 'readonly',
506507
__IS_FIREFOX__: 'readonly',
507508
__IS_EDGE__: 'readonly',
508509
__IS_NATIVE__: 'readonly',
509510
__IS_INTERNAL_VERSION__: 'readonly',
511+
chrome: 'readonly',
510512
},
511513
},
512514
{

packages/react-devtools-extensions/chrome/manifest.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@
4343
"permissions": [
4444
"scripting",
4545
"storage",
46-
"tabs",
46+
"tabs"
47+
],
48+
"optional_permissions": [
4749
"clipboardWrite"
4850
],
4951
"host_permissions": [

packages/react-devtools-extensions/edge/manifest.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@
4343
"permissions": [
4444
"scripting",
4545
"storage",
46-
"tabs",
46+
"tabs"
47+
],
48+
"optional_permissions": [
4749
"clipboardWrite"
4850
],
4951
"host_permissions": [

packages/react-devtools-shared/src/devtools/ContextMenu/types.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import type {Node as ReactNode} from 'react';
1111

1212
export type ContextMenuItem = {
13-
onClick: () => void,
13+
onClick: () => mixed,
1414
content: ReactNode,
1515
};
1616

packages/react-devtools-shared/src/devtools/store.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
currentBridgeProtocol,
3939
} from 'react-devtools-shared/src/bridge';
4040
import {StrictMode} from 'react-devtools-shared/src/frontend/types';
41+
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
4142

4243
import type {
4344
Element,
@@ -1494,7 +1495,7 @@ export default class Store extends EventEmitter<{
14941495
};
14951496

14961497
onSaveToClipboard: (text: string) => void = text => {
1497-
copy(text);
1498+
withPermissionsCheck({permissions: ['clipboardWrite']}, () => copy(text))();
14981499
};
14991500

15001501
onBackendInitialized: () => void = () => {

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContextTree.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
ElementTypeClass,
2020
ElementTypeFunction,
2121
} from 'react-devtools-shared/src/frontend/types';
22+
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
2223

2324
import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
2425
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
@@ -41,14 +42,18 @@ export default function InspectedElementContextTree({
4142

4243
const isReadOnly = type !== ElementTypeClass && type !== ElementTypeFunction;
4344

44-
const entries = context != null ? Object.entries(context) : null;
45-
if (entries !== null) {
46-
entries.sort(alphaSortEntries);
45+
if (context == null) {
46+
return null;
4747
}
4848

49-
const isEmpty = entries === null || entries.length === 0;
49+
const entries = Object.entries(context);
50+
entries.sort(alphaSortEntries);
51+
const isEmpty = entries.length === 0;
5052

51-
const handleCopy = () => copy(serializeDataForCopy(((context: any): Object)));
53+
const handleCopy = withPermissionsCheck(
54+
{permissions: ['clipboardWrite']},
55+
() => copy(serializeDataForCopy(context)),
56+
);
5257

5358
// We add an object with a "value" key as a wrapper around Context data
5459
// so that we can use the shared <KeyValue> component to display it.

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {alphaSortEntries, serializeDataForCopy} from '../utils';
1818
import Store from '../../store';
1919
import styles from './InspectedElementSharedStyles.css';
2020
import {ElementTypeClass} from 'react-devtools-shared/src/frontend/types';
21+
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
2122

2223
import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
2324
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
@@ -53,17 +54,19 @@ export default function InspectedElementPropsTree({
5354
const canRenamePaths =
5455
type === ElementTypeClass || canEditFunctionPropsRenamePaths;
5556

56-
const entries = props != null ? Object.entries(props) : null;
57-
if (entries === null) {
58-
// Skip the section for null props.
57+
// Skip the section for null props.
58+
if (props == null) {
5959
return null;
6060
}
6161

62+
const entries = Object.entries(props);
6263
entries.sort(alphaSortEntries);
63-
6464
const isEmpty = entries.length === 0;
6565

66-
const handleCopy = () => copy(serializeDataForCopy(((props: any): Object)));
66+
const handleCopy = withPermissionsCheck(
67+
{permissions: ['clipboardWrite']},
68+
() => copy(serializeDataForCopy(props)),
69+
);
6770

6871
return (
6972
<div data-testname="InspectedElementPropsTree">
@@ -76,7 +79,7 @@ export default function InspectedElementPropsTree({
7679
)}
7780
</div>
7881
{!isEmpty &&
79-
(entries: any).map(([name, value]) => (
82+
entries.map(([name, value]) => (
8083
<KeyValue
8184
key={name}
8285
alphaSort={true}

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSourcePanel.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {toNormalUrl} from 'jsc-safe-url';
1414
import Button from '../Button';
1515
import ButtonIcon from '../ButtonIcon';
1616
import Skeleton from './Skeleton';
17+
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
1718

1819
import type {Source as InspectedElementSource} from 'react-devtools-shared/src/shared/types';
1920
import styles from './InspectedElementSourcePanel.css';
@@ -59,7 +60,10 @@ function CopySourceButton({source, symbolicatedSourcePromise}: Props) {
5960
const symbolicatedSource = React.use(symbolicatedSourcePromise);
6061
if (symbolicatedSource == null) {
6162
const {sourceURL, line, column} = source;
62-
const handleCopy = () => copy(`${sourceURL}:${line}:${column}`);
63+
const handleCopy = withPermissionsCheck(
64+
{permissions: ['clipboardWrite']},
65+
() => copy(`${sourceURL}:${line}:${column}`),
66+
);
6367

6468
return (
6569
<Button onClick={handleCopy} title="Copy to clipboard">
@@ -69,7 +73,10 @@ function CopySourceButton({source, symbolicatedSourcePromise}: Props) {
6973
}
7074

7175
const {sourceURL, line, column} = symbolicatedSource;
72-
const handleCopy = () => copy(`${sourceURL}:${line}:${column}`);
76+
const handleCopy = withPermissionsCheck(
77+
{permissions: ['clipboardWrite']},
78+
() => copy(`${sourceURL}:${line}:${column}`),
79+
);
7380

7481
return (
7582
<Button onClick={handleCopy} title="Copy to clipboard">

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStateTree.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import KeyValue from './KeyValue';
1616
import {alphaSortEntries, serializeDataForCopy} from '../utils';
1717
import Store from '../../store';
1818
import styles from './InspectedElementSharedStyles.css';
19+
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
1920

2021
import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
2122
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
@@ -35,22 +36,23 @@ export default function InspectedElementStateTree({
3536
store,
3637
}: Props): React.Node {
3738
const {state, type} = inspectedElement;
39+
if (state == null) {
40+
return null;
41+
}
3842

3943
// HostSingleton and HostHoistable may have state that we don't want to expose to users
4044
const isHostComponent = type === ElementTypeHostComponent;
41-
42-
const entries = state != null ? Object.entries(state) : null;
43-
const isEmpty = entries === null || entries.length === 0;
44-
45+
const entries = Object.entries(state);
46+
const isEmpty = entries.length === 0;
4547
if (isEmpty || isHostComponent) {
4648
return null;
4749
}
4850

49-
if (entries !== null) {
50-
entries.sort(alphaSortEntries);
51-
}
52-
53-
const handleCopy = () => copy(serializeDataForCopy(((state: any): Object)));
51+
entries.sort(alphaSortEntries);
52+
const handleCopy = withPermissionsCheck(
53+
{permissions: ['clipboardWrite']},
54+
() => copy(serializeDataForCopy(state)),
55+
);
5456

5557
return (
5658
<div>

packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {serializeDataForCopy} from '../../utils';
2020
import AutoSizeInput from './AutoSizeInput';
2121
import styles from './StyleEditor.css';
2222
import {sanitizeForParse} from '../../../utils';
23+
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
2324

2425
import type {Style} from './types';
2526

@@ -62,7 +63,10 @@ export default function StyleEditor({id, style}: Props): React.Node {
6263

6364
const keys = useMemo(() => Array.from(Object.keys(style)), [style]);
6465

65-
const handleCopy = () => copy(serializeDataForCopy(style));
66+
const handleCopy = withPermissionsCheck(
67+
{permissions: ['clipboardWrite']},
68+
() => copy(serializeDataForCopy(style)),
69+
);
6670

6771
return (
6872
<div className={styles.StyleEditor}>

0 commit comments

Comments
 (0)