Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blocks: Implement recursion checking and reporting for template-parts, post-content and reusable blocks (#26923) #27012

Closed
wants to merge 8 commits into from
36 changes: 36 additions & 0 deletions lib/block-recursion-control.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/**
* Blocks API: Block recursion control functions.
*
* @package gutenberg
*/

/**
* Determines whether or not to process this content.
*
* @param string|integer $id Unique ID for the content.
* @param string $block_name Block name.
* @return bool - true if recursion hasn't been detected.
*/
function gutenberg_process_this_content( $id, $block_name ) {
$recursion_control = WP_Block_Recursion_Control::get_instance();
return $recursion_control->process_this_content( $id, $block_name );
}

/**
* Pops the last item of processed content.
*
* As we return to the previous level we can clear the processed content.
* Basically this is something we have to do while processing certain inner blocks:
*
* - core/post-content
* - core/template-part
* - core/block
* - core/post-excerpt - possibly
*
* Note: The top level is within the template, which loads the template parts and/or queries.
*/
function gutenberg_clear_processed_content() {
$recursion_control = WP_Block_Recursion_Control::get_instance();
$recursion_control->clear_processed_content();
}
27 changes: 27 additions & 0 deletions lib/block-recursion-error.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
/**
* Blocks API: Block recursion error handling.
*
* @package gutenberg
*/

/**
* Reports a recursion error to the user.
*
* If WP_DEBUG is true then additional information is displayed.
* Use the $class parameter to override default behavior.
*
* @param $id string|integer recursive ID detected
* @param $type string content type
* @return string HTML reporting the error to the user
*/
function gutenberg_report_recursion_error( $message=null, $class=null ) {
$recursion_control = WP_Block_Recursion_Control::get_instance();
if ( $class && class_exists( $class ) ) {
$recursion_error = new $class( $recursion_control );
} else {
$recursion_error = new WP_Block_Recursion_Error( $recursion_control );
}
$html = $recursion_error->report_recursion_error( $message );
return $html;
}
139 changes: 139 additions & 0 deletions lib/class-wp-block-recursion-control.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php
/**
* Blocks API: WP_Block_Recursion_Control class
*
* @package Gutenberg
*/

/**
* Class for detecting recursion in inner blocks.
*
* Implements the recursion control functions.
* Blocks use the public API passing the unique ID and block name for their block.
*
* ```
* if ( gutenberg_process_this_content( $id, $block_name ) ) {
* $html = process the block;
* gutenberg_clear_processed_content();
* } else {
* $html = gutenberg_report_recursion_error( $message, $class );
* }
* return $html;
* ```
*
* Blocks wishing to report a recursion error should use
* gutenberg_report_recursion_error( $message, $class )
*
* These functions will be implemented by methods accessed using WP_Block_Recursion_Control::get_instance()
*
* @property array $processed_content
* @property integer|string $id
* @property string $block_name
*/
class WP_Block_Recursion_Control {
/**
* Stack of recursive blocks unique keys.
*
* @var array
*
*/
public $processed_content = [];

/**
* ID of latest block. This could be the last straw.
*/
public $id;

/**
* Block name of the latest block
*/
public $block_name;

/**
* Container for the main instance of the class.
*
* @var WP_Block_Recursion_Control|null
*/
private static $instance = null;

/**
* Utility method to retrieve the main instance of the class.
*
* The instance will be created if it does not exist yet.
*
* @since
*
* @return WP_Block_Recursion_Control The main instance.
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}

/**
* Determines whether or not to process this content.
*
* @param string|integer $id Unique ID for the content
* @param string $block_name Block name e.g. core/post-content
* @return bool True if the post has not been processed. false otherwise
*/
function process_this_content( $id, $block_name ) {
$this->id = $id;
$this->block_name = $block_name;
$processed = isset( $this->processed_content[ $id ] );
if ( !$processed ) {
$this->processed_content[$id] = "$block_name $id" ;
}
return !$processed;
}

/**
* Pops or clears the array of processed content.
*
* As we return to the previous level we can clear the processed content.
* Basically this is something we have to do while processing certain inner blocks:
*
* - core/post-content
* - core/template-part
* - core/post-excerpt - possibly
* - core/block - possibly
*
* Note: The top level is within the template, which loads the template parts and/or queries.
*/
function clear_processed_content() {
array_pop( $this->processed_content );
}

/**
* Returns the unique ID of the last block.
*
* Used when a recursion error has been detected.
* @return int|string
*/
function get_id() {
return $this->id;
}

/**
* Returns the block stack.
*
* Used when a recursion error has been detected.
* @return int|string
*/
function get_processed_content() {
return $this->processed_content;
}

/**
* Returns the block name.
*
* Used when a recursion error has been detected.
* @return int|string
*/
function get_block_name() {
return $this->block_name;
}

}
139 changes: 139 additions & 0 deletions lib/class-wp-block-recursion-error.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php
/**
* Blocks API: WP_Block_Recursion_Error class
*
* @package Gutenberg
*/

/**
* Implements the recursion error functions in an extendable class.
* The extending class may be specified as the $class parameter.
*
* ```
* $html = gutenberg_report_recursion_error( $message, $class=null )
* ```
*/
class WP_Block_Recursion_Error {

private $recursion_control = null;
protected $bad_id = null;
protected $bad_processed_content = [];
protected $block_name = null; // The block name
protected $message = null;

public function __construct( $recursion_control = null ) {
if ( $recursion_control ) {
$this->recursion_control = $recursion_control;
} else {
$this->recursion_control = WP_Block_Recursion_Control::get_instance();
}
}

/**
* Start to produce the error block to be displayed on the front end.
*
* @TODO Implement as a core message block, when available.
*
* @return string
*/
function start_error_block() {
$content = '<div class="recursion-error">';
return $content;
}

/**
* Complete the error block to be displayed on the front end.
*
* @TODO Implement as a core message block, when available.
*
* @return string
*/
function complete_error_block() {
$content = '</div>';
return $content;
}

/**
* Returns the message.
*
* The default message varies depending on the block type where recursion was detected.
*
* @return string
*/
function get_message() {
if ( !$this->message ) {
switch ($this->block_name) {
case 'core/post-content':
$this->message = __('Content not available; already processed.', 'gutenberg');
break;
case 'core/template-part':
$this->message = __('Template part not processed to avoid infinite recursion', 'gutenberg');
break;
case 'core/block':
$this->message = __('Reusable block not processed to avoid infinite recursion', 'gutenberg');
break;
default:
$this->message = __('Infinite recursion error prevented', 'gutenberg');
$this->message .= sprintf(__('Block name: %1$s', 'gutenberg'), $this->block_name);
}
}
return $this->message;
}

/**
* Returns details to help with problem determination.
*
* This is only done when WP_DEBUG is true.
*
* @TODO Also check if the user is logged in.
* @TODO Also check the user's capability with regards to fixing the problem.
*
* @return string HTML with additional details.
*/
function report_debug_details() {
$content = [];
if (defined('WP_DEBUG') && WP_DEBUG) {
$content[] = "<span class=\"recursion-error-context \">";
$content[] = '<br />';
$content[] = sprintf( __( 'Block unique ID: %1$s', 'gutenberg' ), $this->bad_id );
$content[] = '<br />';
$content[] = sprintf( __( 'Block name: %1$s', 'gutenberg' ), $this->block_name );
$content[] = '<br />';
$content[] = sprintf( __('Stack: %1$s', 'gutenberg'), implode(',', $this->bad_processed_content) );
$content[] = '</span>';
}
$content = implode(" \n", $content);
return $content;
}

/**
* Reports a recursion error to the user.
*
* If WP_DEBUG is true then additional information is displayed.
*
* @param $message string
* @return string
*/
public function report_recursion_error( $message = null ) {
$this->bad_id = $this->recursion_control->get_id();
$this->block_name = $this->recursion_control->get_block_name();
$this->bad_processed_content = $this->recursion_control->get_processed_content();
$this->set_message( $message );
$content = $this->start_error_block();
$content .= $this->get_message();
$content .= $this->report_debug_details();
$content .= $this->complete_error_block();
return $content;
}

/**
* Sets the message to be displayed.
*
* Note: If the message is null then a default message is displayed.
*
* @param null $message
*/
public function set_message( $message=null ) {
$this->message = $message;
}
}
9 changes: 9 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,12 @@ function gutenberg_is_experiment_enabled( $name ) {
require dirname( __FILE__ ) . '/block-supports/align.php';
require dirname( __FILE__ ) . '/block-supports/typography.php';
require dirname( __FILE__ ) . '/block-supports/custom-classname.php';

if ( !class_exists( 'WP_Block_Recursion_Control') ) {
require_once __DIR__ . '/class-wp-block-recursion-control.php';
}
require_once __DIR__ . '/block-recursion-control.php';
if ( !class_exists( 'WP_Block_Recursion_Error') ) {
require_once __DIR__ . '/class-wp-block-recursion-error.php';
}
require_once __DIR__ . '/block-recursion-error.php';
Loading