diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index e28290836a0e8b..09b4f058a5227a 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -29,8 +29,12 @@ import { store as reusableBlocksStore } from '@wordpress/reusable-blocks'; /** * Internal dependencies */ +import useNoRecursiveRenders from './use-no-recursive-renders'; export default function ReusableBlockEdit( { attributes: { ref }, clientId } ) { + const [ hasAlreadyRendered, RecursionProvider ] = useNoRecursiveRenders( + ref + ); const { isMissing, hasResolved } = useSelect( ( select ) => { const persistedBlock = select( coreStore ).getEntityRecord( @@ -83,6 +87,16 @@ export default function ReusableBlockEdit( { attributes: { ref }, clientId } ) { const blockProps = useBlockProps(); + if ( hasAlreadyRendered ) { + return ( +
+ + { __( 'Block cannot be rendered inside itself.' ) } + +
+ ); + } + if ( isMissing ) { return (
@@ -104,28 +118,30 @@ export default function ReusableBlockEdit( { attributes: { ref }, clientId } ) { } return ( -
- - - convertBlockToStatic( clientId ) } - > - { __( 'Convert to regular blocks' ) } - - - - - - - - -
- {
} + +
+ + + convertBlockToStatic( clientId ) } + > + { __( 'Convert to regular blocks' ) } + + + + + + + + +
+ {
} +
-
+
); } diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index 5fed48bd9a7c74..771adf27048db8 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -13,6 +13,8 @@ * @return string Rendered HTML of the referenced block. */ function render_block_core_block( $attributes ) { + static $seen_refs = array(); + if ( empty( $attributes['ref'] ) ) { return ''; } @@ -22,6 +24,31 @@ function render_block_core_block( $attributes ) { return ''; } + if ( in_array( $attributes['ref'], $seen_refs, true ) ) { + if ( ! is_admin() ) { + trigger_error( + sprintf( + // translators: %s is the user-provided title of the reusable block. + __( 'Could not render Reusable Block %s: blocks cannot be rendered inside themselves.' ), + $reusable_block->post_title + ), + E_USER_WARNING + ); + } + + // WP_DEBUG_DISPLAY must only be honored when WP_DEBUG. This precedent + // is set in `wp_debug_mode()`. + $is_debug = defined( 'WP_DEBUG' ) && WP_DEBUG && + defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY; + + return $is_debug ? + // translators: Visible only in the front end, this warning takes the place of a faulty block. + __( '[block rendering halted]' ) : + ''; + } + + $seen_refs[] = $attributes['ref']; + if ( 'publish' !== $reusable_block->post_status || ! empty( $reusable_block->post_password ) ) { return ''; } diff --git a/packages/block-library/src/block/test/use-no-recursive-renders.js b/packages/block-library/src/block/test/use-no-recursive-renders.js new file mode 100644 index 00000000000000..a9d9f6e53b789a --- /dev/null +++ b/packages/block-library/src/block/test/use-no-recursive-renders.js @@ -0,0 +1,100 @@ +/** + * External dependencies + */ +import { render } from '@testing-library/react'; + +/** + * WordPress dependencies + */ +import { Fragment } from '@wordpress/element'; + +// Mimics a block's Edit component, such as ReusableBlockEdit, which is capable +// of calling itself depending on its `ref` attribute. +function Edit( { attributes: { ref } } ) { + const [ hasAlreadyRendered, RecursionProvider ] = useNoRecursiveRenders( + ref + ); + + if ( hasAlreadyRendered ) { + return
Halt
; + } + + return ( + +
+ { ref === 'SIMPLE' &&

Done

} + { ref === 'SINGLY-RECURSIVE' && ( + + ) } + { ref === 'MUTUALLY-RECURSIVE-1' && ( + + ) } + { ref === 'MUTUALLY-RECURSIVE-2' && ( + + ) } +
+
+ ); +} + +/** + * Internal dependencies + */ +import useNoRecursiveRenders from '../use-no-recursive-renders'; + +describe( 'useNoRecursiveRenders', () => { + it( 'allows a single block to render', () => { + const { container } = render( + + ); + expect( + container.querySelectorAll( '.wp-block__reusable-block' ) + ).toHaveLength( 1 ); + expect( + container.querySelectorAll( '.wp-block__reusable-block--halted' ) + ).toHaveLength( 0 ); + } ); + + it( 'allows equal but sibling blocks to render', () => { + const { container } = render( + + + + + ); + expect( + container.querySelectorAll( '.wp-block__reusable-block' ) + ).toHaveLength( 2 ); + expect( + container.querySelectorAll( '.wp-block__reusable-block--halted' ) + ).toHaveLength( 0 ); + } ); + + it( 'prevents a block from rendering itself', () => { + const { container } = render( + + + + ); + expect( + container.querySelectorAll( '.wp-block__reusable-block' ) + ).toHaveLength( 1 ); + expect( + container.querySelectorAll( '.wp-block__reusable-block--halted' ) + ).toHaveLength( 1 ); + } ); + + it( 'prevents mutual recursion between two blocks', () => { + const { container } = render( + + + + ); + expect( + container.querySelectorAll( '.wp-block__reusable-block' ) + ).toHaveLength( 2 ); + expect( + container.querySelectorAll( '.wp-block__reusable-block--halted' ) + ).toHaveLength( 1 ); + } ); +} ); diff --git a/packages/block-library/src/block/use-no-recursive-renders.js b/packages/block-library/src/block/use-no-recursive-renders.js new file mode 100644 index 00000000000000..b17ddd9b4f6186 --- /dev/null +++ b/packages/block-library/src/block/use-no-recursive-renders.js @@ -0,0 +1,29 @@ +/** + * WordPress dependencies + */ +import { + createContext, + useCallback, + useContext, + useMemo, +} from '@wordpress/element'; + +const RenderedRefsContext = createContext( [] ); + +export default function useNoRecursiveRenders( ref ) { + const previouslyRenderedRefs = useContext( RenderedRefsContext ); + const hasAlreadyRendered = previouslyRenderedRefs.includes( ref ); + const newRenderedRefs = useMemo( () => [ ...previouslyRenderedRefs, ref ], [ + ref, + previouslyRenderedRefs, + ] ); + const Provider = useCallback( + ( { children } ) => ( + + { children } + + ), + [ newRenderedRefs ] + ); + return [ hasAlreadyRendered, Provider ]; +}