diff --git a/packages/react-dom/src/client/ToStringValue.js b/packages/react-dom/src/client/ToStringValue.js index 1cb8f60f0b5e..cf4a173e1399 100644 --- a/packages/react-dom/src/client/ToStringValue.js +++ b/packages/react-dom/src/client/ToStringValue.js @@ -38,22 +38,6 @@ export function getToStringValue(value: mixed): ToStringValue { } } -/** - * Returns true only if Trusted Types are available in global object and the value is a trusted type. - */ -let isTrustedTypesValue: (value: any) => boolean; -// $FlowExpectedError - TrustedTypes are defined only in some browsers or with polyfill -if (enableTrustedTypesIntegration && typeof trustedTypes !== 'undefined') { - isTrustedTypesValue = (value: any) => - trustedTypes.isHTML(value) || - trustedTypes.isScript(value) || - trustedTypes.isScriptURL(value) || - // TrustedURLs are deprecated and will be removed soon: https://github.com/WICG/trusted-types/pull/204 - (trustedTypes.isURL && trustedTypes.isURL(value)); -} else { - isTrustedTypesValue = () => false; -} - /** Trusted value is a wrapper for "safe" values which can be assigned to DOM execution sinks. */ export opaque type TrustedValue: {toString(): string, valueOf(): string} = { toString(): string, @@ -67,15 +51,21 @@ export opaque type TrustedValue: {toString(): string, valueOf(): string} = { * * If application uses Trusted Types we don't stringify trusted values, but preserve them as objects. */ -export function toStringOrTrustedType(value: any): string | TrustedValue { - if ( - enableTrustedTypesIntegration && - // fast-path string values as it's most frequent usage of the function - typeof value !== 'string' && - isTrustedTypesValue(value) - ) { - return value; - } else { - return '' + value; - } +export let toStringOrTrustedType: any => string | TrustedValue = toString; +if (enableTrustedTypesIntegration && typeof trustedTypes !== 'undefined') { + const isHTML = trustedTypes.isHTML; + const isScript = trustedTypes.isScript; + const isScriptURL = trustedTypes.isScriptURL; + // TrustedURLs are deprecated and will be removed soon: https://github.com/WICG/trusted-types/pull/204 + const isURL = trustedTypes.isURL ? trustedTypes.isURL : value => false; + toStringOrTrustedType = value => { + if ( + typeof value === 'object' && + (isHTML(value) || isScript(value) || isScriptURL(value) || isURL(value)) + ) { + // Pass Trusted Types through. + return value; + } + return toString(value); + }; } diff --git a/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js b/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js index 3a9eb4c15d16..b3511b43dc3a 100644 --- a/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js +++ b/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js @@ -1,13 +1,28 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + describe('when Trusted Types are available in global object', () => { let React; let ReactDOM; let ReactFeatureFlags; let container; + let ttObject1; + let ttObject2; beforeEach(() => { + jest.resetModules(); container = document.createElement('div'); + const fakeTTObjects = new Set(); window.trustedTypes = { - isHTML: () => true, + isHTML: value => fakeTTObjects.has(value), isScript: () => false, isScriptURL: () => false, }; @@ -15,28 +30,151 @@ describe('when Trusted Types are available in global object', () => { ReactFeatureFlags.enableTrustedTypesIntegration = true; React = require('react'); ReactDOM = require('react-dom'); + ttObject1 = { + toString() { + return 'Hi'; + }, + }; + ttObject2 = { + toString() { + return 'Bye'; + }, + }; + fakeTTObjects.add(ttObject1); + fakeTTObjects.add(ttObject2); }); afterEach(() => { delete window.trustedTypes; - ReactFeatureFlags.enableTrustedTypesIntegration = false; }); - it('should not stringify trusted values', () => { - const trustedObject = {toString: () => 'I look like a trusted object'}; - class Component extends React.Component { - state = {inner: undefined}; - render() { - return
; - } + it('should not stringify trusted values for dangerouslySetInnerHTML', () => { + let innerHTMLDescriptor = Object.getOwnPropertyDescriptor( + Element.prototype, + 'innerHTML', + ); + try { + const innerHTMLCalls = []; + Object.defineProperty(Element.prototype, 'innerHTML', { + get() { + return innerHTMLDescriptor.get.apply(this, arguments); + }, + set(value) { + innerHTMLCalls.push(value); + return innerHTMLDescriptor.set.apply(this, arguments); + }, + }); + ReactDOM.render( +
, + container, + ); + expect(container.innerHTML).toBe('
Hi
'); + expect(innerHTMLCalls.length).toBe(1); + // Ensure it didn't get stringified when passed to a DOM sink: + expect(innerHTMLCalls[0]).toBe(ttObject1); + + innerHTMLCalls.length = 0; + ReactDOM.render( +
, + container, + ); + expect(container.innerHTML).toBe('
Bye
'); + expect(innerHTMLCalls.length).toBe(1); + // Ensure it didn't get stringified when passed to a DOM sink: + expect(innerHTMLCalls[0]).toBe(ttObject2); + } finally { + Object.defineProperty( + Element.prototype, + 'innerHTML', + innerHTMLDescriptor, + ); + } + }); + + it('should not stringify trusted values for setAttribute (unknown attribute)', () => { + let setAttribute = Element.prototype.setAttribute; + try { + const setAttributeCalls = []; + Element.prototype.setAttribute = function(name, value) { + setAttributeCalls.push([this, name.toLowerCase(), value]); + return setAttribute.apply(this, arguments); + }; + ReactDOM.render(
, container); + expect(container.innerHTML).toBe('
'); + expect(setAttributeCalls.length).toBe(1); + expect(setAttributeCalls[0][0]).toBe(container.firstChild); + expect(setAttributeCalls[0][1]).toBe('data-foo'); + // Ensure it didn't get stringified when passed to a DOM sink: + expect(setAttributeCalls[0][2]).toBe(ttObject1); + + setAttributeCalls.length = 0; + ReactDOM.render(
, container); + expect(setAttributeCalls.length).toBe(1); + expect(setAttributeCalls[0][0]).toBe(container.firstChild); + expect(setAttributeCalls[0][1]).toBe('data-foo'); + // Ensure it didn't get stringified when passed to a DOM sink: + expect(setAttributeCalls[0][2]).toBe(ttObject2); + } finally { + Element.prototype.setAttribute = setAttribute; } + }); + + it('should not stringify trusted values for setAttribute (known attribute)', () => { + let setAttribute = Element.prototype.setAttribute; + try { + const setAttributeCalls = []; + Element.prototype.setAttribute = function(name, value) { + setAttributeCalls.push([this, name.toLowerCase(), value]); + return setAttribute.apply(this, arguments); + }; + ReactDOM.render(
, container); + expect(container.innerHTML).toBe('
'); + expect(setAttributeCalls.length).toBe(1); + expect(setAttributeCalls[0][0]).toBe(container.firstChild); + expect(setAttributeCalls[0][1]).toBe('class'); + // Ensure it didn't get stringified when passed to a DOM sink: + expect(setAttributeCalls[0][2]).toBe(ttObject1); + + setAttributeCalls.length = 0; + ReactDOM.render(
, container); + expect(setAttributeCalls.length).toBe(1); + expect(setAttributeCalls[0][0]).toBe(container.firstChild); + expect(setAttributeCalls[0][1]).toBe('class'); + // Ensure it didn't get stringified when passed to a DOM sink: + expect(setAttributeCalls[0][2]).toBe(ttObject2); + } finally { + Element.prototype.setAttribute = setAttribute; + } + }); - const isHTMLSpy = jest.spyOn(window.trustedTypes, ['isHTML']); - const instance = ReactDOM.render(, container); - instance.setState({inner: trustedObject}); + it('should not stringify trusted values for setAttributeNS', () => { + let setAttributeNS = Element.prototype.setAttributeNS; + try { + const setAttributeNSCalls = []; + Element.prototype.setAttributeNS = function(ns, name, value) { + setAttributeNSCalls.push([this, ns, name, value]); + return setAttributeNS.apply(this, arguments); + }; + ReactDOM.render(, container); + expect(container.innerHTML).toBe(''); + expect(setAttributeNSCalls.length).toBe(1); + expect(setAttributeNSCalls[0][0]).toBe(container.firstChild); + expect(setAttributeNSCalls[0][1]).toBe('http://www.w3.org/1999/xlink'); + expect(setAttributeNSCalls[0][2]).toBe('xlink:href'); + // Ensure it didn't get stringified when passed to a DOM sink: + expect(setAttributeNSCalls[0][3]).toBe(ttObject1); - expect(container.firstChild.innerHTML).toBe(trustedObject.toString()); - expect(isHTMLSpy).toHaveBeenCalledWith(trustedObject); + setAttributeNSCalls.length = 0; + ReactDOM.render(, container); + expect(setAttributeNSCalls.length).toBe(1); + expect(setAttributeNSCalls[0][0]).toBe(container.firstChild); + expect(setAttributeNSCalls[0][1]).toBe('http://www.w3.org/1999/xlink'); + expect(setAttributeNSCalls[0][2]).toBe('xlink:href'); + // Ensure it didn't get stringified when passed to a DOM sink: + expect(setAttributeNSCalls[0][3]).toBe(ttObject2); + } finally { + Element.prototype.setAttributeNS = setAttributeNS; + } }); describe('dangerouslySetInnerHTML in svg elements in Internet Explorer', () => { @@ -81,6 +219,7 @@ describe('when Trusted Types are available in global object', () => { "You can try to wrap your svg element inside a div and use 'dangerouslySetInnerHTML' " + 'on the enclosing div instead.', ); + expect(container.innerHTML).toBe('unsafe html'); }); }); @@ -95,7 +234,7 @@ describe('when Trusted Types are available in global object', () => { ' in script (at **)', ); - // check that the warning is print only once + // check that the warning is printed only once ReactDOM.render(, container); }); }); diff --git a/packages/react-dom/src/client/setInnerHTML.js b/packages/react-dom/src/client/setInnerHTML.js index 4fa94a4e95c9..2b3227bdcaba 100644 --- a/packages/react-dom/src/client/setInnerHTML.js +++ b/packages/react-dom/src/client/setInnerHTML.js @@ -27,23 +27,26 @@ const setInnerHTML = createMicrosoftUnsafeLocalFunction(function( node: Element, html: string | TrustedValue, ): void { - // IE does not have innerHTML for SVG nodes, so instead we inject the - // new markup in a temp node and then move the child nodes across into - // the target node if (node.namespaceURI === Namespaces.svg) { - if (enableTrustedTypesIntegration && __DEV__) { - warning( - // $FlowExpectedError - trustedTypes are defined only in some browsers or with polyfill - typeof trustedTypes === 'undefined', - "Using 'dangerouslySetInnerHTML' in an svg element with " + - 'Trusted Types enabled in an Internet Explorer will cause ' + - 'the trusted value to be converted to string. Assigning string ' + - "to 'innerHTML' will throw an error if Trusted Types are enforced. " + - "You can try to wrap your svg element inside a div and use 'dangerouslySetInnerHTML' " + - 'on the enclosing div instead.', - ); + if (__DEV__) { + if (enableTrustedTypesIntegration) { + // TODO: reconsider the text of this warning and when it should show + // before enabling the feature flag. + warning( + typeof trustedTypes === 'undefined', + "Using 'dangerouslySetInnerHTML' in an svg element with " + + 'Trusted Types enabled in an Internet Explorer will cause ' + + 'the trusted value to be converted to string. Assigning string ' + + "to 'innerHTML' will throw an error if Trusted Types are enforced. " + + "You can try to wrap your svg element inside a div and use 'dangerouslySetInnerHTML' " + + 'on the enclosing div instead.', + ); + } } if (!('innerHTML' in node)) { + // IE does not have innerHTML for SVG nodes, so instead we inject the + // new markup in a temp node and then move the child nodes across into + // the target node reusableSVGContainer = reusableSVGContainer || document.createElement('div'); reusableSVGContainer.innerHTML = @@ -55,12 +58,10 @@ const setInnerHTML = createMicrosoftUnsafeLocalFunction(function( while (svgNode.firstChild) { node.appendChild(svgNode.firstChild); } - } else { - node.innerHTML = (html: any); + return; } - } else { - node.innerHTML = (html: any); } + node.innerHTML = (html: any); }); export default setInnerHTML; diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 8a0806c4a974..8eeda9784000 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -16,6 +16,14 @@ declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: any; /*?{ inject: ?((stuff: Object) => void) };*/ +declare var trustedTypes: {| + isHTML: (value: any) => boolean, + isScript: (value: any) => boolean, + isScriptURL: (value: any) => boolean, + // TrustedURLs are deprecated and will be removed soon: https://github.com/WICG/trusted-types/pull/204 + isURL?: (value: any) => boolean, +|}; + // ReactFeatureFlags www fork declare module 'ReactFeatureFlags' { declare module.exports: any;