| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Functions related to registering and parsing blocks. 4 * 5 * @package WordPress 6 * @subpackage Blocks 7 * @since 5.0.0 8 */ 9 10 /** 11 * Removes the block asset's path prefix if provided. 12 * 13 * @since 5.5.0 14 * 15 * @param string $asset_handle_or_path Asset handle or prefixed path. 16 * @return string Path without the prefix or the original value. 17 */ 18 function remove_block_asset_path_prefix( $asset_handle_or_path ) { 19 $path_prefix = 'file:'; 20 if ( ! str_starts_with( $asset_handle_or_path, $path_prefix ) ) { 21 return $asset_handle_or_path; 22 } 23 $path = substr( 24 $asset_handle_or_path, 25 strlen( $path_prefix ) 26 ); 27 if ( str_starts_with( $path, './' ) ) { 28 $path = substr( $path, 2 ); 29 } 30 return $path; 31 } 32 33 /** 34 * Generates the name for an asset based on the name of the block 35 * and the field name provided. 36 * 37 * @since 5.5.0 38 * @since 6.1.0 Added `$index` parameter. 39 * @since 6.5.0 Added support for `viewScriptModule` field. 40 * 41 * @param string $block_name Name of the block. 42 * @param string $field_name Name of the metadata field. 43 * @param int $index Optional. Index of the asset when multiple items passed. 44 * Default 0. 45 * @return string Generated asset name for the block's field. 46 */ 47 function generate_block_asset_handle( $block_name, $field_name, $index = 0 ) { 48 if ( str_starts_with( $block_name, 'core/' ) ) { 49 $asset_handle = str_replace( 'core/', 'wp-block-', $block_name ); 50 if ( str_starts_with( $field_name, 'editor' ) ) { 51 $asset_handle .= '-editor'; 52 } 53 if ( str_starts_with( $field_name, 'view' ) ) { 54 $asset_handle .= '-view'; 55 } 56 if ( str_ends_with( strtolower( $field_name ), 'scriptmodule' ) ) { 57 $asset_handle .= '-script-module'; 58 } 59 if ( $index > 0 ) { 60 $asset_handle .= '-' . ( $index + 1 ); 61 } 62 return $asset_handle; 63 } 64 65 $field_mappings = array( 66 'editorScript' => 'editor-script', 67 'editorStyle' => 'editor-style', 68 'script' => 'script', 69 'style' => 'style', 70 'viewScript' => 'view-script', 71 'viewScriptModule' => 'view-script-module', 72 'viewStyle' => 'view-style', 73 ); 74 $asset_handle = str_replace( '/', '-', $block_name ) . 75 '-' . $field_mappings[ $field_name ]; 76 if ( $index > 0 ) { 77 $asset_handle .= '-' . ( $index + 1 ); 78 } 79 return $asset_handle; 80 } 81 82 /** 83 * Gets the URL to a block asset. 84 * 85 * @since 6.4.0 86 * 87 * @param string $path A normalized path to a block asset. 88 * @return string|false The URL to the block asset or false on failure. 89 */ 90 function get_block_asset_url( $path ) { 91 if ( empty( $path ) ) { 92 return false; 93 } 94 95 // Path needs to be normalized to work in Windows env. 96 static $wpinc_path_norm = ''; 97 if ( ! $wpinc_path_norm ) { 98 $wpinc_path_norm = wp_normalize_path( realpath( ABSPATH . WPINC ) ); 99 } 100 101 if ( str_starts_with( $path, $wpinc_path_norm ) ) { 102 return includes_url( str_replace( $wpinc_path_norm, '', $path ) ); 103 } 104 105 static $template_paths_norm = array(); 106 107 $template = get_template(); 108 if ( ! isset( $template_paths_norm[ $template ] ) ) { 109 $template_paths_norm[ $template ] = wp_normalize_path( realpath( get_template_directory() ) ); 110 } 111 112 if ( str_starts_with( $path, trailingslashit( $template_paths_norm[ $template ] ) ) ) { 113 return get_theme_file_uri( str_replace( $template_paths_norm[ $template ], '', $path ) ); 114 } 115 116 if ( is_child_theme() ) { 117 $stylesheet = get_stylesheet(); 118 if ( ! isset( $template_paths_norm[ $stylesheet ] ) ) { 119 $template_paths_norm[ $stylesheet ] = wp_normalize_path( realpath( get_stylesheet_directory() ) ); 120 } 121 122 if ( str_starts_with( $path, trailingslashit( $template_paths_norm[ $stylesheet ] ) ) ) { 123 return get_theme_file_uri( str_replace( $template_paths_norm[ $stylesheet ], '', $path ) ); 124 } 125 } 126 127 return plugins_url( basename( $path ), $path ); 128 } 129 130 /** 131 * Finds a script module ID for the selected block metadata field. It detects 132 * when a path to file was provided and optionally finds a corresponding asset 133 * file with details necessary to register the script module under with an 134 * automatically generated module ID. It returns unprocessed script module 135 * ID otherwise. 136 * 137 * @since 6.5.0 138 * 139 * @param array $metadata Block metadata. 140 * @param string $field_name Field name to pick from metadata. 141 * @param int $index Optional. Index of the script module ID to register when multiple 142 * items passed. Default 0. 143 * @return string|false Script module ID or false on failure. 144 */ 145 function register_block_script_module_id( $metadata, $field_name, $index = 0 ) { 146 if ( empty( $metadata[ $field_name ] ) ) { 147 return false; 148 } 149 150 $module_id = $metadata[ $field_name ]; 151 if ( is_array( $module_id ) ) { 152 if ( empty( $module_id[ $index ] ) ) { 153 return false; 154 } 155 $module_id = $module_id[ $index ]; 156 } 157 158 $module_path = remove_block_asset_path_prefix( $module_id ); 159 if ( $module_id === $module_path ) { 160 return $module_id; 161 } 162 163 $path = dirname( $metadata['file'] ); 164 $module_asset_raw_path = $path . '/' . substr_replace( $module_path, '.asset.php', - strlen( '.js' ) ); 165 $module_id = generate_block_asset_handle( $metadata['name'], $field_name, $index ); 166 $module_asset_path = wp_normalize_path( 167 realpath( $module_asset_raw_path ) 168 ); 169 170 $module_path_norm = wp_normalize_path( realpath( $path . '/' . $module_path ) ); 171 $module_uri = get_block_asset_url( $module_path_norm ); 172 173 $module_asset = ! empty( $module_asset_path ) ? require $module_asset_path : array(); 174 $module_dependencies = $module_asset['dependencies'] ?? array(); 175 $block_version = $metadata['version'] ?? false; 176 $module_version = $module_asset['version'] ?? $block_version; 177 178 $supports_interactivity_true = isset( $metadata['supports']['interactivity'] ) && true === $metadata['supports']['interactivity']; 179 $is_interactive = $supports_interactivity_true || ( isset( $metadata['supports']['interactivity']['interactive'] ) && true === $metadata['supports']['interactivity']['interactive'] ); 180 $supports_client_navigation = $supports_interactivity_true || ( isset( $metadata['supports']['interactivity']['clientNavigation'] ) && true === $metadata['supports']['interactivity']['clientNavigation'] ); 181 182 $args = array(); 183 184 // Blocks using the Interactivity API are server-side rendered, so they are 185 // by design not in the critical rendering path and should be deprioritized. 186 if ( $is_interactive ) { 187 $args['fetchpriority'] = 'low'; 188 $args['in_footer'] = true; 189 } 190 191 // Blocks using the Interactivity API that support client-side navigation 192 // must be marked as such in their script modules. 193 if ( $is_interactive && $supports_client_navigation ) { 194 wp_interactivity()->add_client_navigation_support_to_script_module( $module_id ); 195 } 196 197 wp_register_script_module( 198 $module_id, 199 $module_uri, 200 $module_dependencies, 201 $module_version, 202 $args 203 ); 204 205 return $module_id; 206 } 207 208 /** 209 * Finds a script handle for the selected block metadata field. It detects 210 * when a path to file was provided and optionally finds a corresponding asset 211 * file with details necessary to register the script under automatically 212 * generated handle name. It returns unprocessed script handle otherwise. 213 * 214 * @since 5.5.0 215 * @since 6.1.0 Added `$index` parameter. 216 * @since 6.5.0 The asset file is optional. Added script handle support in the asset file. 217 * 218 * @param array $metadata Block metadata. 219 * @param string $field_name Field name to pick from metadata. 220 * @param int $index Optional. Index of the script to register when multiple items passed. 221 * Default 0. 222 * @return string|false Script handle provided directly or created through 223 * script's registration, or false on failure. 224 */ 225 function register_block_script_handle( $metadata, $field_name, $index = 0 ) { 226 if ( empty( $metadata[ $field_name ] ) ) { 227 return false; 228 } 229 230 $script_handle_or_path = $metadata[ $field_name ]; 231 if ( is_array( $script_handle_or_path ) ) { 232 if ( empty( $script_handle_or_path[ $index ] ) ) { 233 return false; 234 } 235 $script_handle_or_path = $script_handle_or_path[ $index ]; 236 } 237 238 $script_path = remove_block_asset_path_prefix( $script_handle_or_path ); 239 if ( $script_handle_or_path === $script_path ) { 240 return $script_handle_or_path; 241 } 242 243 $path = dirname( $metadata['file'] ); 244 $script_asset_raw_path = $path . '/' . substr_replace( $script_path, '.asset.php', - strlen( '.js' ) ); 245 $script_asset_path = wp_normalize_path( 246 realpath( $script_asset_raw_path ) 247 ); 248 249 // Asset file for blocks is optional. See https://core.trac.wordpress.org/ticket/60460. 250 $script_asset = ! empty( $script_asset_path ) ? require $script_asset_path : array(); 251 $script_handle = $script_asset['handle'] ?? 252 generate_block_asset_handle( $metadata['name'], $field_name, $index ); 253 if ( wp_script_is( $script_handle, 'registered' ) ) { 254 return $script_handle; 255 } 256 257 $script_path_norm = wp_normalize_path( realpath( $path . '/' . $script_path ) ); 258 $script_uri = get_block_asset_url( $script_path_norm ); 259 $script_dependencies = $script_asset['dependencies'] ?? array(); 260 $block_version = $metadata['version'] ?? false; 261 $script_version = $script_asset['version'] ?? $block_version; 262 $script_args = array(); 263 if ( 'viewScript' === $field_name && $script_uri ) { 264 $script_args['strategy'] = 'defer'; 265 } 266 267 $result = wp_register_script( 268 $script_handle, 269 $script_uri, 270 $script_dependencies, 271 $script_version, 272 $script_args 273 ); 274 if ( ! $result ) { 275 return false; 276 } 277 278 if ( ! empty( $metadata['textdomain'] ) && in_array( 'wp-i18n', $script_dependencies, true ) ) { 279 wp_set_script_translations( $script_handle, $metadata['textdomain'] ); 280 } 281 282 return $script_handle; 283 } 284 285 /** 286 * Finds a style handle for the block metadata field. It detects when a path 287 * to file was provided and registers the style under automatically 288 * generated handle name. It returns unprocessed style handle otherwise. 289 * 290 * @since 5.5.0 291 * @since 6.1.0 Added `$index` parameter. 292 * 293 * @param array $metadata Block metadata. 294 * @param string $field_name Field name to pick from metadata. 295 * @param int $index Optional. Index of the style to register when multiple items passed. 296 * Default 0. 297 * @return string|false Style handle provided directly or created through 298 * style's registration, or false on failure. 299 */ 300 function register_block_style_handle( $metadata, $field_name, $index = 0 ) { 301 if ( empty( $metadata[ $field_name ] ) ) { 302 return false; 303 } 304 305 $style_handle = $metadata[ $field_name ]; 306 if ( is_array( $style_handle ) ) { 307 if ( empty( $style_handle[ $index ] ) ) { 308 return false; 309 } 310 $style_handle = $style_handle[ $index ]; 311 } 312 313 $style_handle_name = generate_block_asset_handle( $metadata['name'], $field_name, $index ); 314 // If the style handle is already registered, skip re-registering. 315 if ( wp_style_is( $style_handle_name, 'registered' ) ) { 316 return $style_handle_name; 317 } 318 319 static $wpinc_path_norm = ''; 320 if ( ! $wpinc_path_norm ) { 321 $wpinc_path_norm = wp_normalize_path( realpath( ABSPATH . WPINC ) ); 322 } 323 324 $is_core_block = isset( $metadata['file'] ) && str_starts_with( $metadata['file'], $wpinc_path_norm ); 325 // Skip registering individual styles for each core block when a bundled version provided. 326 if ( $is_core_block && ! wp_should_load_separate_core_block_assets() ) { 327 return false; 328 } 329 330 $style_path = remove_block_asset_path_prefix( $style_handle ); 331 $is_style_handle = $style_handle === $style_path; 332 // Allow only passing style handles for core blocks. 333 if ( $is_core_block && ! $is_style_handle ) { 334 return false; 335 } 336 // Return the style handle unless it's the first item for every core block that requires special treatment. 337 if ( $is_style_handle && ! ( $is_core_block && 0 === $index ) ) { 338 return $style_handle; 339 } 340 341 // Check whether styles should have a ".min" suffix or not. 342 $suffix = SCRIPT_DEBUG ? '' : '.min'; 343 if ( $is_core_block ) { 344 $style_path = ( 'editorStyle' === $field_name ) ? "editor{$suffix}.css" : "style{$suffix}.css"; 345 } 346 347 $style_path_norm = wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . $style_path ) ); 348 $style_uri = get_block_asset_url( $style_path_norm ); 349 350 $block_version = ! $is_core_block && isset( $metadata['version'] ) ? $metadata['version'] : false; 351 $version = $style_path_norm && defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? filemtime( $style_path_norm ) : $block_version; 352 $result = wp_register_style( 353 $style_handle_name, 354 $style_uri, 355 array(), 356 $version 357 ); 358 if ( ! $result ) { 359 return false; 360 } 361 362 if ( $style_uri ) { 363 wp_style_add_data( $style_handle_name, 'path', $style_path_norm ); 364 365 if ( $is_core_block ) { 366 $rtl_file = str_replace( "{$suffix}.css", "-rtl{$suffix}.css", $style_path_norm ); 367 } else { 368 $rtl_file = str_replace( '.css', '-rtl.css', $style_path_norm ); 369 } 370 371 if ( is_rtl() && file_exists( $rtl_file ) ) { 372 wp_style_add_data( $style_handle_name, 'rtl', 'replace' ); 373 wp_style_add_data( $style_handle_name, 'suffix', $suffix ); 374 wp_style_add_data( $style_handle_name, 'path', $rtl_file ); 375 } 376 } 377 378 return $style_handle_name; 379 } 380 381 /** 382 * Gets i18n schema for block's metadata read from `block.json` file. 383 * 384 * @since 5.9.0 385 * 386 * @return object The schema for block's metadata. 387 */ 388 function get_block_metadata_i18n_schema() { 389 static $i18n_block_schema; 390 391 if ( ! isset( $i18n_block_schema ) ) { 392 $i18n_block_schema = wp_json_file_decode( __DIR__ . '/block-i18n.json' ); 393 } 394 395 return $i18n_block_schema; 396 } 397 398 /** 399 * Registers all block types from a block metadata collection. 400 * 401 * This can either reference a previously registered metadata collection or, if the `$manifest` parameter is provided, 402 * register the metadata collection directly within the same function call. 403 * 404 * @since 6.8.0 405 * @see wp_register_block_metadata_collection() 406 * @see register_block_type_from_metadata() 407 * 408 * @param string $path The absolute base path for the collection ( e.g., WP_PLUGIN_DIR . '/my-plugin/blocks/' ). 409 * @param string $manifest Optional. The absolute path to the manifest file containing the metadata collection, in 410 * order to register the collection. If this parameter is not provided, the `$path` parameter 411 * must reference a previously registered block metadata collection. 412 */ 413 function wp_register_block_types_from_metadata_collection( $path, $manifest = '' ) { 414 if ( $manifest ) { 415 wp_register_block_metadata_collection( $path, $manifest ); 416 } 417 418 $block_metadata_files = WP_Block_Metadata_Registry::get_collection_block_metadata_files( $path ); 419 foreach ( $block_metadata_files as $block_metadata_file ) { 420 register_block_type_from_metadata( $block_metadata_file ); 421 } 422 } 423 424 /** 425 * Registers a block metadata collection. 426 * 427 * This function allows core and third-party plugins to register their block metadata 428 * collections in a centralized location. Registering collections can improve performance 429 * by avoiding multiple reads from the filesystem and parsing JSON. 430 * 431 * @since 6.7.0 432 * 433 * @param string $path The base path in which block files for the collection reside. 434 * @param string $manifest The path to the manifest file for the collection. 435 */ 436 function wp_register_block_metadata_collection( $path, $manifest ) { 437 WP_Block_Metadata_Registry::register_collection( $path, $manifest ); 438 } 439 440 /** 441 * Registers a block type from the metadata stored in the `block.json` file. 442 * 443 * @since 5.5.0 444 * @since 5.7.0 Added support for `textdomain` field and i18n handling for all translatable fields. 445 * @since 5.9.0 Added support for `variations` and `viewScript` fields. 446 * @since 6.1.0 Added support for `render` field. 447 * @since 6.3.0 Added `selectors` field. 448 * @since 6.4.0 Added support for `blockHooks` field. 449 * @since 6.5.0 Added support for `allowedBlocks`, `viewScriptModule`, and `viewStyle` fields. 450 * @since 6.7.0 Allow PHP filename as `variations` argument. 451 * 452 * @param string $file_or_folder Path to the JSON file with metadata definition for 453 * the block or path to the folder where the `block.json` file is located. 454 * If providing the path to a JSON file, the filename must end with `block.json`. 455 * @param array $args Optional. Array of block type arguments. Accepts any public property 456 * of `WP_Block_Type`. See WP_Block_Type::__construct() for information 457 * on accepted arguments. Default empty array. 458 * @return WP_Block_Type|false The registered block type on success, or false on failure. 459 */ 460 function register_block_type_from_metadata( $file_or_folder, $args = array() ) { 461 /* 462 * Get an array of metadata from a PHP file. 463 * This improves performance for core blocks as it's only necessary to read a single PHP file 464 * instead of reading a JSON file per-block, and then decoding from JSON to PHP. 465 * Using a static variable ensures that the metadata is only read once per request. 466 */ 467 468 $file_or_folder = wp_normalize_path( $file_or_folder ); 469 470 $metadata_file = ( ! str_ends_with( $file_or_folder, 'block.json' ) ) ? 471 trailingslashit( $file_or_folder ) . 'block.json' : 472 $file_or_folder; 473 474 $is_core_block = str_starts_with( $file_or_folder, wp_normalize_path( ABSPATH . WPINC ) ); 475 $metadata_file_exists = $is_core_block || file_exists( $metadata_file ); 476 $registry_metadata = WP_Block_Metadata_Registry::get_metadata( $file_or_folder ); 477 478 if ( $registry_metadata ) { 479 $metadata = $registry_metadata; 480 } elseif ( $metadata_file_exists ) { 481 $metadata = wp_json_file_decode( $metadata_file, array( 'associative' => true ) ); 482 } else { 483 $metadata = array(); 484 } 485 486 if ( ! is_array( $metadata ) || ( empty( $metadata['name'] ) && empty( $args['name'] ) ) ) { 487 return false; 488 } 489 490 $metadata['file'] = $metadata_file_exists ? wp_normalize_path( realpath( $metadata_file ) ) : null; 491 492 /** 493 * Filters the metadata provided for registering a block type. 494 * 495 * @since 5.7.0 496 * 497 * @param array $metadata Metadata for registering a block type. 498 */ 499 $metadata = apply_filters( 'block_type_metadata', $metadata ); 500 501 // Add `style` and `editor_style` for core blocks if missing. 502 if ( ! empty( $metadata['name'] ) && str_starts_with( $metadata['name'], 'core/' ) ) { 503 $block_name = str_replace( 'core/', '', $metadata['name'] ); 504 505 if ( ! isset( $metadata['style'] ) ) { 506 $metadata['style'] = "wp-block-$block_name"; 507 } 508 if ( current_theme_supports( 'wp-block-styles' ) && wp_should_load_separate_core_block_assets() ) { 509 $metadata['style'] = (array) $metadata['style']; 510 $metadata['style'][] = "wp-block-{$block_name}-theme"; 511 } 512 if ( ! isset( $metadata['editorStyle'] ) ) { 513 $metadata['editorStyle'] = "wp-block-{$block_name}-editor"; 514 } 515 } 516 517 $settings = array(); 518 $property_mappings = array( 519 'apiVersion' => 'api_version', 520 'name' => 'name', 521 'title' => 'title', 522 'category' => 'category', 523 'parent' => 'parent', 524 'ancestor' => 'ancestor', 525 'icon' => 'icon', 526 'description' => 'description', 527 'keywords' => 'keywords', 528 'attributes' => 'attributes', 529 'providesContext' => 'provides_context', 530 'usesContext' => 'uses_context', 531 'selectors' => 'selectors', 532 'supports' => 'supports', 533 'styles' => 'styles', 534 'variations' => 'variations', 535 'example' => 'example', 536 'allowedBlocks' => 'allowed_blocks', 537 ); 538 $textdomain = ! empty( $metadata['textdomain'] ) ? $metadata['textdomain'] : null; 539 $i18n_schema = get_block_metadata_i18n_schema(); 540 541 foreach ( $property_mappings as $key => $mapped_key ) { 542 if ( isset( $metadata[ $key ] ) ) { 543 $settings[ $mapped_key ] = $metadata[ $key ]; 544 if ( $metadata_file_exists && $textdomain && isset( $i18n_schema->$key ) ) { 545 $settings[ $mapped_key ] = translate_settings_using_i18n_schema( $i18n_schema->$key, $settings[ $key ], $textdomain ); 546 } 547 } 548 } 549 550 if ( ! empty( $metadata['render'] ) ) { 551 $template_path = wp_normalize_path( 552 realpath( 553 dirname( $metadata['file'] ) . '/' . 554 remove_block_asset_path_prefix( $metadata['render'] ) 555 ) 556 ); 557 if ( $template_path ) { 558 /** 559 * Renders the block on the server. 560 * 561 * @since 6.1.0 562 * 563 * @param array $attributes Block attributes. 564 * @param string $content Block default content. 565 * @param WP_Block $block Block instance. 566 * 567 * @return string Returns the block content. 568 */ 569 $settings['render_callback'] = static function ( $attributes, $content, $block ) use ( $template_path ) { 570 ob_start(); 571 require $template_path; 572 return ob_get_clean(); 573 }; 574 } 575 } 576 577 // If `variations` is a string, it's the name of a PHP file that 578 // generates the variations. 579 if ( ! empty( $metadata['variations'] ) && is_string( $metadata['variations'] ) ) { 580 $variations_path = wp_normalize_path( 581 realpath( 582 dirname( $metadata['file'] ) . '/' . 583 remove_block_asset_path_prefix( $metadata['variations'] ) 584 ) 585 ); 586 if ( $variations_path ) { 587 /** 588 * Generates the list of block variations. 589 * 590 * @since 6.7.0 591 * 592 * @return string Returns the list of block variations. 593 */ 594 $settings['variation_callback'] = static function () use ( $variations_path ) { 595 $variations = require $variations_path; 596 return $variations; 597 }; 598 // The block instance's `variations` field is only allowed to be an array 599 // (of known block variations). We unset it so that the block instance will 600 // provide a getter that returns the result of the `variation_callback` instead. 601 unset( $settings['variations'] ); 602 } 603 } 604 605 $settings = array_merge( $settings, $args ); 606 607 $script_fields = array( 608 'editorScript' => 'editor_script_handles', 609 'script' => 'script_handles', 610 'viewScript' => 'view_script_handles', 611 ); 612 foreach ( $script_fields as $metadata_field_name => $settings_field_name ) { 613 if ( ! empty( $settings[ $metadata_field_name ] ) ) { 614 $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; 615 } 616 if ( ! empty( $metadata[ $metadata_field_name ] ) ) { 617 $scripts = $metadata[ $metadata_field_name ]; 618 $processed_scripts = array(); 619 if ( is_array( $scripts ) ) { 620 for ( $index = 0; $index < count( $scripts ); $index++ ) { 621 $result = register_block_script_handle( 622 $metadata, 623 $metadata_field_name, 624 $index 625 ); 626 if ( $result ) { 627 $processed_scripts[] = $result; 628 } 629 } 630 } else { 631 $result = register_block_script_handle( 632 $metadata, 633 $metadata_field_name 634 ); 635 if ( $result ) { 636 $processed_scripts[] = $result; 637 } 638 } 639 $settings[ $settings_field_name ] = $processed_scripts; 640 } 641 } 642 643 $module_fields = array( 644 'viewScriptModule' => 'view_script_module_ids', 645 ); 646 foreach ( $module_fields as $metadata_field_name => $settings_field_name ) { 647 if ( ! empty( $settings[ $metadata_field_name ] ) ) { 648 $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; 649 } 650 if ( ! empty( $metadata[ $metadata_field_name ] ) ) { 651 $modules = $metadata[ $metadata_field_name ]; 652 $processed_modules = array(); 653 if ( is_array( $modules ) ) { 654 for ( $index = 0; $index < count( $modules ); $index++ ) { 655 $result = register_block_script_module_id( 656 $metadata, 657 $metadata_field_name, 658 $index 659 ); 660 if ( $result ) { 661 $processed_modules[] = $result; 662 } 663 } 664 } else { 665 $result = register_block_script_module_id( 666 $metadata, 667 $metadata_field_name 668 ); 669 if ( $result ) { 670 $processed_modules[] = $result; 671 } 672 } 673 $settings[ $settings_field_name ] = $processed_modules; 674 } 675 } 676 677 $style_fields = array( 678 'editorStyle' => 'editor_style_handles', 679 'style' => 'style_handles', 680 'viewStyle' => 'view_style_handles', 681 ); 682 foreach ( $style_fields as $metadata_field_name => $settings_field_name ) { 683 if ( ! empty( $settings[ $metadata_field_name ] ) ) { 684 $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; 685 } 686 if ( ! empty( $metadata[ $metadata_field_name ] ) ) { 687 $styles = $metadata[ $metadata_field_name ]; 688 $processed_styles = array(); 689 if ( is_array( $styles ) ) { 690 for ( $index = 0; $index < count( $styles ); $index++ ) { 691 $result = register_block_style_handle( 692 $metadata, 693 $metadata_field_name, 694 $index 695 ); 696 if ( $result ) { 697 $processed_styles[] = $result; 698 } 699 } 700 } else { 701 $result = register_block_style_handle( 702 $metadata, 703 $metadata_field_name 704 ); 705 if ( $result ) { 706 $processed_styles[] = $result; 707 } 708 } 709 $settings[ $settings_field_name ] = $processed_styles; 710 } 711 } 712 713 if ( ! empty( $metadata['blockHooks'] ) ) { 714 /** 715 * Map camelCased position string (from block.json) to snake_cased block type position. 716 * 717 * @var array 718 */ 719 $position_mappings = array( 720 'before' => 'before', 721 'after' => 'after', 722 'firstChild' => 'first_child', 723 'lastChild' => 'last_child', 724 ); 725 726 $settings['block_hooks'] = array(); 727 foreach ( $metadata['blockHooks'] as $anchor_block_name => $position ) { 728 // Avoid infinite recursion (hooking to itself). 729 if ( $metadata['name'] === $anchor_block_name ) { 730 _doing_it_wrong( 731 __METHOD__, 732 __( 'Cannot hook block to itself.' ), 733 '6.4.0' 734 ); 735 continue; 736 } 737 738 if ( ! isset( $position_mappings[ $position ] ) ) { 739 continue; 740 } 741 742 $settings['block_hooks'][ $anchor_block_name ] = $position_mappings[ $position ]; 743 } 744 } 745 746 /** 747 * Filters the settings determined from the block type metadata. 748 * 749 * @since 5.7.0 750 * 751 * @param array $settings Array of determined settings for registering a block type. 752 * @param array $metadata Metadata provided for registering a block type. 753 */ 754 $settings = apply_filters( 'block_type_metadata_settings', $settings, $metadata ); 755 756 $metadata['name'] = ! empty( $settings['name'] ) ? $settings['name'] : $metadata['name']; 757 758 return WP_Block_Type_Registry::get_instance()->register( 759 $metadata['name'], 760 $settings 761 ); 762 } 763 764 /** 765 * Registers a block type. The recommended way is to register a block type using 766 * the metadata stored in the `block.json` file. 767 * 768 * @since 5.0.0 769 * @since 5.8.0 First parameter now accepts a path to the `block.json` file. 770 * 771 * @param string|WP_Block_Type $block_type Block type name including namespace, or alternatively 772 * a path to the JSON file with metadata definition for the block, 773 * or a path to the folder where the `block.json` file is located, 774 * or a complete WP_Block_Type instance. 775 * In case a WP_Block_Type is provided, the $args parameter will be ignored. 776 * @param array $args Optional. Array of block type arguments. Accepts any public property 777 * of `WP_Block_Type`. See WP_Block_Type::__construct() for information 778 * on accepted arguments. Default empty array. 779 * 780 * @return WP_Block_Type|false The registered block type on success, or false on failure. 781 */ 782 function register_block_type( $block_type, $args = array() ) { 783 if ( is_string( $block_type ) && file_exists( $block_type ) ) { 784 return register_block_type_from_metadata( $block_type, $args ); 785 } 786 787 return WP_Block_Type_Registry::get_instance()->register( $block_type, $args ); 788 } 789 790 /** 791 * Unregisters a block type. 792 * 793 * @since 5.0.0 794 * 795 * @param string|WP_Block_Type $name Block type name including namespace, or alternatively 796 * a complete WP_Block_Type instance. 797 * @return WP_Block_Type|false The unregistered block type on success, or false on failure. 798 */ 799 function unregister_block_type( $name ) { 800 return WP_Block_Type_Registry::get_instance()->unregister( $name ); 801 } 802 803 /** 804 * Determines whether a post or content string has blocks. 805 * 806 * This test optimizes for performance rather than strict accuracy, detecting 807 * the pattern of a block but not validating its structure. For strict accuracy, 808 * you should use the block parser on post content. 809 * 810 * @since 5.0.0 811 * 812 * @see parse_blocks() 813 * 814 * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. 815 * Defaults to global $post. 816 * @return bool Whether the post has blocks. 817 */ 818 function has_blocks( $post = null ) { 819 if ( ! is_string( $post ) ) { 820 $wp_post = get_post( $post ); 821 822 if ( ! $wp_post instanceof WP_Post ) { 823 return false; 824 } 825 826 $post = $wp_post->post_content; 827 } 828 829 return str_contains( (string) $post, '<!-- wp:' ); 830 } 831 832 /** 833 * Determines whether a $post or a string contains a specific block type. 834 * 835 * This test optimizes for performance rather than strict accuracy, detecting 836 * whether the block type exists but not validating its structure and not checking 837 * synced patterns (formerly called reusable blocks). For strict accuracy, 838 * you should use the block parser on post content. 839 * 840 * @since 5.0.0 841 * 842 * @see parse_blocks() 843 * 844 * @param string $block_name Full block type to look for. 845 * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. 846 * Defaults to global $post. 847 * @return bool Whether the post content contains the specified block. 848 */ 849 function has_block( $block_name, $post = null ) { 850 if ( ! has_blocks( $post ) ) { 851 return false; 852 } 853 854 if ( ! is_string( $post ) ) { 855 $wp_post = get_post( $post ); 856 if ( $wp_post instanceof WP_Post ) { 857 $post = $wp_post->post_content; 858 } 859 } 860 861 /* 862 * Normalize block name to include namespace, if provided as non-namespaced. 863 * This matches behavior for WordPress 5.0.0 - 5.3.0 in matching blocks by 864 * their serialized names. 865 */ 866 if ( ! str_contains( $block_name, '/' ) ) { 867 $block_name = 'core/' . $block_name; 868 } 869 870 // Test for existence of block by its fully qualified name. 871 $has_block = str_contains( $post, '<!-- wp:' . $block_name . ' ' ); 872 873 if ( ! $has_block ) { 874 /* 875 * If the given block name would serialize to a different name, test for 876 * existence by the serialized form. 877 */ 878 $serialized_block_name = strip_core_block_namespace( $block_name ); 879 if ( $serialized_block_name !== $block_name ) { 880 $has_block = str_contains( $post, '<!-- wp:' . $serialized_block_name . ' ' ); 881 } 882 } 883 884 return $has_block; 885 } 886 887 /** 888 * Returns an array of the names of all registered dynamic block types. 889 * 890 * @since 5.0.0 891 * 892 * @return string[] Array of dynamic block names. 893 */ 894 function get_dynamic_block_names() { 895 $dynamic_block_names = array(); 896 897 $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); 898 foreach ( $block_types as $block_type ) { 899 if ( $block_type->is_dynamic() ) { 900 $dynamic_block_names[] = $block_type->name; 901 } 902 } 903 904 return $dynamic_block_names; 905 } 906 907 /** 908 * Retrieves block types hooked into the given block, grouped by anchor block type and the relative position. 909 * 910 * @since 6.4.0 911 * 912 * @return array[] Array of block types grouped by anchor block type and the relative position. 913 */ 914 function get_hooked_blocks() { 915 $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); 916 $hooked_blocks = array(); 917 foreach ( $block_types as $block_type ) { 918 if ( ! ( $block_type instanceof WP_Block_Type ) || ! is_array( $block_type->block_hooks ) ) { 919 continue; 920 } 921 foreach ( $block_type->block_hooks as $anchor_block_type => $relative_position ) { 922 if ( ! isset( $hooked_blocks[ $anchor_block_type ] ) ) { 923 $hooked_blocks[ $anchor_block_type ] = array(); 924 } 925 if ( ! isset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) ) { 926 $hooked_blocks[ $anchor_block_type ][ $relative_position ] = array(); 927 } 928 $hooked_blocks[ $anchor_block_type ][ $relative_position ][] = $block_type->name; 929 } 930 } 931 932 return $hooked_blocks; 933 } 934 935 /** 936 * Returns the markup for blocks hooked to the given anchor block in a specific relative position. 937 * 938 * @since 6.5.0 939 * @access private 940 * 941 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 942 * @param string $relative_position The relative position of the hooked blocks. 943 * Can be one of 'before', 'after', 'first_child', or 'last_child'. 944 * @param array $hooked_blocks An array of hooked block types, grouped by anchor block and relative position. 945 * @param WP_Block_Template|WP_Post|array $context The block template, template part, or pattern that the anchor block belongs to. 946 * @return string 947 */ 948 function insert_hooked_blocks( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) { 949 $anchor_block_type = $parsed_anchor_block['blockName']; 950 $hooked_block_types = isset( $anchor_block_type, $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) 951 ? $hooked_blocks[ $anchor_block_type ][ $relative_position ] 952 : array(); 953 954 /** 955 * Filters the list of hooked block types for a given anchor block type and relative position. 956 * 957 * @since 6.4.0 958 * 959 * @param string[] $hooked_block_types The list of hooked block types. 960 * @param string $relative_position The relative position of the hooked blocks. 961 * Can be one of 'before', 'after', 'first_child', or 'last_child'. 962 * @param string $anchor_block_type The anchor block type. 963 * @param WP_Block_Template|WP_Post|array $context The block template, template part, post object, 964 * or pattern that the anchor block belongs to. 965 */ 966 $hooked_block_types = apply_filters( 'hooked_block_types', $hooked_block_types, $relative_position, $anchor_block_type, $context ); 967 968 $markup = ''; 969 foreach ( $hooked_block_types as $hooked_block_type ) { 970 $parsed_hooked_block = array( 971 'blockName' => $hooked_block_type, 972 'attrs' => array(), 973 'innerBlocks' => array(), 974 'innerHTML' => '', 975 'innerContent' => array(), 976 ); 977 978 /** 979 * Filters the parsed block array for a given hooked block. 980 * 981 * @since 6.5.0 982 * 983 * @param array|null $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block. 984 * @param string $hooked_block_type The hooked block type name. 985 * @param string $relative_position The relative position of the hooked block. 986 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 987 * @param WP_Block_Template|WP_Post|array $context The block template, template part, post object, 988 * or pattern that the anchor block belongs to. 989 */ 990 $parsed_hooked_block = apply_filters( 'hooked_block', $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 991 992 /** 993 * Filters the parsed block array for a given hooked block. 994 * 995 * The dynamic portion of the hook name, `$hooked_block_type`, refers to the block type name of the specific hooked block. 996 * 997 * @since 6.5.0 998 * 999 * @param array|null $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block. 1000 * @param string $hooked_block_type The hooked block type name. 1001 * @param string $relative_position The relative position of the hooked block. 1002 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 1003 * @param WP_Block_Template|WP_Post|array $context The block template, template part, post object, 1004 * or pattern that the anchor block belongs to. 1005 */ 1006 $parsed_hooked_block = apply_filters( "hooked_block_{$hooked_block_type}", $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 1007 1008 if ( null === $parsed_hooked_block ) { 1009 continue; 1010 } 1011 1012 // It's possible that the filter returned a block of a different type, so we explicitly 1013 // look for the original `$hooked_block_type` in the `ignoredHookedBlocks` metadata. 1014 if ( 1015 ! isset( $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] ) || 1016 ! in_array( $hooked_block_type, $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'], true ) 1017 ) { 1018 $markup .= serialize_block( $parsed_hooked_block ); 1019 } 1020 } 1021 1022 return $markup; 1023 } 1024 1025 /** 1026 * Adds a list of hooked block types to an anchor block's ignored hooked block types. 1027 * 1028 * This function is meant for internal use only. 1029 * 1030 * @since 6.5.0 1031 * @access private 1032 * 1033 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 1034 * @param string $relative_position The relative position of the hooked blocks. 1035 * Can be one of 'before', 'after', 'first_child', or 'last_child'. 1036 * @param array $hooked_blocks An array of hooked block types, grouped by anchor block and relative position. 1037 * @param WP_Block_Template|WP_Post|array $context The block template, template part, or pattern that the anchor block belongs to. 1038 * @return string Empty string. 1039 */ 1040 function set_ignored_hooked_blocks_metadata( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) { 1041 $anchor_block_type = $parsed_anchor_block['blockName']; 1042 $hooked_block_types = isset( $anchor_block_type, $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) 1043 ? $hooked_blocks[ $anchor_block_type ][ $relative_position ] 1044 : array(); 1045 1046 /** This filter is documented in wp-includes/blocks.php */ 1047 $hooked_block_types = apply_filters( 'hooked_block_types', $hooked_block_types, $relative_position, $anchor_block_type, $context ); 1048 if ( empty( $hooked_block_types ) ) { 1049 return ''; 1050 } 1051 1052 foreach ( $hooked_block_types as $index => $hooked_block_type ) { 1053 $parsed_hooked_block = array( 1054 'blockName' => $hooked_block_type, 1055 'attrs' => array(), 1056 'innerBlocks' => array(), 1057 'innerContent' => array(), 1058 ); 1059 1060 /** This filter is documented in wp-includes/blocks.php */ 1061 $parsed_hooked_block = apply_filters( 'hooked_block', $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 1062 1063 /** This filter is documented in wp-includes/blocks.php */ 1064 $parsed_hooked_block = apply_filters( "hooked_block_{$hooked_block_type}", $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 1065 1066 if ( null === $parsed_hooked_block ) { 1067 unset( $hooked_block_types[ $index ] ); 1068 } 1069 } 1070 1071 $previously_ignored_hooked_blocks = $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] ?? array(); 1072 1073 $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] = array_unique( 1074 array_merge( 1075 $previously_ignored_hooked_blocks, 1076 $hooked_block_types 1077 ) 1078 ); 1079 1080 // Markup for the hooked blocks has already been created (in `insert_hooked_blocks`). 1081 return ''; 1082 } 1083 1084 /** 1085 * Runs the hooked blocks algorithm on the given content. 1086 * 1087 * @since 6.6.0 1088 * @since 6.7.0 Injects the `theme` attribute into Template Part blocks, even if no hooked blocks are registered. 1089 * @since 6.8.0 Have the `$context` parameter default to `null`, in which case `get_post()` will be called to use the current post as context. 1090 * @access private 1091 * 1092 * @param string $content Serialized content. 1093 * @param WP_Block_Template|WP_Post|array|null $context A block template, template part, post object, or pattern 1094 * that the blocks belong to. If set to `null`, `get_post()` 1095 * will be called to use the current post as context. 1096 * Default: `null`. 1097 * @param callable $callback A function that will be called for each block to generate 1098 * the markup for a given list of blocks that are hooked to it. 1099 * Default: 'insert_hooked_blocks'. 1100 * @return string The serialized markup. 1101 */ 1102 function apply_block_hooks_to_content( $content, $context = null, $callback = 'insert_hooked_blocks' ) { 1103 // Default to the current post if no context is provided. 1104 if ( null === $context ) { 1105 $context = get_post(); 1106 } 1107 1108 $hooked_blocks = get_hooked_blocks(); 1109 1110 $before_block_visitor = '_inject_theme_attribute_in_template_part_block'; 1111 $after_block_visitor = null; 1112 if ( ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ) ) { 1113 $before_block_visitor = make_before_block_visitor( $hooked_blocks, $context, $callback ); 1114 $after_block_visitor = make_after_block_visitor( $hooked_blocks, $context, $callback ); 1115 } 1116 1117 $block_allows_multiple_instances = array(); 1118 /* 1119 * Remove hooked blocks from `$hooked_block_types` if they have `multiple` set to false and 1120 * are already present in `$content`. 1121 */ 1122 foreach ( $hooked_blocks as $anchor_block_type => $relative_positions ) { 1123 foreach ( $relative_positions as $relative_position => $hooked_block_types ) { 1124 foreach ( $hooked_block_types as $index => $hooked_block_type ) { 1125 $hooked_block_type_definition = 1126 WP_Block_Type_Registry::get_instance()->get_registered( $hooked_block_type ); 1127 1128 $block_allows_multiple_instances[ $hooked_block_type ] = 1129 block_has_support( $hooked_block_type_definition, 'multiple', true ); 1130 1131 if ( 1132 ! $block_allows_multiple_instances[ $hooked_block_type ] && 1133 has_block( $hooked_block_type, $content ) 1134 ) { 1135 unset( $hooked_blocks[ $anchor_block_type ][ $relative_position ][ $index ] ); 1136 } 1137 } 1138 if ( empty( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) ) { 1139 unset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ); 1140 } 1141 } 1142 if ( empty( $hooked_blocks[ $anchor_block_type ] ) ) { 1143 unset( $hooked_blocks[ $anchor_block_type ] ); 1144 } 1145 } 1146 1147 /* 1148 * We also need to cover the case where the hooked block is not present in 1149 * `$content` at first and we're allowed to insert it once -- but not again. 1150 */ 1151 $suppress_single_instance_blocks = static function ( $hooked_block_types ) use ( &$block_allows_multiple_instances, $content ) { 1152 static $single_instance_blocks_present_in_content = array(); 1153 foreach ( $hooked_block_types as $index => $hooked_block_type ) { 1154 if ( ! isset( $block_allows_multiple_instances[ $hooked_block_type ] ) ) { 1155 $hooked_block_type_definition = 1156 WP_Block_Type_Registry::get_instance()->get_registered( $hooked_block_type ); 1157 1158 $block_allows_multiple_instances[ $hooked_block_type ] = 1159 block_has_support( $hooked_block_type_definition, 'multiple', true ); 1160 } 1161 1162 if ( $block_allows_multiple_instances[ $hooked_block_type ] ) { 1163 continue; 1164 } 1165 1166 // The block doesn't allow multiple instances, so we need to check if it's already present. 1167 if ( 1168 in_array( $hooked_block_type, $single_instance_blocks_present_in_content, true ) || 1169 has_block( $hooked_block_type, $content ) 1170 ) { 1171 unset( $hooked_block_types[ $index ] ); 1172 } else { 1173 // We can insert the block once, but need to remember not to insert it again. 1174 $single_instance_blocks_present_in_content[] = $hooked_block_type; 1175 } 1176 } 1177 return $hooked_block_types; 1178 }; 1179 add_filter( 'hooked_block_types', $suppress_single_instance_blocks, PHP_INT_MAX ); 1180 $content = traverse_and_serialize_blocks( 1181 parse_blocks( $content ), 1182 $before_block_visitor, 1183 $after_block_visitor 1184 ); 1185 remove_filter( 'hooked_block_types', $suppress_single_instance_blocks, PHP_INT_MAX ); 1186 1187 return $content; 1188 } 1189 1190 /** 1191 * Run the Block Hooks algorithm on a post object's content. 1192 * 1193 * This function is different from `apply_block_hooks_to_content` in that 1194 * it takes ignored hooked block information from the post's metadata into 1195 * account. This ensures that any blocks hooked as first or last child 1196 * of the block that corresponds to the post type are handled correctly. 1197 * 1198 * @since 6.8.0 1199 * @since 7.0.0 Added the `$ignored_hooked_blocks_at_root` parameter. 1200 * @access private 1201 * 1202 * @param string $content Serialized content. 1203 * @param WP_Post|null $post A post object that the content belongs to. If set to `null`, 1204 * `get_post()` will be called to use the current post as context. 1205 * Default: `null`. 1206 * @param callable $callback A function that will be called for each block to generate 1207 * the markup for a given list of blocks that are hooked to it. 1208 * Default: 'insert_hooked_blocks'. 1209 * @param array|null $ignored_hooked_blocks_at_root A reference to an array that will be populated 1210 * with the ignored hooked blocks at the root level. 1211 * Default: `null`. 1212 * @return string The serialized markup. 1213 */ 1214 function apply_block_hooks_to_content_from_post_object( 1215 $content, 1216 $post = null, 1217 $callback = 'insert_hooked_blocks', 1218 &$ignored_hooked_blocks_at_root = null 1219 ) { 1220 // Default to the current post if no context is provided. 1221 if ( null === $post ) { 1222 $post = get_post(); 1223 } 1224 1225 if ( ! $post instanceof WP_Post ) { 1226 return apply_block_hooks_to_content( $content, $post, $callback ); 1227 } 1228 1229 /* 1230 * If the content was created using the classic editor or using a single Classic block 1231 * (`core/freeform`), it might not contain any block markup at all. 1232 * However, we still might need to inject hooked blocks in the first child or last child 1233 * positions of the parent block. To be able to apply the Block Hooks algorithm, we wrap 1234 * the content in a `core/freeform` wrapper block. 1235 */ 1236 if ( ! has_blocks( $content ) ) { 1237 $original_content = $content; 1238 1239 $content_wrapped_in_classic_block = get_comment_delimited_block_content( 1240 'core/freeform', 1241 array(), 1242 $content 1243 ); 1244 1245 $content = $content_wrapped_in_classic_block; 1246 } 1247 1248 $attributes = array(); 1249 1250 // If context is a post object, `ignoredHookedBlocks` information is stored in its post meta. 1251 $ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); 1252 if ( ! empty( $ignored_hooked_blocks ) ) { 1253 $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true ); 1254 $attributes['metadata'] = array( 1255 'ignoredHookedBlocks' => $ignored_hooked_blocks, 1256 ); 1257 } 1258 1259 /* 1260 * We need to wrap the content in a temporary wrapper block with that metadata 1261 * so the Block Hooks algorithm can insert blocks that are hooked as first or last child 1262 * of the wrapper block. 1263 * To that end, we need to determine the wrapper block type based on the post type. 1264 */ 1265 if ( 'wp_navigation' === $post->post_type ) { 1266 $wrapper_block_type = 'core/navigation'; 1267 } elseif ( 'wp_block' === $post->post_type ) { 1268 $wrapper_block_type = 'core/block'; 1269 } else { 1270 $wrapper_block_type = 'core/post-content'; 1271 } 1272 1273 $content = get_comment_delimited_block_content( 1274 $wrapper_block_type, 1275 $attributes, 1276 $content 1277 ); 1278 1279 /* 1280 * We need to avoid inserting any blocks hooked into the `before` and `after` positions 1281 * of the temporary wrapper block that we create to wrap the content. 1282 * See https://core.trac.wordpress.org/ticket/63287 for more details. 1283 */ 1284 $suppress_blocks_from_insertion_before_and_after_wrapper_block = static function ( $hooked_block_types, $relative_position, $anchor_block_type ) use ( $wrapper_block_type ) { 1285 if ( 1286 $wrapper_block_type === $anchor_block_type && 1287 in_array( $relative_position, array( 'before', 'after' ), true ) 1288 ) { 1289 return array(); 1290 } 1291 return $hooked_block_types; 1292 }; 1293 1294 // Apply Block Hooks. 1295 add_filter( 'hooked_block_types', $suppress_blocks_from_insertion_before_and_after_wrapper_block, PHP_INT_MAX, 3 ); 1296 $content = apply_block_hooks_to_content( $content, $post, $callback ); 1297 remove_filter( 'hooked_block_types', $suppress_blocks_from_insertion_before_and_after_wrapper_block, PHP_INT_MAX ); 1298 1299 if ( null !== $ignored_hooked_blocks_at_root ) { 1300 // Check wrapper block's metadata for ignored hooked blocks at the root level, and populate the reference parameter if needed. 1301 $wrapper_block_markup = extract_serialized_parent_block( $content ); 1302 $wrapper_block = parse_blocks( $wrapper_block_markup )[0]; 1303 1304 if ( ! empty( $wrapper_block['attrs']['metadata']['ignoredHookedBlocks'] ) ) { 1305 $ignored_hooked_blocks_at_root = $wrapper_block['attrs']['metadata']['ignoredHookedBlocks']; 1306 } 1307 } 1308 1309 // Finally, we need to remove the temporary wrapper block. 1310 $content = remove_serialized_parent_block( $content ); 1311 1312 // If we wrapped the content in a `core/freeform` block, we also need to remove that. 1313 if ( ! empty( $content_wrapped_in_classic_block ) ) { 1314 /* 1315 * We cannot simply use remove_serialized_parent_block() here, 1316 * as that function assumes that the block wrapper is at the top level. 1317 * However, there might now be a hooked block inserted next to it 1318 * (as first or last child of the parent). 1319 */ 1320 $content = str_replace( $content_wrapped_in_classic_block, $original_content, $content ); 1321 } 1322 1323 return $content; 1324 } 1325 1326 /** 1327 * Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the inner blocks. 1328 * 1329 * @since 6.6.0 1330 * @access private 1331 * 1332 * @param string $serialized_block The serialized markup of a block and its inner blocks. 1333 * @return string The serialized markup of the inner blocks. 1334 */ 1335 function remove_serialized_parent_block( $serialized_block ) { 1336 $start = strpos( $serialized_block, '-->' ) + strlen( '-->' ); 1337 $end = strrpos( $serialized_block, '<!--' ); 1338 return substr( $serialized_block, $start, $end - $start ); 1339 } 1340 1341 /** 1342 * Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the wrapper block. 1343 * 1344 * @since 6.7.0 1345 * @access private 1346 * 1347 * @see remove_serialized_parent_block() 1348 * 1349 * @param string $serialized_block The serialized markup of a block and its inner blocks. 1350 * @return string The serialized markup of the wrapper block. 1351 */ 1352 function extract_serialized_parent_block( $serialized_block ) { 1353 $start = strpos( $serialized_block, '-->' ) + strlen( '-->' ); 1354 $end = strrpos( $serialized_block, '<!--' ); 1355 return substr( $serialized_block, 0, $start ) . substr( $serialized_block, $end ); 1356 } 1357 1358 /** 1359 * Updates the wp_postmeta with the list of ignored hooked blocks 1360 * where the inner blocks are stored as post content. 1361 * 1362 * @since 6.6.0 1363 * @since 6.8.0 Support non-`wp_navigation` post types. 1364 * @access private 1365 * 1366 * @param stdClass $post Post object. 1367 * @return stdClass The updated post object. 1368 */ 1369 function update_ignored_hooked_blocks_postmeta( $post ) { 1370 /* 1371 * In this scenario the user has likely tried to create a new post object via the REST API. 1372 * In which case we won't have a post ID to work with and store meta against. 1373 */ 1374 if ( empty( $post->ID ) ) { 1375 return $post; 1376 } 1377 1378 /* 1379 * Skip meta generation when consumers intentionally update specific fields 1380 * and omit the content update. 1381 */ 1382 if ( ! isset( $post->post_content ) ) { 1383 return $post; 1384 } 1385 1386 /* 1387 * Skip meta generation if post type is not set. 1388 */ 1389 if ( ! isset( $post->post_type ) ) { 1390 return $post; 1391 } 1392 1393 $attributes = array(); 1394 1395 $ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); 1396 if ( ! empty( $ignored_hooked_blocks ) ) { 1397 $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true ); 1398 $attributes['metadata'] = array( 1399 'ignoredHookedBlocks' => $ignored_hooked_blocks, 1400 ); 1401 } 1402 1403 if ( 'wp_navigation' === $post->post_type ) { 1404 $wrapper_block_type = 'core/navigation'; 1405 } elseif ( 'wp_block' === $post->post_type ) { 1406 $wrapper_block_type = 'core/block'; 1407 } else { 1408 $wrapper_block_type = 'core/post-content'; 1409 } 1410 1411 $markup = get_comment_delimited_block_content( 1412 $wrapper_block_type, 1413 $attributes, 1414 $post->post_content 1415 ); 1416 1417 $existing_post = get_post( $post->ID ); 1418 // Merge the existing post object with the updated post object to pass to the block hooks algorithm for context. 1419 $context = (object) array_merge( (array) $existing_post, (array) $post ); 1420 $context = new WP_Post( $context ); // Convert to WP_Post object. 1421 $serialized_block = apply_block_hooks_to_content( $markup, $context, 'set_ignored_hooked_blocks_metadata' ); 1422 $root_block = parse_blocks( $serialized_block )[0]; 1423 1424 $ignored_hooked_blocks = $root_block['attrs']['metadata']['ignoredHookedBlocks'] ?? array(); 1425 1426 if ( ! empty( $ignored_hooked_blocks ) ) { 1427 $existing_ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); 1428 if ( ! empty( $existing_ignored_hooked_blocks ) ) { 1429 $existing_ignored_hooked_blocks = json_decode( $existing_ignored_hooked_blocks, true ); 1430 $ignored_hooked_blocks = array_unique( array_merge( $ignored_hooked_blocks, $existing_ignored_hooked_blocks ) ); 1431 } 1432 1433 if ( ! isset( $post->meta_input ) ) { 1434 $post->meta_input = array(); 1435 } 1436 $post->meta_input['_wp_ignored_hooked_blocks'] = json_encode( $ignored_hooked_blocks ); 1437 } 1438 1439 $post->post_content = remove_serialized_parent_block( $serialized_block ); 1440 return $post; 1441 } 1442 1443 /** 1444 * Returns the markup for blocks hooked to the given anchor block in a specific relative position and then 1445 * adds a list of hooked block types to an anchor block's ignored hooked block types. 1446 * 1447 * This function is meant for internal use only. 1448 * 1449 * @since 6.6.0 1450 * @access private 1451 * 1452 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 1453 * @param string $relative_position The relative position of the hooked blocks. 1454 * Can be one of 'before', 'after', 'first_child', or 'last_child'. 1455 * @param array $hooked_blocks An array of hooked block types, grouped by anchor block and relative position. 1456 * @param WP_Block_Template|WP_Post|array $context The block template, template part, or pattern that the anchor block belongs to. 1457 * @return string 1458 */ 1459 function insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) { 1460 $markup = insert_hooked_blocks( $parsed_anchor_block, $relative_position, $hooked_blocks, $context ); 1461 $markup .= set_ignored_hooked_blocks_metadata( $parsed_anchor_block, $relative_position, $hooked_blocks, $context ); 1462 1463 return $markup; 1464 } 1465 1466 /** 1467 * Hooks into the REST API response for the Posts endpoint and adds the first and last inner blocks. 1468 * 1469 * @since 6.6.0 1470 * @since 6.8.0 Support non-`wp_navigation` post types. 1471 * @since 7.0.0 Set `_wp_ignored_hooked_blocks` meta in the response for blocks hooked at the root level. 1472 * 1473 * @param WP_REST_Response $response The response object. 1474 * @param WP_Post $post Post object. 1475 * @return WP_REST_Response The response object. 1476 */ 1477 function insert_hooked_blocks_into_rest_response( $response, $post ) { 1478 if ( empty( $response->data['content']['raw'] ) ) { 1479 return $response; 1480 } 1481 1482 $ignored_hooked_blocks_at_root = array(); 1483 $response->data['content']['raw'] = apply_block_hooks_to_content_from_post_object( 1484 $response->data['content']['raw'], 1485 $post, 1486 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata', 1487 $ignored_hooked_blocks_at_root 1488 ); 1489 1490 if ( ! empty( $ignored_hooked_blocks_at_root ) ) { 1491 $response->data['meta']['_wp_ignored_hooked_blocks'] = wp_json_encode( $ignored_hooked_blocks_at_root ); 1492 } 1493 1494 // If the rendered content was previously empty, we leave it like that. 1495 if ( empty( $response->data['content']['rendered'] ) ) { 1496 return $response; 1497 } 1498 1499 // `apply_block_hooks_to_content` is called above. Ensure it is not called again as a filter. 1500 $priority = has_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object' ); 1501 if ( false !== $priority ) { 1502 remove_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', $priority ); 1503 } 1504 1505 /** This filter is documented in wp-includes/post-template.php */ 1506 $response->data['content']['rendered'] = apply_filters( 1507 'the_content', 1508 $response->data['content']['raw'] 1509 ); 1510 1511 // Restore the filter if it was set initially. 1512 if ( false !== $priority ) { 1513 add_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', $priority ); 1514 } 1515 1516 return $response; 1517 } 1518 1519 /** 1520 * Returns a function that injects the theme attribute into, and hooked blocks before, a given block. 1521 * 1522 * The returned function can be used as `$pre_callback` argument to `traverse_and_serialize_block(s)`, 1523 * where it will inject the `theme` attribute into all Template Part blocks, and prepend the markup for 1524 * any blocks hooked `before` the given block and as its parent's `first_child`, respectively. 1525 * 1526 * This function is meant for internal use only. 1527 * 1528 * @since 6.4.0 1529 * @since 6.5.0 Added $callback argument. 1530 * @access private 1531 * 1532 * @param array $hooked_blocks An array of blocks hooked to another given block. 1533 * @param WP_Block_Template|WP_Post|array $context A block template, template part, post object, 1534 * or pattern that the blocks belong to. 1535 * @param callable $callback A function that will be called for each block to generate 1536 * the markup for a given list of blocks that are hooked to it. 1537 * Default: 'insert_hooked_blocks'. 1538 * @return callable A function that returns the serialized markup for the given block, 1539 * including the markup for any hooked blocks before it. 1540 */ 1541 function make_before_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) { 1542 /** 1543 * Injects hooked blocks before the given block, injects the `theme` attribute into Template Part blocks, and returns the serialized markup. 1544 * 1545 * If the current block is a Template Part block, inject the `theme` attribute. 1546 * Furthermore, prepend the markup for any blocks hooked `before` the given block and as its parent's 1547 * `first_child`, respectively, to the serialized markup for the given block. 1548 * 1549 * @param array $block The block to inject the theme attribute into, and hooked blocks before. Passed by reference. 1550 * @param array $parent_block The parent block of the given block. Passed by reference. Default null. 1551 * @param array $prev The previous sibling block of the given block. Default null. 1552 * @return string The serialized markup for the given block, with the markup for any hooked blocks prepended to it. 1553 */ 1554 return function ( &$block, &$parent_block = null, $prev = null ) use ( $hooked_blocks, $context, $callback ) { 1555 _inject_theme_attribute_in_template_part_block( $block ); 1556 1557 $markup = ''; 1558 1559 if ( $parent_block && ! $prev ) { 1560 // Candidate for first-child insertion. 1561 $markup .= call_user_func_array( 1562 $callback, 1563 array( &$parent_block, 'first_child', $hooked_blocks, $context ) 1564 ); 1565 } 1566 1567 $markup .= call_user_func_array( 1568 $callback, 1569 array( &$block, 'before', $hooked_blocks, $context ) 1570 ); 1571 1572 return $markup; 1573 }; 1574 } 1575 1576 /** 1577 * Returns a function that injects the hooked blocks after a given block. 1578 * 1579 * The returned function can be used as `$post_callback` argument to `traverse_and_serialize_block(s)`, 1580 * where it will append the markup for any blocks hooked `after` the given block and as its parent's 1581 * `last_child`, respectively. 1582 * 1583 * This function is meant for internal use only. 1584 * 1585 * @since 6.4.0 1586 * @since 6.5.0 Added $callback argument. 1587 * @access private 1588 * 1589 * @param array $hooked_blocks An array of blocks hooked to another block. 1590 * @param WP_Block_Template|WP_Post|array $context A block template, template part, post object, 1591 * or pattern that the blocks belong to. 1592 * @param callable $callback A function that will be called for each block to generate 1593 * the markup for a given list of blocks that are hooked to it. 1594 * Default: 'insert_hooked_blocks'. 1595 * @return callable A function that returns the serialized markup for the given block, 1596 * including the markup for any hooked blocks after it. 1597 */ 1598 function make_after_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) { 1599 /** 1600 * Injects hooked blocks after the given block, and returns the serialized markup. 1601 * 1602 * Append the markup for any blocks hooked `after` the given block and as its parent's 1603 * `last_child`, respectively, to the serialized markup for the given block. 1604 * 1605 * @param array $block The block to inject the hooked blocks after. Passed by reference. 1606 * @param array $parent_block The parent block of the given block. Passed by reference. Default null. 1607 * @param array $next The next sibling block of the given block. Default null. 1608 * @return string The serialized markup for the given block, with the markup for any hooked blocks appended to it. 1609 */ 1610 return function ( &$block, &$parent_block = null, $next = null ) use ( $hooked_blocks, $context, $callback ) { 1611 $markup = call_user_func_array( 1612 $callback, 1613 array( &$block, 'after', $hooked_blocks, $context ) 1614 ); 1615 1616 if ( $parent_block && ! $next ) { 1617 // Candidate for last-child insertion. 1618 $markup .= call_user_func_array( 1619 $callback, 1620 array( &$parent_block, 'last_child', $hooked_blocks, $context ) 1621 ); 1622 } 1623 1624 return $markup; 1625 }; 1626 } 1627 1628 /** 1629 * Given an array of attributes, returns a string in the serialized attributes 1630 * format prepared for post content. 1631 * 1632 * The serialized result is a JSON-encoded string, with unicode escape sequence 1633 * substitution for characters which might otherwise interfere with embedding 1634 * the result in an HTML comment. 1635 * 1636 * This function must produce output that remains in sync with the output of 1637 * the serializeAttributes JavaScript function in the block editor in order 1638 * to ensure consistent operation between PHP and JavaScript. 1639 * 1640 * @since 5.3.1 1641 * 1642 * @param array $block_attributes Attributes object. 1643 * @return string Serialized attributes. 1644 */ 1645 function serialize_block_attributes( $block_attributes ) { 1646 $encoded_attributes = wp_json_encode( $block_attributes, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); 1647 1648 return strtr( 1649 $encoded_attributes, 1650 array( 1651 '\\\\' => '\\u005c', 1652 '--' => '\\u002d\\u002d', 1653 '<' => '\\u003c', 1654 '>' => '\\u003e', 1655 '&' => '\\u0026', 1656 '\\"' => '\\u0022', 1657 ) 1658 ); 1659 } 1660 1661 /** 1662 * Returns the block name to use for serialization. This will remove the default 1663 * "core/" namespace from a block name. 1664 * 1665 * @since 5.3.1 1666 * 1667 * @param string|null $block_name Optional. Original block name. Null if the block name is unknown, 1668 * e.g. Classic blocks have their name set to null. Default null. 1669 * @return string Block name to use for serialization. 1670 */ 1671 function strip_core_block_namespace( $block_name = null ) { 1672 if ( is_string( $block_name ) && str_starts_with( $block_name, 'core/' ) ) { 1673 return substr( $block_name, 5 ); 1674 } 1675 1676 return $block_name; 1677 } 1678 1679 /** 1680 * Returns the content of a block, including comment delimiters. 1681 * 1682 * @since 5.3.1 1683 * 1684 * @param string|null $block_name Block name. Null if the block name is unknown, 1685 * e.g. Classic blocks have their name set to null. 1686 * @param array $block_attributes Block attributes. 1687 * @param string $block_content Block save content. 1688 * @return string Comment-delimited block content. 1689 */ 1690 function get_comment_delimited_block_content( $block_name, $block_attributes, $block_content ) { 1691 if ( is_null( $block_name ) ) { 1692 return $block_content; 1693 } 1694 1695 $serialized_block_name = strip_core_block_namespace( $block_name ); 1696 $serialized_attributes = empty( $block_attributes ) ? '' : serialize_block_attributes( $block_attributes ) . ' '; 1697 1698 if ( empty( $block_content ) ) { 1699 return sprintf( '<!-- wp:%s %s/-->', $serialized_block_name, $serialized_attributes ); 1700 } 1701 1702 return sprintf( 1703 '<!-- wp:%s %s-->%s<!-- /wp:%s -->', 1704 $serialized_block_name, 1705 $serialized_attributes, 1706 $block_content, 1707 $serialized_block_name 1708 ); 1709 } 1710 1711 /** 1712 * Returns the content of a block, including comment delimiters, serializing all 1713 * attributes from the given parsed block. 1714 * 1715 * This should be used when preparing a block to be saved to post content. 1716 * Prefer `render_block` when preparing a block for display. Unlike 1717 * `render_block`, this does not evaluate a block's `render_callback`, and will 1718 * instead preserve the markup as parsed. 1719 * 1720 * @since 5.3.1 1721 * 1722 * @param array $block { 1723 * An associative array of a single parsed block object. See WP_Block_Parser_Block. 1724 * 1725 * @type string|null $blockName Name of block. 1726 * @type array $attrs Attributes from block comment delimiters. 1727 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1728 * have the same structure as this one. 1729 * @type string $innerHTML HTML from inside block comment delimiters. 1730 * @type array $innerContent List of string fragments and null markers where 1731 * inner blocks were found. 1732 * } 1733 * @return string String of rendered HTML. 1734 */ 1735 function serialize_block( $block ) { 1736 $block_content = ''; 1737 1738 $index = 0; 1739 foreach ( $block['innerContent'] as $chunk ) { 1740 $block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] ); 1741 } 1742 1743 if ( ! is_array( $block['attrs'] ) ) { 1744 $block['attrs'] = array(); 1745 } 1746 1747 return get_comment_delimited_block_content( 1748 $block['blockName'], 1749 $block['attrs'], 1750 $block_content 1751 ); 1752 } 1753 1754 /** 1755 * Returns a joined string of the aggregate serialization of the given 1756 * parsed blocks. 1757 * 1758 * @since 5.3.1 1759 * 1760 * @param array[] $blocks { 1761 * Array of block structures. 1762 * 1763 * @type array ...$0 { 1764 * An associative array of a single parsed block object. See WP_Block_Parser_Block. 1765 * 1766 * @type string|null $blockName Name of block. 1767 * @type array $attrs Attributes from block comment delimiters. 1768 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1769 * have the same structure as this one. 1770 * @type string $innerHTML HTML from inside block comment delimiters. 1771 * @type array $innerContent List of string fragments and null markers where 1772 * inner blocks were found. 1773 * } 1774 * } 1775 * @return string String of rendered HTML. 1776 */ 1777 function serialize_blocks( $blocks ) { 1778 return implode( '', array_map( 'serialize_block', $blocks ) ); 1779 } 1780 1781 /** 1782 * Traverses a parsed block tree and applies callbacks before and after serializing it. 1783 * 1784 * Recursively traverses the block and its inner blocks and applies the two callbacks provided as 1785 * arguments, the first one before serializing the block, and the second one after serializing it. 1786 * If either callback returns a string value, it will be prepended and appended to the serialized 1787 * block markup, respectively. 1788 * 1789 * The callbacks will receive a reference to the current block as their first argument, so that they 1790 * can also modify it, and the current block's parent block as second argument. Finally, the 1791 * `$pre_callback` receives the previous block, whereas the `$post_callback` receives 1792 * the next block as third argument. 1793 * 1794 * Serialized blocks are returned including comment delimiters, and with all attributes serialized. 1795 * 1796 * This function should be used when there is a need to modify the saved block, or to inject markup 1797 * into the return value. Prefer `serialize_block` when preparing a block to be saved to post content. 1798 * 1799 * This function is meant for internal use only. 1800 * 1801 * @since 6.4.0 1802 * @access private 1803 * 1804 * @see serialize_block() 1805 * 1806 * @param array $block An associative array of a single parsed block object. See WP_Block_Parser_Block. 1807 * @param callable $pre_callback Callback to run on each block in the tree before it is traversed and serialized. 1808 * It is called with the following arguments: &$block, $parent_block, $previous_block. 1809 * Its string return value will be prepended to the serialized block markup. 1810 * @param callable $post_callback Callback to run on each block in the tree after it is traversed and serialized. 1811 * It is called with the following arguments: &$block, $parent_block, $next_block. 1812 * Its string return value will be appended to the serialized block markup. 1813 * @return string Serialized block markup. 1814 */ 1815 function traverse_and_serialize_block( $block, $pre_callback = null, $post_callback = null ) { 1816 $block_content = ''; 1817 $block_index = 0; 1818 1819 foreach ( $block['innerContent'] as $chunk ) { 1820 if ( is_string( $chunk ) ) { 1821 $block_content .= $chunk; 1822 } else { 1823 $inner_block = $block['innerBlocks'][ $block_index ]; 1824 1825 if ( is_callable( $pre_callback ) ) { 1826 $prev = 0 === $block_index 1827 ? null 1828 : $block['innerBlocks'][ $block_index - 1 ]; 1829 1830 $block_content .= call_user_func_array( 1831 $pre_callback, 1832 array( &$inner_block, &$block, $prev ) 1833 ); 1834 } 1835 1836 if ( is_callable( $post_callback ) ) { 1837 $next = count( $block['innerBlocks'] ) - 1 === $block_index 1838 ? null 1839 : $block['innerBlocks'][ $block_index + 1 ]; 1840 1841 $post_markup = call_user_func_array( 1842 $post_callback, 1843 array( &$inner_block, &$block, $next ) 1844 ); 1845 } 1846 1847 $block_content .= traverse_and_serialize_block( $inner_block, $pre_callback, $post_callback ); 1848 $block_content .= $post_markup ?? ''; 1849 1850 ++$block_index; 1851 } 1852 } 1853 1854 if ( ! is_array( $block['attrs'] ) ) { 1855 $block['attrs'] = array(); 1856 } 1857 1858 return get_comment_delimited_block_content( 1859 $block['blockName'], 1860 $block['attrs'], 1861 $block_content 1862 ); 1863 } 1864 1865 /** 1866 * Replaces patterns in a block tree with their content. 1867 * 1868 * @since 6.6.0 1869 * @since 7.0.0 Adds metadata to attributes of single-pattern container blocks. 1870 * 1871 * @param array $blocks An array blocks. 1872 * 1873 * @return array An array of blocks with patterns replaced by their content. 1874 */ 1875 function resolve_pattern_blocks( $blocks ) { 1876 static $inner_content; 1877 // Keep track of seen references to avoid infinite loops. 1878 static $seen_refs = array(); 1879 $i = 0; 1880 while ( $i < count( $blocks ) ) { 1881 if ( 'core/pattern' === $blocks[ $i ]['blockName'] ) { 1882 $attrs = $blocks[ $i ]['attrs']; 1883 1884 if ( empty( $attrs['slug'] ) ) { 1885 ++$i; 1886 continue; 1887 } 1888 1889 $slug = $attrs['slug']; 1890 1891 if ( isset( $seen_refs[ $slug ] ) ) { 1892 // Skip recursive patterns. 1893 array_splice( $blocks, $i, 1 ); 1894 continue; 1895 } 1896 1897 $registry = WP_Block_Patterns_Registry::get_instance(); 1898 $pattern = $registry->get_registered( $slug ); 1899 1900 // Skip unknown patterns. 1901 if ( ! $pattern ) { 1902 ++$i; 1903 continue; 1904 } 1905 1906 $blocks_to_insert = parse_blocks( trim( $pattern['content'] ) ); 1907 1908 /* 1909 * For single-root patterns, add the pattern name to make this a pattern instance in the editor. 1910 * If the pattern has metadata, merge it with the existing metadata. 1911 */ 1912 if ( count( $blocks_to_insert ) === 1 ) { 1913 $block_metadata = $blocks_to_insert[0]['attrs']['metadata'] ?? array(); 1914 $block_metadata['patternName'] = $slug; 1915 1916 /* 1917 * Merge pattern metadata with existing block metadata. 1918 * Pattern metadata takes precedence, but existing block metadata 1919 * is preserved as a fallback when the pattern doesn't define that field. 1920 * Only the defined fields (name, description, categories) are updated; 1921 * other metadata keys are preserved. 1922 */ 1923 foreach ( array( 1924 'name' => 'title', // 'title' is the field in the pattern object 'name' is the field in the block metadata. 1925 'description' => 'description', 1926 'categories' => 'categories', 1927 ) as $key => $pattern_key ) { 1928 $value = $pattern[ $pattern_key ] ?? $block_metadata[ $key ] ?? null; 1929 if ( $value ) { 1930 $block_metadata[ $key ] = is_array( $value ) 1931 ? array_map( 'sanitize_text_field', $value ) 1932 : sanitize_text_field( $value ); 1933 } 1934 } 1935 1936 $blocks_to_insert[0]['attrs']['metadata'] = $block_metadata; 1937 } 1938 1939 $seen_refs[ $slug ] = true; 1940 $prev_inner_content = $inner_content; 1941 $inner_content = null; 1942 $blocks_to_insert = resolve_pattern_blocks( $blocks_to_insert ); 1943 $inner_content = $prev_inner_content; 1944 unset( $seen_refs[ $slug ] ); 1945 array_splice( $blocks, $i, 1, $blocks_to_insert ); 1946 1947 // If we have inner content, we need to insert nulls in the 1948 // inner content array, otherwise serialize_blocks will skip 1949 // blocks. 1950 if ( $inner_content ) { 1951 $null_indices = array_keys( $inner_content, null, true ); 1952 $content_index = $null_indices[ $i ]; 1953 $nulls = array_fill( 0, count( $blocks_to_insert ), null ); 1954 array_splice( $inner_content, $content_index, 1, $nulls ); 1955 } 1956 1957 // Skip inserted blocks. 1958 $i += count( $blocks_to_insert ); 1959 } else { 1960 if ( ! empty( $blocks[ $i ]['innerBlocks'] ) ) { 1961 $prev_inner_content = $inner_content; 1962 $inner_content = $blocks[ $i ]['innerContent']; 1963 $blocks[ $i ]['innerBlocks'] = resolve_pattern_blocks( 1964 $blocks[ $i ]['innerBlocks'] 1965 ); 1966 $blocks[ $i ]['innerContent'] = $inner_content; 1967 $inner_content = $prev_inner_content; 1968 } 1969 ++$i; 1970 } 1971 } 1972 return $blocks; 1973 } 1974 1975 /** 1976 * Given an array of parsed block trees, applies callbacks before and after serializing them and 1977 * returns their concatenated output. 1978 * 1979 * Recursively traverses the blocks and their inner blocks and applies the two callbacks provided as 1980 * arguments, the first one before serializing a block, and the second one after serializing. 1981 * If either callback returns a string value, it will be prepended and appended to the serialized 1982 * block markup, respectively. 1983 * 1984 * The callbacks will receive a reference to the current block as their first argument, so that they 1985 * can also modify it, and the current block's parent block as second argument. Finally, the 1986 * `$pre_callback` receives the previous block, whereas the `$post_callback` receives 1987 * the next block as third argument. 1988 * 1989 * Serialized blocks are returned including comment delimiters, and with all attributes serialized. 1990 * 1991 * This function should be used when there is a need to modify the saved blocks, or to inject markup 1992 * into the return value. Prefer `serialize_blocks` when preparing blocks to be saved to post content. 1993 * 1994 * This function is meant for internal use only. 1995 * 1996 * @since 6.4.0 1997 * @access private 1998 * 1999 * @see serialize_blocks() 2000 * 2001 * @param array[] $blocks An array of parsed blocks. See WP_Block_Parser_Block. 2002 * @param callable $pre_callback Callback to run on each block in the tree before it is traversed and serialized. 2003 * It is called with the following arguments: &$block, $parent_block, $previous_block. 2004 * Its string return value will be prepended to the serialized block markup. 2005 * @param callable $post_callback Callback to run on each block in the tree after it is traversed and serialized. 2006 * It is called with the following arguments: &$block, $parent_block, $next_block. 2007 * Its string return value will be appended to the serialized block markup. 2008 * @return string Serialized block markup. 2009 */ 2010 function traverse_and_serialize_blocks( $blocks, $pre_callback = null, $post_callback = null ) { 2011 $result = ''; 2012 $parent_block = null; // At the top level, there is no parent block to pass to the callbacks; yet the callbacks expect a reference. 2013 2014 $pre_callback_is_callable = is_callable( $pre_callback ); 2015 $post_callback_is_callable = is_callable( $post_callback ); 2016 2017 foreach ( $blocks as $index => $block ) { 2018 if ( $pre_callback_is_callable ) { 2019 $prev = 0 === $index 2020 ? null 2021 : $blocks[ $index - 1 ]; 2022 2023 $result .= call_user_func_array( 2024 $pre_callback, 2025 array( &$block, &$parent_block, $prev ) 2026 ); 2027 } 2028 2029 if ( $post_callback_is_callable ) { 2030 $next = count( $blocks ) - 1 === $index 2031 ? null 2032 : $blocks[ $index + 1 ]; 2033 2034 $post_markup = call_user_func_array( 2035 $post_callback, 2036 array( &$block, &$parent_block, $next ) 2037 ); 2038 } 2039 2040 $result .= traverse_and_serialize_block( $block, $pre_callback, $post_callback ); 2041 $result .= $post_markup ?? ''; 2042 } 2043 2044 return $result; 2045 } 2046 2047 /** 2048 * Filters and sanitizes block content to remove non-allowable HTML 2049 * from parsed block attribute values. 2050 * 2051 * @since 5.3.1 2052 * 2053 * @param string $text Text that may contain block content. 2054 * @param array[]|string $allowed_html Optional. An array of allowed HTML elements and attributes, 2055 * or a context name such as 'post'. See wp_kses_allowed_html() 2056 * for the list of accepted context names. Default 'post'. 2057 * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. 2058 * Defaults to the result of wp_allowed_protocols(). 2059 * @return string The filtered and sanitized content result. 2060 */ 2061 function filter_block_content( $text, $allowed_html = 'post', $allowed_protocols = array() ) { 2062 $result = ''; 2063 2064 if ( str_contains( $text, '<!--' ) && str_contains( $text, '--->' ) ) { 2065 $text = preg_replace_callback( '%<!--(.*?)--->%', '_filter_block_content_callback', $text ); 2066 } 2067 2068 $blocks = parse_blocks( $text ); 2069 foreach ( $blocks as $block ) { 2070 $block = filter_block_kses( $block, $allowed_html, $allowed_protocols ); 2071 $result .= serialize_block( $block ); 2072 } 2073 2074 return $result; 2075 } 2076 2077 /** 2078 * Callback used for regular expression replacement in filter_block_content(). 2079 * 2080 * @since 6.2.1 2081 * @access private 2082 * 2083 * @param array $matches Array of preg_replace_callback matches. 2084 * @return string Replacement string. 2085 */ 2086 function _filter_block_content_callback( $matches ) { 2087 return '<!--' . rtrim( $matches[1], '-' ) . '-->'; 2088 } 2089 2090 /** 2091 * Filters and sanitizes a parsed block to remove non-allowable HTML 2092 * from block attribute values. 2093 * 2094 * @since 5.3.1 2095 * 2096 * @param WP_Block_Parser_Block $block The parsed block object. 2097 * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, 2098 * or a context name such as 'post'. See wp_kses_allowed_html() 2099 * for the list of accepted context names. 2100 * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. 2101 * Defaults to the result of wp_allowed_protocols(). 2102 * @return array The filtered and sanitized block object result. 2103 */ 2104 function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) { 2105 $block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols, $block ); 2106 2107 if ( is_array( $block['innerBlocks'] ) ) { 2108 foreach ( $block['innerBlocks'] as $i => $inner_block ) { 2109 $block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols ); 2110 } 2111 } 2112 2113 return $block; 2114 } 2115 2116 /** 2117 * Filters and sanitizes a parsed block attribute value to remove 2118 * non-allowable HTML. 2119 * 2120 * @since 5.3.1 2121 * @since 6.5.5 Added the `$block_context` parameter. 2122 * 2123 * @param string[]|string $value The attribute value to filter. 2124 * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, 2125 * or a context name such as 'post'. See wp_kses_allowed_html() 2126 * for the list of accepted context names. 2127 * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. 2128 * Defaults to the result of wp_allowed_protocols(). 2129 * @param array $block_context Optional. The block the attribute belongs to, in parsed block array format. 2130 * @return string[]|string The filtered and sanitized result. 2131 */ 2132 function filter_block_kses_value( $value, $allowed_html, $allowed_protocols = array(), $block_context = null ) { 2133 if ( is_array( $value ) ) { 2134 foreach ( $value as $key => $inner_value ) { 2135 $filtered_key = filter_block_kses_value( $key, $allowed_html, $allowed_protocols, $block_context ); 2136 $filtered_value = filter_block_kses_value( $inner_value, $allowed_html, $allowed_protocols, $block_context ); 2137 2138 if ( isset( $block_context['blockName'] ) && 'core/template-part' === $block_context['blockName'] ) { 2139 $filtered_value = filter_block_core_template_part_attributes( $filtered_value, $filtered_key, $allowed_html ); 2140 } 2141 if ( $filtered_key !== $key ) { 2142 unset( $value[ $key ] ); 2143 } 2144 2145 $value[ $filtered_key ] = $filtered_value; 2146 } 2147 } elseif ( is_string( $value ) ) { 2148 return wp_kses( $value, $allowed_html, $allowed_protocols ); 2149 } 2150 2151 return $value; 2152 } 2153 2154 /** 2155 * Sanitizes the value of the Template Part block's `tagName` attribute. 2156 * 2157 * @since 6.5.5 2158 * 2159 * @param string $attribute_value The attribute value to filter. 2160 * @param string $attribute_name The attribute name. 2161 * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, 2162 * or a context name such as 'post'. See wp_kses_allowed_html() 2163 * for the list of accepted context names. 2164 * @return string The sanitized attribute value. 2165 */ 2166 function filter_block_core_template_part_attributes( $attribute_value, $attribute_name, $allowed_html ) { 2167 if ( empty( $attribute_value ) || 'tagName' !== $attribute_name ) { 2168 return $attribute_value; 2169 } 2170 if ( ! is_array( $allowed_html ) ) { 2171 $allowed_html = wp_kses_allowed_html( $allowed_html ); 2172 } 2173 return isset( $allowed_html[ $attribute_value ] ) ? $attribute_value : ''; 2174 } 2175 2176 /** 2177 * Parses blocks out of a content string, and renders those appropriate for the excerpt. 2178 * 2179 * As the excerpt should be a small string of text relevant to the full post content, 2180 * this function renders the blocks that are most likely to contain such text. 2181 * 2182 * @since 5.0.0 2183 * 2184 * @param string $content The content to parse. 2185 * @return string The parsed and filtered content. 2186 */ 2187 function excerpt_remove_blocks( $content ) { 2188 if ( ! has_blocks( $content ) ) { 2189 return $content; 2190 } 2191 2192 $allowed_inner_blocks = array( 2193 // Classic blocks have their blockName set to null. 2194 null, 2195 'core/freeform', 2196 'core/heading', 2197 'core/html', 2198 'core/list', 2199 'core/media-text', 2200 'core/paragraph', 2201 'core/preformatted', 2202 'core/pullquote', 2203 'core/quote', 2204 'core/table', 2205 'core/verse', 2206 ); 2207 2208 $allowed_wrapper_blocks = array( 2209 'core/columns', 2210 'core/column', 2211 'core/group', 2212 ); 2213 2214 /** 2215 * Filters the list of blocks that can be used as wrapper blocks, allowing 2216 * excerpts to be generated from the `innerBlocks` of these wrappers. 2217 * 2218 * @since 5.8.0 2219 * 2220 * @param string[] $allowed_wrapper_blocks The list of names of allowed wrapper blocks. 2221 */ 2222 $allowed_wrapper_blocks = apply_filters( 'excerpt_allowed_wrapper_blocks', $allowed_wrapper_blocks ); 2223 2224 $allowed_blocks = array_merge( $allowed_inner_blocks, $allowed_wrapper_blocks ); 2225 2226 /** 2227 * Filters the list of blocks that can contribute to the excerpt. 2228 * 2229 * If a dynamic block is added to this list, it must not generate another 2230 * excerpt, as this will cause an infinite loop to occur. 2231 * 2232 * @since 5.0.0 2233 * 2234 * @param string[] $allowed_blocks The list of names of allowed blocks. 2235 */ 2236 $allowed_blocks = apply_filters( 'excerpt_allowed_blocks', $allowed_blocks ); 2237 $blocks = parse_blocks( $content ); 2238 $output = ''; 2239 2240 foreach ( $blocks as $block ) { 2241 // Hide the block whenever the value is boolean false, regardless of the 2242 // block's current visibility support. This prevents blocks that previously 2243 // supported visibility from unintentionally appearing on the front end 2244 // after their support was disabled. 2245 if ( false === ( $block['attrs']['metadata']['blockVisibility'] ?? null ) ) { 2246 continue; 2247 } 2248 2249 if ( in_array( $block['blockName'], $allowed_blocks, true ) ) { 2250 if ( ! empty( $block['innerBlocks'] ) ) { 2251 if ( in_array( $block['blockName'], $allowed_wrapper_blocks, true ) ) { 2252 $output .= _excerpt_render_inner_blocks( $block, $allowed_blocks ); 2253 continue; 2254 } 2255 2256 // Skip the block if it has disallowed or nested inner blocks. 2257 foreach ( $block['innerBlocks'] as $inner_block ) { 2258 if ( 2259 ! in_array( $inner_block['blockName'], $allowed_inner_blocks, true ) || 2260 ! empty( $inner_block['innerBlocks'] ) 2261 ) { 2262 continue 2; 2263 } 2264 } 2265 } 2266 2267 $output .= render_block( $block ); 2268 } 2269 } 2270 2271 return $output; 2272 } 2273 2274 /** 2275 * Parses footnotes markup out of a content string, 2276 * and renders those appropriate for the excerpt. 2277 * 2278 * @since 6.3.0 2279 * 2280 * @param string $content The content to parse. 2281 * @return string The parsed and filtered content. 2282 */ 2283 function excerpt_remove_footnotes( $content ) { 2284 if ( ! str_contains( $content, 'data-fn=' ) ) { 2285 return $content; 2286 } 2287 2288 return preg_replace( 2289 '_<sup data-fn="[^"]+" class="[^"]+">\s*<a href="[^"]+" id="[^"]+">\d+</a>\s*</sup>_', 2290 '', 2291 $content 2292 ); 2293 } 2294 2295 /** 2296 * Renders inner blocks from the allowed wrapper blocks 2297 * for generating an excerpt. 2298 * 2299 * @since 5.8.0 2300 * @access private 2301 * 2302 * @param array $parsed_block The parsed block. 2303 * @param array $allowed_blocks The list of allowed inner blocks. 2304 * @return string The rendered inner blocks. 2305 */ 2306 function _excerpt_render_inner_blocks( $parsed_block, $allowed_blocks ) { 2307 $output = ''; 2308 2309 foreach ( $parsed_block['innerBlocks'] as $inner_block ) { 2310 // Hide the block whenever the value is boolean false, regardless of the 2311 // block's current visibility support. This prevents blocks that previously 2312 // supported visibility from unintentionally appearing on the front end 2313 // after their support was disabled. 2314 if ( false === ( $inner_block['attrs']['metadata']['blockVisibility'] ?? null ) ) { 2315 continue; 2316 } 2317 2318 if ( ! in_array( $inner_block['blockName'], $allowed_blocks, true ) ) { 2319 continue; 2320 } 2321 2322 if ( empty( $inner_block['innerBlocks'] ) ) { 2323 $output .= render_block( $inner_block ); 2324 } else { 2325 $output .= _excerpt_render_inner_blocks( $inner_block, $allowed_blocks ); 2326 } 2327 } 2328 2329 return $output; 2330 } 2331 2332 /** 2333 * Renders a single block into a HTML string. 2334 * 2335 * @since 5.0.0 2336 * 2337 * @global WP_Post $post The post to edit. 2338 * 2339 * @param array $parsed_block { 2340 * An associative array of the block being rendered. See WP_Block_Parser_Block. 2341 * 2342 * @type string|null $blockName Name of block. 2343 * @type array $attrs Attributes from block comment delimiters. 2344 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 2345 * have the same structure as this one. 2346 * @type string $innerHTML HTML from inside block comment delimiters. 2347 * @type array $innerContent List of string fragments and null markers where 2348 * inner blocks were found. 2349 * } 2350 * @return string String of rendered HTML. 2351 */ 2352 function render_block( $parsed_block ) { 2353 global $post; 2354 $parent_block = null; 2355 2356 /** 2357 * Allows render_block() to be short-circuited, by returning a non-null value. 2358 * 2359 * @since 5.1.0 2360 * @since 5.9.0 The `$parent_block` parameter was added. 2361 * 2362 * @param string|null $pre_render The pre-rendered content. Default null. 2363 * @param array $parsed_block { 2364 * An associative array of the block being rendered. See WP_Block_Parser_Block. 2365 * 2366 * @type string|null $blockName Name of block. 2367 * @type array $attrs Attributes from block comment delimiters. 2368 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 2369 * have the same structure as this one. 2370 * @type string $innerHTML HTML from inside block comment delimiters. 2371 * @type array $innerContent List of string fragments and null markers where 2372 * inner blocks were found. 2373 * } 2374 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. 2375 */ 2376 $pre_render = apply_filters( 'pre_render_block', null, $parsed_block, $parent_block ); 2377 if ( ! is_null( $pre_render ) ) { 2378 return $pre_render; 2379 } 2380 2381 $source_block = $parsed_block; 2382 2383 /** 2384 * Filters the block being rendered in render_block(), before it's processed. 2385 * 2386 * @since 5.1.0 2387 * @since 5.9.0 The `$parent_block` parameter was added. 2388 * 2389 * @param array $parsed_block { 2390 * An associative array of the block being rendered. See WP_Block_Parser_Block. 2391 * 2392 * @type string|null $blockName Name of block. 2393 * @type array $attrs Attributes from block comment delimiters. 2394 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 2395 * have the same structure as this one. 2396 * @type string $innerHTML HTML from inside block comment delimiters. 2397 * @type array $innerContent List of string fragments and null markers where 2398 * inner blocks were found. 2399 * } 2400 * @param array $source_block { 2401 * An un-modified copy of `$parsed_block`, as it appeared in the source content. 2402 * See WP_Block_Parser_Block. 2403 * 2404 * @type string|null $blockName Name of block. 2405 * @type array $attrs Attributes from block comment delimiters. 2406 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 2407 * have the same structure as this one. 2408 * @type string $innerHTML HTML from inside block comment delimiters. 2409 * @type array $innerContent List of string fragments and null markers where 2410 * inner blocks were found. 2411 * } 2412 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. 2413 */ 2414 $parsed_block = apply_filters( 'render_block_data', $parsed_block, $source_block, $parent_block ); 2415 2416 $context = array(); 2417 2418 if ( $post instanceof WP_Post ) { 2419 $context['postId'] = $post->ID; 2420 2421 /* 2422 * The `postType` context is largely unnecessary server-side, since the ID 2423 * is usually sufficient on its own. That being said, since a block's 2424 * manifest is expected to be shared between the server and the client, 2425 * it should be included to consistently fulfill the expectation. 2426 */ 2427 $context['postType'] = $post->post_type; 2428 } 2429 2430 /** 2431 * Filters the default context provided to a rendered block. 2432 * 2433 * @since 5.5.0 2434 * @since 5.9.0 The `$parent_block` parameter was added. 2435 * 2436 * @param array $context Default context. 2437 * @param array $parsed_block { 2438 * An associative array of the block being rendered. See WP_Block_Parser_Block. 2439 * 2440 * @type string|null $blockName Name of block. 2441 * @type array $attrs Attributes from block comment delimiters. 2442 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 2443 * have the same structure as this one. 2444 * @type string $innerHTML HTML from inside block comment delimiters. 2445 * @type array $innerContent List of string fragments and null markers where 2446 * inner blocks were found. 2447 * } 2448 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. 2449 */ 2450 $context = apply_filters( 'render_block_context', $context, $parsed_block, $parent_block ); 2451 2452 $block = new WP_Block( $parsed_block, $context ); 2453 2454 return $block->render(); 2455 } 2456 2457 /** 2458 * Parses blocks out of a content string. 2459 * 2460 * Given an HTML document, this function fully-parses block content, producing 2461 * a tree of blocks and their contents, as well as top-level non-block content, 2462 * which will appear as a block with no `blockName`. 2463 * 2464 * This function can be memory heavy for certain documents, particularly those 2465 * with deeply-nested blocks or blocks with extensive attribute values. Further, 2466 * this function must parse an entire document in one atomic operation. 2467 * 2468 * If the entire parsed document is not necessary, consider using {@see WP_Block_Processor} 2469 * instead, as it provides a streaming and low-overhead interface for finding blocks. 2470 * 2471 * @since 5.0.0 2472 * 2473 * @param string $content Post content. 2474 * @return array[] { 2475 * Array of block structures. 2476 * 2477 * @type array ...$0 { 2478 * An associative array of a single parsed block object. See WP_Block_Parser_Block. 2479 * 2480 * @type string|null $blockName Name of block. 2481 * @type array $attrs Attributes from block comment delimiters. 2482 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 2483 * have the same structure as this one. 2484 * @type string $innerHTML HTML from inside block comment delimiters. 2485 * @type array $innerContent List of string fragments and null markers where 2486 * inner blocks were found. 2487 * } 2488 * } 2489 */ 2490 function parse_blocks( $content ) { 2491 /** 2492 * Filter to allow plugins to replace the server-side block parser. 2493 * 2494 * @since 5.0.0 2495 * 2496 * @param string $parser_class Name of block parser class. 2497 */ 2498 $parser_class = apply_filters( 'block_parser_class', 'WP_Block_Parser' ); 2499 2500 $parser = new $parser_class(); 2501 return $parser->parse( $content ); 2502 } 2503 2504 /** 2505 * Parses dynamic blocks out of `post_content` and re-renders them. 2506 * 2507 * @since 5.0.0 2508 * 2509 * @param string $content Post content. 2510 * @return string Updated post content. 2511 */ 2512 function do_blocks( $content ) { 2513 $blocks = parse_blocks( $content ); 2514 $top_level_block_count = count( $blocks ); 2515 $output = ''; 2516 2517 /** 2518 * Parsed blocks consist of a list of top-level blocks. Those top-level 2519 * blocks may themselves contain nested inner blocks. However, every 2520 * top-level block is rendered independently, meaning there are no data 2521 * dependencies between them. 2522 * 2523 * Ideally, therefore, the parser would only need to parse one complete 2524 * top-level block at a time, render it, and move on. Unfortunately, this 2525 * is not possible with {@see \parse_blocks()} because it must parse the 2526 * entire given document at once. 2527 * 2528 * While the current implementation prevents this optimization, it’s still 2529 * possible to reduce the peak memory use when calls to `render_block()` 2530 * on those top-level blocks are memory-heavy (which many of them are). 2531 * By setting each parsed block to `NULL` after rendering it, any memory 2532 * allocated during the render will be freed and reused for the next block. 2533 * Before making this change, that memory was retained and would lead to 2534 * out-of-memory crashes for certain posts that now run with this change. 2535 */ 2536 for ( $i = 0; $i < $top_level_block_count; $i++ ) { 2537 $output .= render_block( $blocks[ $i ] ); 2538 $blocks[ $i ] = null; 2539 } 2540 2541 // If there are blocks in this content, we shouldn't run wpautop() on it later. 2542 $priority = has_filter( 'the_content', 'wpautop' ); 2543 if ( false !== $priority && doing_filter( 'the_content' ) && has_blocks( $content ) ) { 2544 remove_filter( 'the_content', 'wpautop', $priority ); 2545 add_filter( 'the_content', '_restore_wpautop_hook', $priority + 1 ); 2546 } 2547 2548 return $output; 2549 } 2550 2551 /** 2552 * If do_blocks() needs to remove wpautop() from the `the_content` filter, this re-adds it afterwards, 2553 * for subsequent `the_content` usage. 2554 * 2555 * @since 5.0.0 2556 * @access private 2557 * 2558 * @param string $content The post content running through this filter. 2559 * @return string The unmodified content. 2560 */ 2561 function _restore_wpautop_hook( $content ) { 2562 $current_priority = has_filter( 'the_content', '_restore_wpautop_hook' ); 2563 2564 add_filter( 'the_content', 'wpautop', $current_priority - 1 ); 2565 remove_filter( 'the_content', '_restore_wpautop_hook', $current_priority ); 2566 2567 return $content; 2568 } 2569 2570 /** 2571 * Returns the current version of the block format that the content string is using. 2572 * 2573 * If the string doesn't contain blocks, it returns 0. 2574 * 2575 * @since 5.0.0 2576 * 2577 * @param string $content Content to test. 2578 * @return int The block format version is 1 if the content contains one or more blocks, 0 otherwise. 2579 */ 2580 function block_version( $content ) { 2581 return has_blocks( $content ) ? 1 : 0; 2582 } 2583 2584 /** 2585 * Registers a new block style. 2586 * 2587 * @since 5.3.0 2588 * @since 6.6.0 Added support for registering styles for multiple block types. 2589 * 2590 * @link https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/ 2591 * 2592 * @param string|string[] $block_name Block type name including namespace or array of namespaced block type names. 2593 * @param array $style_properties Array containing the properties of the style name, label, 2594 * style_handle (name of the stylesheet to be enqueued), 2595 * inline_style (string containing the CSS to be added), 2596 * style_data (theme.json-like array to generate CSS from). 2597 * See WP_Block_Styles_Registry::register(). 2598 * @return bool True if the block style was registered with success and false otherwise. 2599 */ 2600 function register_block_style( $block_name, $style_properties ) { 2601 return WP_Block_Styles_Registry::get_instance()->register( $block_name, $style_properties ); 2602 } 2603 2604 /** 2605 * Unregisters a block style. 2606 * 2607 * @since 5.3.0 2608 * 2609 * @param string $block_name Block type name including namespace. 2610 * @param string $block_style_name Block style name. 2611 * @return bool True if the block style was unregistered with success and false otherwise. 2612 */ 2613 function unregister_block_style( $block_name, $block_style_name ) { 2614 return WP_Block_Styles_Registry::get_instance()->unregister( $block_name, $block_style_name ); 2615 } 2616 2617 /** 2618 * Checks whether the current block type supports the feature requested. 2619 * 2620 * @since 5.8.0 2621 * @since 6.4.0 The `$feature` parameter now supports a string. 2622 * 2623 * @param WP_Block_Type|null $block_type Block type to check for support. 2624 * @param string|array $feature Feature slug, or path to a specific feature to check support for. 2625 * @param mixed $default_value Optional. Fallback value for feature support. Default false. 2626 * @return bool Whether the feature is supported. 2627 */ 2628 function block_has_support( $block_type, $feature, $default_value = false ) { 2629 $block_support = $default_value; 2630 if ( $block_type instanceof WP_Block_Type ) { 2631 if ( is_array( $feature ) && count( $feature ) === 1 ) { 2632 $feature = $feature[0]; 2633 } 2634 2635 if ( is_array( $feature ) ) { 2636 $block_support = _wp_array_get( $block_type->supports, $feature, $default_value ); 2637 } elseif ( isset( $block_type->supports[ $feature ] ) ) { 2638 $block_support = $block_type->supports[ $feature ]; 2639 } 2640 } 2641 2642 return true === $block_support || is_array( $block_support ); 2643 } 2644 2645 /** 2646 * Converts typography keys declared under `supports.*` to `supports.typography.*`. 2647 * 2648 * Displays a `_doing_it_wrong()` notice when a block using the older format is detected. 2649 * 2650 * @since 5.8.0 2651 * 2652 * @param array $metadata Metadata for registering a block type. 2653 * @return array Filtered metadata for registering a block type. 2654 */ 2655 function wp_migrate_old_typography_shape( $metadata ) { 2656 if ( ! isset( $metadata['supports'] ) ) { 2657 return $metadata; 2658 } 2659 2660 $typography_keys = array( 2661 '__experimentalFontFamily', 2662 '__experimentalFontStyle', 2663 '__experimentalFontWeight', 2664 '__experimentalLetterSpacing', 2665 '__experimentalTextDecoration', 2666 '__experimentalTextTransform', 2667 'fontSize', 2668 'lineHeight', 2669 ); 2670 2671 foreach ( $typography_keys as $typography_key ) { 2672 $support_for_key = $metadata['supports'][ $typography_key ] ?? null; 2673 2674 if ( null !== $support_for_key ) { 2675 _doing_it_wrong( 2676 'register_block_type_from_metadata()', 2677 sprintf( 2678 /* translators: 1: Block type, 2: Typography supports key, e.g: fontSize, lineHeight, etc. 3: block.json, 4: Old metadata key, 5: New metadata key. */ 2679 __( 'Block "%1$s" is declaring %2$s support in %3$s file under %4$s. %2$s support is now declared under %5$s.' ), 2680 $metadata['name'], 2681 "<code>$typography_key</code>", 2682 '<code>block.json</code>', 2683 "<code>supports.$typography_key</code>", 2684 "<code>supports.typography.$typography_key</code>" 2685 ), 2686 '5.8.0' 2687 ); 2688 2689 _wp_array_set( $metadata['supports'], array( 'typography', $typography_key ), $support_for_key ); 2690 unset( $metadata['supports'][ $typography_key ] ); 2691 } 2692 } 2693 2694 return $metadata; 2695 } 2696 2697 /** 2698 * Helper function that constructs a WP_Query args array from 2699 * a `Query` block properties. 2700 * 2701 * It's used in Query Loop, Query Pagination Numbers and Query Pagination Next blocks. 2702 * 2703 * @since 5.8.0 2704 * @since 6.1.0 Added `query_loop_block_query_vars` filter and `parents` support in query. 2705 * @since 6.7.0 Added support for the `format` property in query. 2706 * @since 7.0.0 Updated `taxQuery` structure. 2707 * 2708 * @param WP_Block $block Block instance. 2709 * @param int $page Current query's page. 2710 * 2711 * @return array Returns the constructed WP_Query arguments. 2712 */ 2713 function build_query_vars_from_query_block( $block, $page ) { 2714 $query = array( 2715 'post_type' => 'post', 2716 'order' => 'DESC', 2717 'orderby' => 'date', 2718 'post__not_in' => array(), 2719 'tax_query' => array(), 2720 ); 2721 2722 if ( isset( $block->context['query'] ) ) { 2723 if ( ! empty( $block->context['query']['postType'] ) ) { 2724 $post_type_param = $block->context['query']['postType']; 2725 if ( is_post_type_viewable( $post_type_param ) ) { 2726 $query['post_type'] = $post_type_param; 2727 } 2728 } 2729 if ( isset( $block->context['query']['sticky'] ) && ! empty( $block->context['query']['sticky'] ) ) { 2730 $sticky = get_option( 'sticky_posts' ); 2731 if ( 'only' === $block->context['query']['sticky'] ) { 2732 /* 2733 * Passing an empty array to post__in will return have_posts() as true (and all posts will be returned). 2734 * Logic should be used before hand to determine if WP_Query should be used in the event that the array 2735 * being passed to post__in is empty. 2736 * 2737 * @see https://core.trac.wordpress.org/ticket/28099 2738 */ 2739 $query['post__in'] = ! empty( $sticky ) ? $sticky : array( 0 ); 2740 $query['ignore_sticky_posts'] = 1; 2741 } elseif ( 'exclude' === $block->context['query']['sticky'] ) { 2742 $query['post__not_in'] = array_merge( $query['post__not_in'], $sticky ); 2743 } elseif ( 'ignore' === $block->context['query']['sticky'] ) { 2744 $query['ignore_sticky_posts'] = 1; 2745 } 2746 } 2747 if ( ! empty( $block->context['query']['exclude'] ) ) { 2748 $excluded_post_ids = array_map( 'intval', $block->context['query']['exclude'] ); 2749 $excluded_post_ids = array_filter( $excluded_post_ids ); 2750 $query['post__not_in'] = array_merge( $query['post__not_in'], $excluded_post_ids ); 2751 } 2752 if ( 2753 isset( $block->context['query']['perPage'] ) && 2754 is_numeric( $block->context['query']['perPage'] ) 2755 ) { 2756 $per_page = absint( $block->context['query']['perPage'] ); 2757 $offset = 0; 2758 2759 if ( 2760 isset( $block->context['query']['offset'] ) && 2761 is_numeric( $block->context['query']['offset'] ) 2762 ) { 2763 $offset = absint( $block->context['query']['offset'] ); 2764 } 2765 2766 $query['offset'] = ( $per_page * ( $page - 1 ) ) + $offset; 2767 $query['posts_per_page'] = $per_page; 2768 } 2769 // Migrate `categoryIds` and `tagIds` to `tax_query` for backwards compatibility. 2770 if ( ! empty( $block->context['query']['categoryIds'] ) || ! empty( $block->context['query']['tagIds'] ) ) { 2771 $tax_query_back_compat = array(); 2772 if ( ! empty( $block->context['query']['categoryIds'] ) ) { 2773 $tax_query_back_compat[] = array( 2774 'taxonomy' => 'category', 2775 'terms' => array_filter( array_map( 'intval', $block->context['query']['categoryIds'] ) ), 2776 'include_children' => false, 2777 ); 2778 } 2779 if ( ! empty( $block->context['query']['tagIds'] ) ) { 2780 $tax_query_back_compat[] = array( 2781 'taxonomy' => 'post_tag', 2782 'terms' => array_filter( array_map( 'intval', $block->context['query']['tagIds'] ) ), 2783 'include_children' => false, 2784 ); 2785 } 2786 $query['tax_query'] = array_merge( $query['tax_query'], $tax_query_back_compat ); 2787 } 2788 2789 if ( ! empty( $block->context['query']['taxQuery'] ) && is_array( $block->context['query']['taxQuery'] ) ) { 2790 $tax_query_input = $block->context['query']['taxQuery']; 2791 $tax_query = array(); 2792 // If there are keys other than include/exclude, it's the old 2793 // format e.g. "taxQuery":{"category":[4]} 2794 if ( ! empty( array_diff( array_keys( $tax_query_input ), array( 'include', 'exclude' ) ) ) ) { 2795 foreach ( $block->context['query']['taxQuery'] as $taxonomy => $terms ) { 2796 if ( is_taxonomy_viewable( $taxonomy ) && ! empty( $terms ) ) { 2797 $tax_query[] = array( 2798 'taxonomy' => $taxonomy, 2799 'terms' => array_filter( array_map( 'intval', $terms ) ), 2800 'include_children' => false, 2801 ); 2802 } 2803 } 2804 } else { 2805 // This is the new format e.g. "taxQuery":{"include":{"category":[4]},"exclude":{"post_tag":[5]}} 2806 2807 // Helper function to build tax_query conditions from taxonomy terms. 2808 $build_conditions = static function ( $terms, string $operator = 'IN' ): array { 2809 $terms = (array) $terms; 2810 $conditions = array(); 2811 foreach ( $terms as $taxonomy => $tax_terms ) { 2812 if ( ! empty( $tax_terms ) && is_taxonomy_viewable( $taxonomy ) ) { 2813 $conditions[] = array( 2814 'taxonomy' => $taxonomy, 2815 'terms' => array_filter( array_map( 'intval', $tax_terms ) ), 2816 'operator' => $operator, 2817 'include_children' => false, 2818 ); 2819 } 2820 } 2821 return $conditions; 2822 }; 2823 2824 // Separate exclude from include terms. 2825 $exclude_terms = isset( $tax_query_input['exclude'] ) && is_array( $tax_query_input['exclude'] ) 2826 ? $tax_query_input['exclude'] 2827 : array(); 2828 $include_terms = isset( $tax_query_input['include'] ) && is_array( $tax_query_input['include'] ) 2829 ? $tax_query_input['include'] 2830 : array(); 2831 2832 $tax_query = array_merge( 2833 $build_conditions( $include_terms ), 2834 $build_conditions( $exclude_terms, 'NOT IN' ) 2835 ); 2836 } 2837 2838 if ( ! empty( $tax_query ) ) { 2839 // Merge with any existing `tax_query` conditions. 2840 $query['tax_query'] = array_merge( $query['tax_query'], $tax_query ); 2841 } 2842 } 2843 if ( ! empty( $block->context['query']['format'] ) && is_array( $block->context['query']['format'] ) ) { 2844 $formats = $block->context['query']['format']; 2845 /* 2846 * Validate that the format is either `standard` or a supported post format. 2847 * - First, add `standard` to the array of valid formats. 2848 * - Then, remove any invalid formats. 2849 */ 2850 $valid_formats = array_merge( array( 'standard' ), get_post_format_slugs() ); 2851 $formats = array_intersect( $formats, $valid_formats ); 2852 2853 /* 2854 * The relation needs to be set to `OR` since the request can contain 2855 * two separate conditions. The user may be querying for items that have 2856 * either the `standard` format or a specific format. 2857 */ 2858 $formats_query = array( 'relation' => 'OR' ); 2859 2860 /* 2861 * The default post format, `standard`, is not stored in the database. 2862 * If `standard` is part of the request, the query needs to exclude all post items that 2863 * have a format assigned. 2864 */ 2865 if ( in_array( 'standard', $formats, true ) ) { 2866 $formats_query[] = array( 2867 'taxonomy' => 'post_format', 2868 'field' => 'slug', 2869 'operator' => 'NOT EXISTS', 2870 ); 2871 // Remove the `standard` format, since it cannot be queried. 2872 unset( $formats[ array_search( 'standard', $formats, true ) ] ); 2873 } 2874 // Add any remaining formats to the formats query. 2875 if ( ! empty( $formats ) ) { 2876 // Add the `post-format-` prefix. 2877 $terms = array_map( 2878 static function ( $format ) { 2879 return "post-format-$format"; 2880 }, 2881 $formats 2882 ); 2883 $formats_query[] = array( 2884 'taxonomy' => 'post_format', 2885 'field' => 'slug', 2886 'terms' => $terms, 2887 'operator' => 'IN', 2888 ); 2889 } 2890 2891 /* 2892 * Add `$formats_query` to `$query`, as long as it contains more than one key: 2893 * If `$formats_query` only contains the initial `relation` key, there are no valid formats to query, 2894 * and the query should not be modified. 2895 */ 2896 if ( count( $formats_query ) > 1 ) { 2897 // Enable filtering by both post formats and other taxonomies by combining them with `AND`. 2898 if ( empty( $query['tax_query'] ) ) { 2899 $query['tax_query'] = $formats_query; 2900 } else { 2901 $query['tax_query'] = array( 2902 'relation' => 'AND', 2903 $query['tax_query'], 2904 $formats_query, 2905 ); 2906 } 2907 } 2908 } 2909 2910 if ( 2911 isset( $block->context['query']['order'] ) && 2912 in_array( strtoupper( $block->context['query']['order'] ), array( 'ASC', 'DESC' ), true ) 2913 ) { 2914 $query['order'] = strtoupper( $block->context['query']['order'] ); 2915 } 2916 if ( isset( $block->context['query']['orderBy'] ) ) { 2917 $query['orderby'] = $block->context['query']['orderBy']; 2918 } 2919 if ( 2920 isset( $block->context['query']['author'] ) 2921 ) { 2922 if ( is_array( $block->context['query']['author'] ) ) { 2923 $query['author__in'] = array_filter( array_map( 'intval', $block->context['query']['author'] ) ); 2924 } elseif ( is_string( $block->context['query']['author'] ) ) { 2925 $query['author__in'] = array_filter( array_map( 'intval', explode( ',', $block->context['query']['author'] ) ) ); 2926 } elseif ( is_int( $block->context['query']['author'] ) && $block->context['query']['author'] > 0 ) { 2927 $query['author'] = $block->context['query']['author']; 2928 } 2929 } 2930 if ( ! empty( $block->context['query']['search'] ) ) { 2931 $query['s'] = $block->context['query']['search']; 2932 } 2933 if ( ! empty( $block->context['query']['parents'] ) && is_post_type_hierarchical( $query['post_type'] ) ) { 2934 $query['post_parent__in'] = array_unique( array_map( 'intval', $block->context['query']['parents'] ) ); 2935 } 2936 } 2937 2938 /** 2939 * Filters the arguments which will be passed to `WP_Query` for the Query Loop Block. 2940 * 2941 * Anything to this filter should be compatible with the `WP_Query` API to form 2942 * the query context which will be passed down to the Query Loop Block's children. 2943 * This can help, for example, to include additional settings or meta queries not 2944 * directly supported by the core Query Loop Block, and extend its capabilities. 2945 * 2946 * Please note that this will only influence the query that will be rendered on the 2947 * front-end. The editor preview is not affected by this filter. Also, worth noting 2948 * that the editor preview uses the REST API, so, ideally, one should aim to provide 2949 * attributes which are also compatible with the REST API, in order to be able to 2950 * implement identical queries on both sides. 2951 * 2952 * @since 6.1.0 2953 * 2954 * @param array $query Array containing parameters for `WP_Query` as parsed by the block context. 2955 * @param WP_Block $block Block instance. 2956 * @param int $page Current query's page. 2957 */ 2958 return apply_filters( 'query_loop_block_query_vars', $query, $block, $page ); 2959 } 2960 2961 /** 2962 * Helper function that returns the proper pagination arrow HTML for 2963 * `QueryPaginationNext` and `QueryPaginationPrevious` blocks based 2964 * on the provided `paginationArrow` from `QueryPagination` context. 2965 * 2966 * It's used in QueryPaginationNext and QueryPaginationPrevious blocks. 2967 * 2968 * @since 5.9.0 2969 * 2970 * @param WP_Block $block Block instance. 2971 * @param bool $is_next Flag for handling `next/previous` blocks. 2972 * @return string|null The pagination arrow HTML or null if there is none. 2973 */ 2974 function get_query_pagination_arrow( $block, $is_next ) { 2975 $arrow_map = array( 2976 'none' => '', 2977 'arrow' => array( 2978 'next' => '→', 2979 'previous' => '←', 2980 ), 2981 'chevron' => array( 2982 'next' => '»', 2983 'previous' => '«', 2984 ), 2985 ); 2986 if ( ! empty( $block->context['paginationArrow'] ) && array_key_exists( $block->context['paginationArrow'], $arrow_map ) && ! empty( $arrow_map[ $block->context['paginationArrow'] ] ) ) { 2987 $pagination_type = $is_next ? 'next' : 'previous'; 2988 $arrow_attribute = $block->context['paginationArrow']; 2989 $arrow = $arrow_map[ $block->context['paginationArrow'] ][ $pagination_type ]; 2990 $arrow_classes = "wp-block-query-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; 2991 return "<span class='$arrow_classes' aria-hidden='true'>$arrow</span>"; 2992 } 2993 return null; 2994 } 2995 2996 /** 2997 * Helper function that constructs a comment query vars array from the passed 2998 * block properties. 2999 * 3000 * It's used with the Comment Query Loop inner blocks. 3001 * 3002 * @since 6.0.0 3003 * 3004 * @param WP_Block $block Block instance. 3005 * @return array Returns the comment query parameters to use with the 3006 * WP_Comment_Query constructor. 3007 */ 3008 function build_comment_query_vars_from_block( $block ) { 3009 3010 $comment_args = array( 3011 'orderby' => 'comment_date_gmt', 3012 'order' => 'ASC', 3013 'status' => 'approve', 3014 'no_found_rows' => false, 3015 ); 3016 3017 if ( is_user_logged_in() ) { 3018 $comment_args['include_unapproved'] = array( get_current_user_id() ); 3019 } else { 3020 $unapproved_email = wp_get_unapproved_comment_author_email(); 3021 3022 if ( $unapproved_email ) { 3023 $comment_args['include_unapproved'] = array( $unapproved_email ); 3024 } 3025 } 3026 3027 if ( ! empty( $block->context['postId'] ) ) { 3028 $comment_args['post_id'] = (int) $block->context['postId']; 3029 } 3030 3031 if ( get_option( 'thread_comments' ) ) { 3032 $comment_args['hierarchical'] = 'threaded'; 3033 } else { 3034 $comment_args['hierarchical'] = false; 3035 } 3036 3037 if ( get_option( 'page_comments' ) === '1' || get_option( 'page_comments' ) === true ) { 3038 $per_page = get_option( 'comments_per_page' ); 3039 $default_page = get_option( 'default_comments_page' ); 3040 if ( $per_page > 0 ) { 3041 $comment_args['number'] = $per_page; 3042 3043 $page = (int) get_query_var( 'cpage' ); 3044 if ( $page ) { 3045 $comment_args['paged'] = $page; 3046 } elseif ( 'oldest' === $default_page ) { 3047 $comment_args['paged'] = 1; 3048 } elseif ( 'newest' === $default_page ) { 3049 $max_num_pages = (int) ( new WP_Comment_Query( $comment_args ) )->max_num_pages; 3050 if ( 0 !== $max_num_pages ) { 3051 $comment_args['paged'] = $max_num_pages; 3052 } 3053 } 3054 } 3055 } 3056 3057 return $comment_args; 3058 } 3059 3060 /** 3061 * Helper function that returns the proper pagination arrow HTML for 3062 * `CommentsPaginationNext` and `CommentsPaginationPrevious` blocks based on the 3063 * provided `paginationArrow` from `CommentsPagination` context. 3064 * 3065 * It's used in CommentsPaginationNext and CommentsPaginationPrevious blocks. 3066 * 3067 * @since 6.0.0 3068 * 3069 * @param WP_Block $block Block instance. 3070 * @param string $pagination_type Optional. Type of the arrow we will be rendering. 3071 * Accepts 'next' or 'previous'. Default 'next'. 3072 * @return string|null The pagination arrow HTML or null if there is none. 3073 */ 3074 function get_comments_pagination_arrow( $block, $pagination_type = 'next' ) { 3075 $arrow_map = array( 3076 'none' => '', 3077 'arrow' => array( 3078 'next' => '→', 3079 'previous' => '←', 3080 ), 3081 'chevron' => array( 3082 'next' => '»', 3083 'previous' => '«', 3084 ), 3085 ); 3086 if ( ! empty( $block->context['comments/paginationArrow'] ) && ! empty( $arrow_map[ $block->context['comments/paginationArrow'] ][ $pagination_type ] ) ) { 3087 $arrow_attribute = $block->context['comments/paginationArrow']; 3088 $arrow = $arrow_map[ $block->context['comments/paginationArrow'] ][ $pagination_type ]; 3089 $arrow_classes = "wp-block-comments-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; 3090 return "<span class='$arrow_classes' aria-hidden='true'>$arrow</span>"; 3091 } 3092 return null; 3093 } 3094 3095 /** 3096 * Strips all HTML from the content of footnotes, and sanitizes the ID. 3097 * 3098 * This function expects slashed data on the footnotes content. 3099 * 3100 * @access private 3101 * @since 6.3.2 3102 * 3103 * @param string $footnotes JSON-encoded string of an array containing the content and ID of each footnote. 3104 * @return string Filtered content without any HTML on the footnote content and with the sanitized ID. 3105 */ 3106 function _wp_filter_post_meta_footnotes( $footnotes ) { 3107 $footnotes_decoded = json_decode( $footnotes, true ); 3108 if ( ! is_array( $footnotes_decoded ) ) { 3109 return ''; 3110 } 3111 $footnotes_sanitized = array(); 3112 foreach ( $footnotes_decoded as $footnote ) { 3113 if ( ! empty( $footnote['content'] ) && ! empty( $footnote['id'] ) ) { 3114 $footnotes_sanitized[] = array( 3115 'id' => sanitize_key( $footnote['id'] ), 3116 'content' => wp_unslash( wp_filter_post_kses( wp_slash( $footnote['content'] ) ) ), 3117 ); 3118 } 3119 } 3120 return wp_json_encode( $footnotes_sanitized ); 3121 } 3122 3123 /** 3124 * Adds the filters for footnotes meta field. 3125 * 3126 * @access private 3127 * @since 6.3.2 3128 */ 3129 function _wp_footnotes_kses_init_filters() { 3130 add_filter( 'sanitize_post_meta_footnotes', '_wp_filter_post_meta_footnotes' ); 3131 } 3132 3133 /** 3134 * Removes the filters for footnotes meta field. 3135 * 3136 * @access private 3137 * @since 6.3.2 3138 */ 3139 function _wp_footnotes_remove_filters() { 3140 remove_filter( 'sanitize_post_meta_footnotes', '_wp_filter_post_meta_footnotes' ); 3141 } 3142 3143 /** 3144 * Registers the filter of footnotes meta field if the user does not have `unfiltered_html` capability. 3145 * 3146 * @access private 3147 * @since 6.3.2 3148 */ 3149 function _wp_footnotes_kses_init() { 3150 _wp_footnotes_remove_filters(); 3151 if ( ! current_user_can( 'unfiltered_html' ) ) { 3152 _wp_footnotes_kses_init_filters(); 3153 } 3154 } 3155 3156 /** 3157 * Initializes the filters for footnotes meta field when imported data should be filtered. 3158 * 3159 * This filter is the last one being executed on {@see 'force_filtered_html_on_import'}. 3160 * If the input of the filter is true, it means we are in an import situation and should 3161 * enable kses, independently of the user capabilities. So in that case we call 3162 * _wp_footnotes_kses_init_filters(). 3163 * 3164 * @access private 3165 * @since 6.3.2 3166 * 3167 * @param string $arg Input argument of the filter. 3168 * @return string Input argument of the filter. 3169 */ 3170 function _wp_footnotes_force_filtered_html_on_import_filter( $arg ) { 3171 // If `force_filtered_html_on_import` is true, we need to init the global styles kses filters. 3172 if ( $arg ) { 3173 _wp_footnotes_kses_init_filters(); 3174 } 3175 return $arg; 3176 } 3177 3178 /** 3179 * Exposes blocks with autoRegister flag for ServerSideRender in the editor. 3180 * 3181 * Detects blocks that have the autoRegister flag set in their supports 3182 * and passes them to JavaScript for auto-registration with ServerSideRender. 3183 * 3184 * @access private 3185 * @since 7.0.0 3186 */ 3187 function _wp_enqueue_auto_register_blocks() { 3188 $auto_register_blocks = array(); 3189 $registered_blocks = WP_Block_Type_Registry::get_instance()->get_all_registered(); 3190 3191 foreach ( $registered_blocks as $block_name => $block_type ) { 3192 if ( ! empty( $block_type->supports['autoRegister'] ) && ! empty( $block_type->render_callback ) ) { 3193 $auto_register_blocks[] = $block_name; 3194 } 3195 } 3196 3197 if ( ! empty( $auto_register_blocks ) ) { 3198 wp_add_inline_script( 3199 'wp-block-library', 3200 sprintf( 'window.__unstableAutoRegisterBlocks = %s;', wp_json_encode( $auto_register_blocks ) ), 3201 'before' 3202 ); 3203 } 3204 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Jul 4 08:20:12 2026 | Cross-referenced by PHPXref |