diff --git a/packages/block-library/src/pattern/edit.js b/packages/block-library/src/pattern/edit.js
index e01fb37eb849e..7befaa9cb2dee 100644
--- a/packages/block-library/src/pattern/edit.js
+++ b/packages/block-library/src/pattern/edit.js
@@ -3,12 +3,19 @@
*/
import { cloneBlock } from '@wordpress/blocks';
import { useSelect, useDispatch } from '@wordpress/data';
-import { useEffect } from '@wordpress/element';
+import { useState, useEffect } from '@wordpress/element';
import {
+ Warning,
store as blockEditorStore,
useBlockProps,
} from '@wordpress/block-editor';
import { store as coreStore } from '@wordpress/core-data';
+import { __, sprintf } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { useParsePatternDependencies } from './recursion-detector';
const PatternEdit = ( { attributes, clientId } ) => {
const selectedPattern = useSelect(
@@ -32,6 +39,9 @@ const PatternEdit = ( { attributes, clientId } ) => {
const { getBlockRootClientId, getBlockEditingMode } =
useSelect( blockEditorStore );
+ const [ hasRecursionError, setHasRecursionError ] = useState( false );
+ const parsePatternDependencies = useParsePatternDependencies();
+
// Duplicated in packages/edit-site/src/components/start-template-options/index.js.
function injectThemeAttributeInBlockTemplateContent( block ) {
if (
@@ -64,7 +74,14 @@ const PatternEdit = ( { attributes, clientId } ) => {
// This change won't be saved.
// It will continue to pull from the pattern file unless changes are made to its respective template part.
useEffect( () => {
- if ( selectedPattern?.blocks ) {
+ if ( ! hasRecursionError && selectedPattern?.blocks ) {
+ try {
+ parsePatternDependencies( selectedPattern );
+ } catch ( error ) {
+ setHasRecursionError( true );
+ return;
+ }
+
// We batch updates to block list settings to avoid triggering cascading renders
// for each container block included in a tree and optimize initial render.
// Since the above uses microtasks, we need to use a microtask here as well,
@@ -93,7 +110,8 @@ const PatternEdit = ( { attributes, clientId } ) => {
}
}, [
clientId,
- selectedPattern?.blocks,
+ hasRecursionError,
+ selectedPattern,
__unstableMarkNextChangeAsNotPersistent,
replaceBlocks,
getBlockEditingMode,
@@ -103,6 +121,20 @@ const PatternEdit = ( { attributes, clientId } ) => {
const props = useBlockProps();
+ if ( hasRecursionError ) {
+ return (
+
+
+ { sprintf(
+ // translators: A warning in which %s is the name of a pattern.
+ __( 'Pattern "%s" cannot be rendered inside itself.' ),
+ selectedPattern?.name
+ ) }
+
+
+ );
+ }
+
return ;
};
diff --git a/packages/block-library/src/pattern/index.php b/packages/block-library/src/pattern/index.php
index 436452f685300..70c389e4ec8db 100644
--- a/packages/block-library/src/pattern/index.php
+++ b/packages/block-library/src/pattern/index.php
@@ -27,6 +27,8 @@ function register_block_core_pattern() {
* @return string Returns the output of the pattern.
*/
function render_block_core_pattern( $attributes ) {
+ static $seen_refs = array();
+
if ( empty( $attributes['slug'] ) ) {
return '';
}
@@ -38,6 +40,17 @@ function render_block_core_pattern( $attributes ) {
return '';
}
+ if ( isset( $seen_refs[ $attributes['slug'] ] ) ) {
+ // WP_DEBUG_DISPLAY must only be honored when WP_DEBUG. This precedent
+ // is set in `wp_debug_mode()`.
+ $is_debug = WP_DEBUG && WP_DEBUG_DISPLAY;
+
+ return $is_debug ?
+ // translators: Visible only in the front end, this warning takes the place of a faulty block. %s represents a pattern's slug.
+ sprintf( __( '[block rendering halted for pattern "%s"]' ), $slug ) :
+ '';
+ }
+
$pattern = $registry->get_registered( $slug );
$content = $pattern['content'];
@@ -48,11 +61,14 @@ function render_block_core_pattern( $attributes ) {
$content = gutenberg_serialize_blocks( $blocks );
}
+ $seen_refs[ $attributes['slug'] ] = true;
+
$content = do_blocks( $content );
global $wp_embed;
$content = $wp_embed->autoembed( $content );
+ unset( $seen_refs[ $attributes['slug'] ] );
return $content;
}
diff --git a/packages/block-library/src/pattern/recursion-detector.js b/packages/block-library/src/pattern/recursion-detector.js
new file mode 100644
index 0000000000000..f2e80087dd4b0
--- /dev/null
+++ b/packages/block-library/src/pattern/recursion-detector.js
@@ -0,0 +1,145 @@
+/**
+ * THIS MODULE IS INTENTIONALLY KEPT WITHIN THE PATTERN BLOCK'S SOURCE.
+ *
+ * This is because this approach for preventing infinite loops due to
+ * recursively rendering blocks is specific to the way that the `core/pattern`
+ * block behaves in the editor. Any other block types that deal with recursion
+ * SHOULD USE THE STANDARD METHOD for avoiding loops:
+ *
+ * @see https://github.com/WordPress/gutenberg/pull/31455
+ * @see packages/block-editor/src/components/recursion-provider/README.md
+ */
+
+/**
+ * WordPress dependencies
+ */
+import { useRegistry } from '@wordpress/data';
+
+/**
+ * Naming is hard.
+ *
+ * @see useParsePatternDependencies
+ *
+ * @type {WeakMap