Skip to content

Commit

Permalink
refactor[devtools]: forbid editing class instances in props
Browse files Browse the repository at this point in the history
  • Loading branch information
hoxyq committed Mar 30, 2023
1 parent 0ffc7f6 commit ab8fa95
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 5 deletions.
27 changes: 27 additions & 0 deletions packages/react-devtools-shared/src/__tests__/utils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import {
getDisplayName,
getDisplayNameForReactElement,
isPlainObject,
} from 'react-devtools-shared/src/utils';
import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
import {
Expand Down Expand Up @@ -270,4 +271,30 @@ describe('utils', () => {
expect(gte('10.0.0', '9.0.0')).toBe(true);
});
});

describe('isPlainObject', () => {
it('should return true for plain objects', () => {
expect(isPlainObject({})).toBe(true);
expect(isPlainObject({a: 1})).toBe(true);
expect(isPlainObject({a: {b: {c: 123}}})).toBe(true);
});

it('should return false if object is a class instance', () => {
expect(isPlainObject(new (class C {})())).toBe(false);
});

it('should retun false for objects, which have not only Object in its prototype chain', () => {
expect(isPlainObject([])).toBe(false);
expect(isPlainObject(Symbol())).toBe(false);
});

it('should retun false for primitives', () => {
expect(isPlainObject(5)).toBe(false);
expect(isPlainObject(true)).toBe(false);
});

it('should return true for objects with no prototype', () => {
expect(isPlainObject(Object.create(null))).toBe(true);
});
});
});
37 changes: 32 additions & 5 deletions packages/react-devtools-shared/src/hydration.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export type Unserializable = {
size?: number,
type: string,
unserializable: boolean,
...
[string | number]: any,
};

// This threshold determines the depth at which the bridge "dehydrates" nested data.
Expand Down Expand Up @@ -248,7 +248,6 @@ export function dehydrate(
// Other types (e.g. typed arrays, Sets) will not spread correctly.
Array.from(data).forEach(
(item, i) =>
// $FlowFixMe[prop-missing] Unserializable doesn't have an index signature
(unserializableValue[i] = dehydrate(
item,
cleaned,
Expand Down Expand Up @@ -296,6 +295,7 @@ export function dehydrate(

case 'object':
isPathAllowedCheck = isPathAllowed(path);

if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
return createDehydrated(type, true, data, cleaned, path);
} else {
Expand All @@ -316,15 +316,42 @@ export function dehydrate(
return object;
}

case 'class_instance':
isPathAllowedCheck = isPathAllowed(path);

const value: Unserializable = {
unserializable: true,
type,
readonly: true,
preview_short: formatDataForPreview(data, false),
preview_long: formatDataForPreview(data, true),
name: data.constructor.name,
};

getAllEnumerableKeys(data).forEach(key => {
const keyAsString = key.toString();

value[keyAsString] = dehydrate(
data[key],
cleaned,
unserializable,
path.concat([keyAsString]),
isPathAllowed,
isPathAllowedCheck ? 1 : level + 1,
);
});

unserializable.push(path);

return value;

case 'infinity':
case 'nan':
case 'undefined':
// Some values are lossy when sent through a WebSocket.
// We dehydrate+rehydrate them to preserve their type.
cleaned.push(path);
return {
type,
};
return {type};

default:
return data;
Expand Down
15 changes: 15 additions & 0 deletions packages/react-devtools-shared/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ export type DataType =
| 'array_buffer'
| 'bigint'
| 'boolean'
| 'class_instance'
| 'data_view'
| 'date'
| 'function'
Expand Down Expand Up @@ -620,6 +621,9 @@ export function getDataType(data: Object): DataType {
return 'html_all_collection';
}
}

if (!isPlainObject(data)) return 'class_instance';

return 'object';
case 'string':
return 'string';
Expand Down Expand Up @@ -835,6 +839,8 @@ export function formatDataForPreview(
}
case 'date':
return data.toString();
case 'class_instance':
return data.constructor.name;
case 'object':
if (showFormattedValue) {
const keys = Array.from(getAllEnumerableKeys(data)).sort(alphaSortKeys);
Expand Down Expand Up @@ -873,3 +879,12 @@ export function formatDataForPreview(
}
}
}

// Basically checking that the object only has Object in its prototype chain
export const isPlainObject = (object: Object): boolean => {
const objectPrototype = Object.getPrototypeOf(object);
if (!objectPrototype) return true;

const objectParentPrototype = Object.getPrototypeOf(objectPrototype);
return objectParentPrototype == null;
};

0 comments on commit ab8fa95

Please sign in to comment.