diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index 33bd3c0f7dd35..160546d9b7806 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -406,6 +406,12 @@ function setProp( break; } // These attributes accept URLs. These must not allow javascript: URLS. + case 'data': + if (tag !== 'object') { + setValueForKnownAttribute(domElement, 'data', value); + break; + } + // fallthrough case 'src': case 'href': { if (enableFilterEmptyStringAttributesDOM) { @@ -2453,13 +2459,22 @@ function diffHydratedGenericElement( warnForPropDifference(propKey, serverValue, value, serverDifferences); continue; } + case 'data': + if (tag !== 'object') { + extraAttributes.delete(propKey); + const serverValue = (domElement: any).getAttribute('data'); + warnForPropDifference(propKey, serverValue, value, serverDifferences); + continue; + } + // fallthrough case 'src': case 'href': if (enableFilterEmptyStringAttributesDOM) { if ( value === '' && // is fine for "reload" links. - !(tag === 'a' && propKey === 'href') + !(tag === 'a' && propKey === 'href') && + !(tag === 'object' && propKey === 'data') ) { if (__DEV__) { if (propKey === 'src') { diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 5712e935a6e75..0c9fc0fc0484c 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -1601,6 +1601,73 @@ function pushStartAnchor( return children; } +function pushStartObject( + target: Array, + props: Object, +): ReactNodeList { + target.push(startChunkForTag('object')); + + let children = null; + let innerHTML = null; + for (const propKey in props) { + if (hasOwnProperty.call(props, propKey)) { + const propValue = props[propKey]; + if (propValue == null) { + continue; + } + switch (propKey) { + case 'children': + children = propValue; + break; + case 'dangerouslySetInnerHTML': + innerHTML = propValue; + break; + case 'data': { + if (__DEV__) { + checkAttributeStringCoercion(propValue, 'data'); + } + const sanitizedValue = sanitizeURL('' + propValue); + if (enableFilterEmptyStringAttributesDOM) { + if (sanitizedValue === '') { + if (__DEV__) { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + propKey, + propKey, + ); + } + break; + } + } + target.push( + attributeSeparator, + stringToChunk('data'), + attributeAssign, + stringToChunk(escapeTextForBrowser(sanitizedValue)), + attributeEnd, + ); + break; + } + default: + pushAttribute(target, propKey, propValue); + break; + } + } + } + + target.push(endOfStartTag); + pushInnerHTML(target, innerHTML, children); + if (typeof children === 'string') { + // Special case children as a string to avoid the unnecessary comment. + // TODO: Remove this special case after the general optimization is in place. + target.push(stringToChunk(encodeHTMLTextNode(children))); + return null; + } + return children; +} + function pushStartSelect( target: Array, props: Object, @@ -3569,6 +3636,8 @@ export function pushStartInstance( return pushStartForm(target, props, resumableState, renderState); case 'menuitem': return pushStartMenuItem(target, props); + case 'object': + return pushStartObject(target, props); case 'title': return pushTitle( target, diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationObject-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationObject-test.js new file mode 100644 index 0000000000000..5e48134052bee --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationObject-test.js @@ -0,0 +1,55 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment + */ + +'use strict'; + +const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); + +let React; +let ReactDOMClient; +let ReactDOMServer; + +function initModules() { + // Reset warning cache. + jest.resetModules(); + React = require('react'); + ReactDOMClient = require('react-dom/client'); + ReactDOMServer = require('react-dom/server'); + + // Make them available to the helpers. + return { + ReactDOMClient, + ReactDOMServer, + }; +} + +const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules); + +describe('ReactDOMServerIntegrationObject', () => { + beforeEach(() => { + resetModules(); + }); + + itRenders('an object with children', async render => { + const e = await render( + +
preview
+
, + ); + + expect(e.outerHTML).toBe( + '
preview
', + ); + }); + + itRenders('an object with empty data', async render => { + const e = await render(, 1); + expect(e.outerHTML).toBe(''); + }); +}); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js index dfdb9ffd23757..384e8beb0b214 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js @@ -69,6 +69,25 @@ describe('ReactDOMServerIntegration - Untrusted URLs', () => { expect(e.lastChild.href).toBe(EXPECTED_SAFE_URL); }); + itRenders('sanitizes on various tags', async render => { + const aElement = await render(); + expect(aElement.href).toBe(EXPECTED_SAFE_URL); + + const objectElement = await render(); + expect(objectElement.data).toBe(EXPECTED_SAFE_URL); + + const embedElement = await render(); + expect(embedElement.src).toBe(EXPECTED_SAFE_URL); + }); + + itRenders('passes through data on non-object tags', async render => { + const div = await render(