| [ 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 'innerContent' => array(), 975 ); 976 977 /** 978 * Filters the parsed block array for a given hooked block. 979 * 980 * @since 6.5.0 981 * 982 * @param array|null $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block. 983 * @param string $hooked_block_type The hooked block type name. 984 * @param string $relative_position The relative position of the hooked block. 985 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 986 * @param WP_Block_Template|WP_Post|array $context The block template, template part, post object, 987 * or pattern that the anchor block belongs to. 988 */ 989 $parsed_hooked_block = apply_filters( 'hooked_block', $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 990 991 /** 992 * Filters the parsed block array for a given hooked block. 993 * 994 * The dynamic portion of the hook name, `$hooked_block_type`, refers to the block type name of the specific hooked block. 995 * 996 * @since 6.5.0 997 * 998 * @param array|null $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block. 999 * @param string $hooked_block_type The hooked block type name. 1000 * @param string $relative_position The relative position of the hooked block. 1001 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 1002 * @param WP_Block_Template|WP_Post|array $context The block template, template part, post object, 1003 * or pattern that the anchor block belongs to. 1004 */ 1005 $parsed_hooked_block = apply_filters( "hooked_block_{$hooked_block_type}", $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 1006 1007 if ( null === $parsed_hooked_block ) { 1008 continue; 1009 } 1010 1011 // It's possible that the filter returned a block of a different type, so we explicitly 1012 // look for the original `$hooked_block_type` in the `ignoredHookedBlocks` metadata. 1013 if ( 1014 ! isset( $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] ) || 1015 ! in_array( $hooked_block_type, $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'], true ) 1016 ) { 1017 $markup .= serialize_block( $parsed_hooked_block ); 1018 } 1019 } 1020 1021 return $markup; 1022 } 1023 1024 /** 1025 * Adds a list of hooked block types to an anchor block's ignored hooked block types. 1026 * 1027 * This function is meant for internal use only. 1028 * 1029 * @since 6.5.0 1030 * @access private 1031 * 1032 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 1033 * @param string $relative_position The relative position of the hooked blocks. 1034 * Can be one of 'before', 'after', 'first_child', or 'last_child'. 1035 * @param array $hooked_blocks An array of hooked block types, grouped by anchor block and relative position. 1036 * @param WP_Block_Template|WP_Post|array $context The block template, template part, or pattern that the anchor block belongs to. 1037 * @return string Empty string. 1038 */ 1039 function set_ignored_hooked_blocks_metadata( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) { 1040 $anchor_block_type = $parsed_anchor_block['blockName']; 1041 $hooked_block_types = isset( $anchor_block_type, $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) 1042 ? $hooked_blocks[ $anchor_block_type ][ $relative_position ] 1043 : array(); 1044 1045 /** This filter is documented in wp-includes/blocks.php */ 1046 $hooked_block_types = apply_filters( 'hooked_block_types', $hooked_block_types, $relative_position, $anchor_block_type, $context ); 1047 if ( empty( $hooked_block_types ) ) { 1048 return ''; 1049 } 1050 1051 foreach ( $hooked_block_types as $index => $hooked_block_type ) { 1052 $parsed_hooked_block = array( 1053 'blockName' => $hooked_block_type, 1054 'attrs' => array(), 1055 'innerBlocks' => array(), 1056 'innerContent' => array(), 1057 ); 1058 1059 /** This filter is documented in wp-includes/blocks.php */ 1060 $parsed_hooked_block = apply_filters( 'hooked_block', $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 1061 1062 /** This filter is documented in wp-includes/blocks.php */ 1063 $parsed_hooked_block = apply_filters( "hooked_block_{$hooked_block_type}", $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 1064 1065 if ( null === $parsed_hooked_block ) { 1066 unset( $hooked_block_types[ $index ] ); 1067 } 1068 } 1069 1070 $previously_ignored_hooked_blocks = $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] ?? array(); 1071 1072 $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] = array_unique( 1073 array_merge( 1074 $previously_ignored_hooked_blocks, 1075 $hooked_block_types 1076 ) 1077 ); 1078 1079 // Markup for the hooked blocks has already been created (in `insert_hooked_blocks`). 1080 return ''; 1081 } 1082 1083 /** 1084 * Runs the hooked blocks algorithm on the given content. 1085 * 1086 * @since 6.6.0 1087 * @since 6.7.0 Injects the `theme` attribute into Template Part blocks, even if no hooked blocks are registered. 1088 * @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. 1089 * @access private 1090 * 1091 * @param string $content Serialized content. 1092 * @param WP_Block_Template|WP_Post|array|null $context A block template, template part, post object, or pattern 1093 * that the blocks belong to. If set to `null`, `get_post()` 1094 * will be called to use the current post as context. 1095 * Default: `null`. 1096 * @param callable $callback A function that will be called for each block to generate 1097 * the markup for a given list of blocks that are hooked to it. 1098 * Default: 'insert_hooked_blocks'. 1099 * @return string The serialized markup. 1100 */ 1101 function apply_block_hooks_to_content( $content, $context = null, $callback = 'insert_hooked_blocks' ) { 1102 // Default to the current post if no context is provided. 1103 if ( null === $context ) { 1104 $context = get_post(); 1105 } 1106 1107 $hooked_blocks = get_hooked_blocks(); 1108 1109 $before_block_visitor = '_inject_theme_attribute_in_template_part_block'; 1110 $after_block_visitor = null; 1111 if ( ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ) ) { 1112 $before_block_visitor = make_before_block_visitor( $hooked_blocks, $context, $callback ); 1113 $after_block_visitor = make_after_block_visitor( $hooked_blocks, $context, $callback ); 1114 } 1115 1116 $block_allows_multiple_instances = array(); 1117 /* 1118 * Remove hooked blocks from `$hooked_block_types` if they have `multiple` set to false and 1119 * are already present in `$content`. 1120 */ 1121 foreach ( $hooked_blocks as $anchor_block_type => $relative_positions ) { 1122 foreach ( $relative_positions as $relative_position => $hooked_block_types ) { 1123 foreach ( $hooked_block_types as $index => $hooked_block_type ) { 1124 $hooked_block_type_definition = 1125 WP_Block_Type_Registry::get_instance()->get_registered( $hooked_block_type ); 1126 1127 $block_allows_multiple_instances[ $hooked_block_type ] = 1128 block_has_support( $hooked_block_type_definition, 'multiple', true ); 1129 1130 if ( 1131 ! $block_allows_multiple_instances[ $hooked_block_type ] && 1132 has_block( $hooked_block_type, $content ) 1133 ) { 1134 unset( $hooked_blocks[ $anchor_block_type ][ $relative_position ][ $index ] ); 1135 } 1136 } 1137 if ( empty( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) ) { 1138 unset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ); 1139 } 1140 } 1141 if ( empty( $hooked_blocks[ $anchor_block_type ] ) ) { 1142 unset( $hooked_blocks[ $anchor_block_type ] ); 1143 } 1144 } 1145 1146 /* 1147 * We also need to cover the case where the hooked block is not present in 1148 * `$content` at first and we're allowed to insert it once -- but not again. 1149 */ 1150 $suppress_single_instance_blocks = static function ( $hooked_block_types ) use ( &$block_allows_multiple_instances, $content ) { 1151 static $single_instance_blocks_present_in_content = array(); 1152 foreach ( $hooked_block_types as $index => $hooked_block_type ) { 1153 if ( ! isset( $block_allows_multiple_instances[ $hooked_block_type ] ) ) { 1154 $hooked_block_type_definition = 1155 WP_Block_Type_Registry::get_instance()->get_registered( $hooked_block_type ); 1156 1157 $block_allows_multiple_instances[ $hooked_block_type ] = 1158 block_has_support( $hooked_block_type_definition, 'multiple', true ); 1159 } 1160 1161 if ( $block_allows_multiple_instances[ $hooked_block_type ] ) { 1162 continue; 1163 } 1164 1165 // The block doesn't allow multiple instances, so we need to check if it's already present. 1166 if ( 1167 in_array( $hooked_block_type, $single_instance_blocks_present_in_content, true ) || 1168 has_block( $hooked_block_type, $content ) 1169 ) { 1170 unset( $hooked_block_types[ $index ] ); 1171 } else { 1172 // We can insert the block once, but need to remember not to insert it again. 1173 $single_instance_blocks_present_in_content[] = $hooked_block_type; 1174 } 1175 } 1176 return $hooked_block_types; 1177 }; 1178 add_filter( 'hooked_block_types', $suppress_single_instance_blocks, PHP_INT_MAX ); 1179 $content = traverse_and_serialize_blocks( 1180 parse_blocks( $content ), 1181 $before_block_visitor, 1182 $after_block_visitor 1183 ); 1184 remove_filter( 'hooked_block_types', $suppress_single_instance_blocks, PHP_INT_MAX ); 1185 1186 return $content; 1187 } 1188 1189 /** 1190 * Run the Block Hooks algorithm on a post object's content. 1191 * 1192 * This function is different from `apply_block_hooks_to_content` in that 1193 * it takes ignored hooked block information from the post's metadata into 1194 * account. This ensures that any blocks hooked as first or last child 1195 * of the block that corresponds to the post type are handled correctly. 1196 * 1197 * @since 6.8.0 1198 * @access private 1199 * 1200 * @param string $content Serialized content. 1201 * @param WP_Post|null $post A post object that the content belongs to. If set to `null`, 1202 * `get_post()` will be called to use the current post as context. 1203 * Default: `null`. 1204 * @param callable $callback A function that will be called for each block to generate 1205 * the markup for a given list of blocks that are hooked to it. 1206 * Default: 'insert_hooked_blocks'. 1207 * @return string The serialized markup. 1208 */ 1209 function apply_block_hooks_to_content_from_post_object( $content, $post = null, $callback = 'insert_hooked_blocks' ) { 1210 // Default to the current post if no context is provided. 1211 if ( null === $post ) { 1212 $post = get_post(); 1213 } 1214 1215 if ( ! $post instanceof WP_Post ) { 1216 return apply_block_hooks_to_content( $content, $post, $callback ); 1217 } 1218 1219 /* 1220 * If the content was created using the classic editor or using a single Classic block 1221 * (`core/freeform`), it might not contain any block markup at all. 1222 * However, we still might need to inject hooked blocks in the first child or last child 1223 * positions of the parent block. To be able to apply the Block Hooks algorithm, we wrap 1224 * the content in a `core/freeform` wrapper block. 1225 */ 1226 if ( ! has_blocks( $content ) ) { 1227 $original_content = $content; 1228 1229 $content_wrapped_in_classic_block = get_comment_delimited_block_content( 1230 'core/freeform', 1231 array(), 1232 $content 1233 ); 1234 1235 $content = $content_wrapped_in_classic_block; 1236 } 1237 1238 $attributes = array(); 1239 1240 // If context is a post object, `ignoredHookedBlocks` information is stored in its post meta. 1241 $ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); 1242 if ( ! empty( $ignored_hooked_blocks ) ) { 1243 $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true ); 1244 $attributes['metadata'] = array( 1245 'ignoredHookedBlocks' => $ignored_hooked_blocks, 1246 ); 1247 } 1248 1249 /* 1250 * We need to wrap the content in a temporary wrapper block with that metadata 1251 * so the Block Hooks algorithm can insert blocks that are hooked as first or last child 1252 * of the wrapper block. 1253 * To that end, we need to determine the wrapper block type based on the post type. 1254 */ 1255 if ( 'wp_navigation' === $post->post_type ) { 1256 $wrapper_block_type = 'core/navigation'; 1257 } elseif ( 'wp_block' === $post->post_type ) { 1258 $wrapper_block_type = 'core/block'; 1259 } else { 1260 $wrapper_block_type = 'core/post-content'; 1261 } 1262 1263 $content = get_comment_delimited_block_content( 1264 $wrapper_block_type, 1265 $attributes, 1266 $content 1267 ); 1268 1269 /* 1270 * We need to avoid inserting any blocks hooked into the `before` and `after` positions 1271 * of the temporary wrapper block that we create to wrap the content. 1272 * See https://core.trac.wordpress.org/ticket/63287 for more details. 1273 */ 1274 $suppress_blocks_from_insertion_before_and_after_wrapper_block = static function ( $hooked_block_types, $relative_position, $anchor_block_type ) use ( $wrapper_block_type ) { 1275 if ( 1276 $wrapper_block_type === $anchor_block_type && 1277 in_array( $relative_position, array( 'before', 'after' ), true ) 1278 ) { 1279 return array(); 1280 } 1281 return $hooked_block_types; 1282 }; 1283 1284 // Apply Block Hooks. 1285 add_filter( 'hooked_block_types', $suppress_blocks_from_insertion_before_and_after_wrapper_block, PHP_INT_MAX, 3 ); 1286 $content = apply_block_hooks_to_content( $content, $post, $callback ); 1287 remove_filter( 'hooked_block_types', $suppress_blocks_from_insertion_before_and_after_wrapper_block, PHP_INT_MAX ); 1288 1289 // Finally, we need to remove the temporary wrapper block. 1290 $content = remove_serialized_parent_block( $content ); 1291 1292 // If we wrapped the content in a `core/freeform` block, we also need to remove that. 1293 if ( ! empty( $content_wrapped_in_classic_block ) ) { 1294 /* 1295 * We cannot simply use remove_serialized_parent_block() here, 1296 * as that function assumes that the block wrapper is at the top level. 1297 * However, there might now be a hooked block inserted next to it 1298 * (as first or last child of the parent). 1299 */ 1300 $content = str_replace( $content_wrapped_in_classic_block, $original_content, $content ); 1301 } 1302 1303 return $content; 1304 } 1305 1306 /** 1307 * Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the inner blocks. 1308 * 1309 * @since 6.6.0 1310 * @access private 1311 * 1312 * @param string $serialized_block The serialized markup of a block and its inner blocks. 1313 * @return string The serialized markup of the inner blocks. 1314 */ 1315 function remove_serialized_parent_block( $serialized_block ) { 1316 $start = strpos( $serialized_block, '-->' ) + strlen( '-->' ); 1317 $end = strrpos( $serialized_block, '<!--' ); 1318 return substr( $serialized_block, $start, $end - $start ); 1319 } 1320 1321 /** 1322 * Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the wrapper block. 1323 * 1324 * @since 6.7.0 1325 * @access private 1326 * 1327 * @see remove_serialized_parent_block() 1328 * 1329 * @param string $serialized_block The serialized markup of a block and its inner blocks. 1330 * @return string The serialized markup of the wrapper block. 1331 */ 1332 function extract_serialized_parent_block( $serialized_block ) { 1333 $start = strpos( $serialized_block, '-->' ) + strlen( '-->' ); 1334 $end = strrpos( $serialized_block, '<!--' ); 1335 return substr( $serialized_block, 0, $start ) . substr( $serialized_block, $end ); 1336 } 1337 1338 /** 1339 * Updates the wp_postmeta with the list of ignored hooked blocks 1340 * where the inner blocks are stored as post content. 1341 * 1342 * @since 6.6.0 1343 * @since 6.8.0 Support non-`wp_navigation` post types. 1344 * @access private 1345 * 1346 * @param stdClass $post Post object. 1347 * @return stdClass The updated post object. 1348 */ 1349 function update_ignored_hooked_blocks_postmeta( $post ) { 1350 /* 1351 * In this scenario the user has likely tried to create a new post object via the REST API. 1352 * In which case we won't have a post ID to work with and store meta against. 1353 */ 1354 if ( empty( $post->ID ) ) { 1355 return $post; 1356 } 1357 1358 /* 1359 * Skip meta generation when consumers intentionally update specific fields 1360 * and omit the content update. 1361 */ 1362 if ( ! isset( $post->post_content ) ) { 1363 return $post; 1364 } 1365 1366 /* 1367 * Skip meta generation if post type is not set. 1368 */ 1369 if ( ! isset( $post->post_type ) ) { 1370 return $post; 1371 } 1372 1373 $attributes = array(); 1374 1375 $ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); 1376 if ( ! empty( $ignored_hooked_blocks ) ) { 1377 $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true ); 1378 $attributes['metadata'] = array( 1379 'ignoredHookedBlocks' => $ignored_hooked_blocks, 1380 ); 1381 } 1382 1383 if ( 'wp_navigation' === $post->post_type ) { 1384 $wrapper_block_type = 'core/navigation'; 1385 } elseif ( 'wp_block' === $post->post_type ) { 1386 $wrapper_block_type = 'core/block'; 1387 } else { 1388 $wrapper_block_type = 'core/post-content'; 1389 } 1390 1391 $markup = get_comment_delimited_block_content( 1392 $wrapper_block_type, 1393 $attributes, 1394 $post->post_content 1395 ); 1396 1397 $existing_post = get_post( $post->ID ); 1398 // Merge the existing post object with the updated post object to pass to the block hooks algorithm for context. 1399 $context = (object) array_merge( (array) $existing_post, (array) $post ); 1400 $context = new WP_Post( $context ); // Convert to WP_Post object. 1401 $serialized_block = apply_block_hooks_to_content( $markup, $context, 'set_ignored_hooked_blocks_metadata' ); 1402 $root_block = parse_blocks( $serialized_block )[0]; 1403 1404 $ignored_hooked_blocks = $root_block['attrs']['metadata']['ignoredHookedBlocks'] ?? array(); 1405 1406 if ( ! empty( $ignored_hooked_blocks ) ) { 1407 $existing_ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); 1408 if ( ! empty( $existing_ignored_hooked_blocks ) ) { 1409 $existing_ignored_hooked_blocks = json_decode( $existing_ignored_hooked_blocks, true ); 1410 $ignored_hooked_blocks = array_unique( array_merge( $ignored_hooked_blocks, $existing_ignored_hooked_blocks ) ); 1411 } 1412 1413 if ( ! isset( $post->meta_input ) ) { 1414 $post->meta_input = array(); 1415 } 1416 $post->meta_input['_wp_ignored_hooked_blocks'] = json_encode( $ignored_hooked_blocks ); 1417 } 1418 1419 $post->post_content = remove_serialized_parent_block( $serialized_block ); 1420 return $post; 1421 } 1422 1423 /** 1424 * Returns the markup for blocks hooked to the given anchor block in a specific relative position and then 1425 * adds a list of hooked block types to an anchor block's ignored hooked block types. 1426 * 1427 * This function is meant for internal use only. 1428 * 1429 * @since 6.6.0 1430 * @access private 1431 * 1432 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 1433 * @param string $relative_position The relative position of the hooked blocks. 1434 * Can be one of 'before', 'after', 'first_child', or 'last_child'. 1435 * @param array $hooked_blocks An array of hooked block types, grouped by anchor block and relative position. 1436 * @param WP_Block_Template|WP_Post|array $context The block template, template part, or pattern that the anchor block belongs to. 1437 * @return string 1438 */ 1439 function insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) { 1440 $markup = insert_hooked_blocks( $parsed_anchor_block, $relative_position, $hooked_blocks, $context ); 1441 $markup .= set_ignored_hooked_blocks_metadata( $parsed_anchor_block, $relative_position, $hooked_blocks, $context ); 1442 1443 return $markup; 1444 } 1445 1446 /** 1447 * Hooks into the REST API response for the Posts endpoint and adds the first and last inner blocks. 1448 * 1449 * @since 6.6.0 1450 * @since 6.8.0 Support non-`wp_navigation` post types. 1451 * 1452 * @param WP_REST_Response $response The response object. 1453 * @param WP_Post $post Post object. 1454 * @return WP_REST_Response The response object. 1455 */ 1456 function insert_hooked_blocks_into_rest_response( $response, $post ) { 1457 if ( empty( $response->data['content']['raw'] ) ) { 1458 return $response; 1459 } 1460 1461 $response->data['content']['raw'] = apply_block_hooks_to_content_from_post_object( 1462 $response->data['content']['raw'], 1463 $post, 1464 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata' 1465 ); 1466 1467 // If the rendered content was previously empty, we leave it like that. 1468 if ( empty( $response->data['content']['rendered'] ) ) { 1469 return $response; 1470 } 1471 1472 // `apply_block_hooks_to_content` is called above. Ensure it is not called again as a filter. 1473 $priority = has_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object' ); 1474 if ( false !== $priority ) { 1475 remove_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', $priority ); 1476 } 1477 1478 /** This filter is documented in wp-includes/post-template.php */ 1479 $response->data['content']['rendered'] = apply_filters( 1480 'the_content', 1481 $response->data['content']['raw'] 1482 ); 1483 1484 // Restore the filter if it was set initially. 1485 if ( false !== $priority ) { 1486 add_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', $priority ); 1487 } 1488 1489 return $response; 1490 } 1491 1492 /** 1493 * Returns a function that injects the theme attribute into, and hooked blocks before, a given block. 1494 * 1495 * The returned function can be used as `$pre_callback` argument to `traverse_and_serialize_block(s)`, 1496 * where it will inject the `theme` attribute into all Template Part blocks, and prepend the markup for 1497 * any blocks hooked `before` the given block and as its parent's `first_child`, respectively. 1498 * 1499 * This function is meant for internal use only. 1500 * 1501 * @since 6.4.0 1502 * @since 6.5.0 Added $callback argument. 1503 * @access private 1504 * 1505 * @param array $hooked_blocks An array of blocks hooked to another given block. 1506 * @param WP_Block_Template|WP_Post|array $context A block template, template part, post object, 1507 * or pattern that the blocks belong to. 1508 * @param callable $callback A function that will be called for each block to generate 1509 * the markup for a given list of blocks that are hooked to it. 1510 * Default: 'insert_hooked_blocks'. 1511 * @return callable A function that returns the serialized markup for the given block, 1512 * including the markup for any hooked blocks before it. 1513 */ 1514 function make_before_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) { 1515 /** 1516 * Injects hooked blocks before the given block, injects the `theme` attribute into Template Part blocks, and returns the serialized markup. 1517 * 1518 * If the current block is a Template Part block, inject the `theme` attribute. 1519 * Furthermore, prepend the markup for any blocks hooked `before` the given block and as its parent's 1520 * `first_child`, respectively, to the serialized markup for the given block. 1521 * 1522 * @param array $block The block to inject the theme attribute into, and hooked blocks before. Passed by reference. 1523 * @param array $parent_block The parent block of the given block. Passed by reference. Default null. 1524 * @param array $prev The previous sibling block of the given block. Default null. 1525 * @return string The serialized markup for the given block, with the markup for any hooked blocks prepended to it. 1526 */ 1527 return function ( &$block, &$parent_block = null, $prev = null ) use ( $hooked_blocks, $context, $callback ) { 1528 _inject_theme_attribute_in_template_part_block( $block ); 1529 1530 $markup = ''; 1531 1532 if ( $parent_block && ! $prev ) { 1533 // Candidate for first-child insertion. 1534 $markup .= call_user_func_array( 1535 $callback, 1536 array( &$parent_block, 'first_child', $hooked_blocks, $context ) 1537 ); 1538 } 1539 1540 $markup .= call_user_func_array( 1541 $callback, 1542 array( &$block, 'before', $hooked_blocks, $context ) 1543 ); 1544 1545 return $markup; 1546 }; 1547 } 1548 1549 /** 1550 * Returns a function that injects the hooked blocks after a given block. 1551 * 1552 * The returned function can be used as `$post_callback` argument to `traverse_and_serialize_block(s)`, 1553 * where it will append the markup for any blocks hooked `after` the given block and as its parent's 1554 * `last_child`, respectively. 1555 * 1556 * This function is meant for internal use only. 1557 * 1558 * @since 6.4.0 1559 * @since 6.5.0 Added $callback argument. 1560 * @access private 1561 * 1562 * @param array $hooked_blocks An array of blocks hooked to another block. 1563 * @param WP_Block_Template|WP_Post|array $context A block template, template part, post object, 1564 * or pattern that the blocks belong to. 1565 * @param callable $callback A function that will be called for each block to generate 1566 * the markup for a given list of blocks that are hooked to it. 1567 * Default: 'insert_hooked_blocks'. 1568 * @return callable A function that returns the serialized markup for the given block, 1569 * including the markup for any hooked blocks after it. 1570 */ 1571 function make_after_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) { 1572 /** 1573 * Injects hooked blocks after the given block, and returns the serialized markup. 1574 * 1575 * Append the markup for any blocks hooked `after` the given block and as its parent's 1576 * `last_child`, respectively, to the serialized markup for the given block. 1577 * 1578 * @param array $block The block to inject the hooked blocks after. Passed by reference. 1579 * @param array $parent_block The parent block of the given block. Passed by reference. Default null. 1580 * @param array $next The next sibling block of the given block. Default null. 1581 * @return string The serialized markup for the given block, with the markup for any hooked blocks appended to it. 1582 */ 1583 return function ( &$block, &$parent_block = null, $next = null ) use ( $hooked_blocks, $context, $callback ) { 1584 $markup = call_user_func_array( 1585 $callback, 1586 array( &$block, 'after', $hooked_blocks, $context ) 1587 ); 1588 1589 if ( $parent_block && ! $next ) { 1590 // Candidate for last-child insertion. 1591 $markup .= call_user_func_array( 1592 $callback, 1593 array( &$parent_block, 'last_child', $hooked_blocks, $context ) 1594 ); 1595 } 1596 1597 return $markup; 1598 }; 1599 } 1600 1601 /** 1602 * Given an array of attributes, returns a string in the serialized attributes 1603 * format prepared for post content. 1604 * 1605 * The serialized result is a JSON-encoded string, with unicode escape sequence 1606 * substitution for characters which might otherwise interfere with embedding 1607 * the result in an HTML comment. 1608 * 1609 * This function must produce output that remains in sync with the output of 1610 * the serializeAttributes JavaScript function in the block editor in order 1611 * to ensure consistent operation between PHP and JavaScript. 1612 * 1613 * @since 5.3.1 1614 * 1615 * @param array $block_attributes Attributes object. 1616 * @return string Serialized attributes. 1617 */ 1618 function serialize_block_attributes( $block_attributes ) { 1619 $encoded_attributes = wp_json_encode( $block_attributes, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); 1620 1621 return strtr( 1622 $encoded_attributes, 1623 array( 1624 '\\\\' => '\\u005c', 1625 '--' => '\\u002d\\u002d', 1626 '<' => '\\u003c', 1627 '>' => '\\u003e', 1628 '&' => '\\u0026', 1629 '\\"' => '\\u0022', 1630 ) 1631 ); 1632 } 1633 1634 /** 1635 * Returns the block name to use for serialization. This will remove the default 1636 * "core/" namespace from a block name. 1637 * 1638 * @since 5.3.1 1639 * 1640 * @param string|null $block_name Optional. Original block name. Null if the block name is unknown, 1641 * e.g. Classic blocks have their name set to null. Default null. 1642 * @return string Block name to use for serialization. 1643 */ 1644 function strip_core_block_namespace( $block_name = null ) { 1645 if ( is_string( $block_name ) && str_starts_with( $block_name, 'core/' ) ) { 1646 return substr( $block_name, 5 ); 1647 } 1648 1649 return $block_name; 1650 } 1651 1652 /** 1653 * Returns the content of a block, including comment delimiters. 1654 * 1655 * @since 5.3.1 1656 * 1657 * @param string|null $block_name Block name. Null if the block name is unknown, 1658 * e.g. Classic blocks have their name set to null. 1659 * @param array $block_attributes Block attributes. 1660 * @param string $block_content Block save content. 1661 * @return string Comment-delimited block content. 1662 */ 1663 function get_comment_delimited_block_content( $block_name, $block_attributes, $block_content ) { 1664 if ( is_null( $block_name ) ) { 1665 return $block_content; 1666 } 1667 1668 $serialized_block_name = strip_core_block_namespace( $block_name ); 1669 $serialized_attributes = empty( $block_attributes ) ? '' : serialize_block_attributes( $block_attributes ) . ' '; 1670 1671 if ( empty( $block_content ) ) { 1672 return sprintf( '<!-- wp:%s %s/-->', $serialized_block_name, $serialized_attributes ); 1673 } 1674 1675 return sprintf( 1676 '<!-- wp:%s %s-->%s<!-- /wp:%s -->', 1677 $serialized_block_name, 1678 $serialized_attributes, 1679 $block_content, 1680 $serialized_block_name 1681 ); 1682 } 1683 1684 /** 1685 * Returns the content of a block, including comment delimiters, serializing all 1686 * attributes from the given parsed block. 1687 * 1688 * This should be used when preparing a block to be saved to post content. 1689 * Prefer `render_block` when preparing a block for display. Unlike 1690 * `render_block`, this does not evaluate a block's `render_callback`, and will 1691 * instead preserve the markup as parsed. 1692 * 1693 * @since 5.3.1 1694 * 1695 * @param array $block { 1696 * An associative array of a single parsed block object. See WP_Block_Parser_Block. 1697 * 1698 * @type string|null $blockName Name of block. 1699 * @type array $attrs Attributes from block comment delimiters. 1700 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1701 * have the same structure as this one. 1702 * @type string $innerHTML HTML from inside block comment delimiters. 1703 * @type array $innerContent List of string fragments and null markers where 1704 * inner blocks were found. 1705 * } 1706 * @return string String of rendered HTML. 1707 */ 1708 function serialize_block( $block ) { 1709 $block_content = ''; 1710 1711 $index = 0; 1712 foreach ( $block['innerContent'] as $chunk ) { 1713 $block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] ); 1714 } 1715 1716 if ( ! is_array( $block['attrs'] ) ) { 1717 $block['attrs'] = array(); 1718 } 1719 1720 return get_comment_delimited_block_content( 1721 $block['blockName'], 1722 $block['attrs'], 1723 $block_content 1724 ); 1725 } 1726 1727 /** 1728 * Returns a joined string of the aggregate serialization of the given 1729 * parsed blocks. 1730 * 1731 * @since 5.3.1 1732 * 1733 * @param array[] $blocks { 1734 * Array of block structures. 1735 * 1736 * @type array ...$0 { 1737 * An associative array of a single parsed block object. See WP_Block_Parser_Block. 1738 * 1739 * @type string|null $blockName Name of block. 1740 * @type array $attrs Attributes from block comment delimiters. 1741 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 1742 * have the same structure as this one. 1743 * @type string $innerHTML HTML from inside block comment delimiters. 1744 * @type array $innerContent List of string fragments and null markers where 1745 * inner blocks were found. 1746 * } 1747 * } 1748 * @return string String of rendered HTML. 1749 */ 1750 function serialize_blocks( $blocks ) { 1751 return implode( '', array_map( 'serialize_block', $blocks ) ); 1752 } 1753 1754 /** 1755 * Traverses a parsed block tree and applies callbacks before and after serializing it. 1756 * 1757 * Recursively traverses the block and its inner blocks and applies the two callbacks provided as 1758 * arguments, the first one before serializing the block, and the second one after serializing it. 1759 * If either callback returns a string value, it will be prepended and appended to the serialized 1760 * block markup, respectively. 1761 * 1762 * The callbacks will receive a reference to the current block as their first argument, so that they 1763 * can also modify it, and the current block's parent block as second argument. Finally, the 1764 * `$pre_callback` receives the previous block, whereas the `$post_callback` receives 1765 * the next block as third argument. 1766 * 1767 * Serialized blocks are returned including comment delimiters, and with all attributes serialized. 1768 * 1769 * This function should be used when there is a need to modify the saved block, or to inject markup 1770 * into the return value. Prefer `serialize_block` when preparing a block to be saved to post content. 1771 * 1772 * This function is meant for internal use only. 1773 * 1774 * @since 6.4.0 1775 * @access private 1776 * 1777 * @see serialize_block() 1778 * 1779 * @param array $block An associative array of a single parsed block object. See WP_Block_Parser_Block. 1780 * @param callable $pre_callback Callback to run on each block in the tree before it is traversed and serialized. 1781 * It is called with the following arguments: &$block, $parent_block, $previous_block. 1782 * Its string return value will be prepended to the serialized block markup. 1783 * @param callable $post_callback Callback to run on each block in the tree after it is traversed and serialized. 1784 * It is called with the following arguments: &$block, $parent_block, $next_block. 1785 * Its string return value will be appended to the serialized block markup. 1786 * @return string Serialized block markup. 1787 */ 1788 function traverse_and_serialize_block( $block, $pre_callback = null, $post_callback = null ) { 1789 $block_content = ''; 1790 $block_index = 0; 1791 1792 foreach ( $block['innerContent'] as $chunk ) { 1793 if ( is_string( $chunk ) ) { 1794 $block_content .= $chunk; 1795 } else { 1796 $inner_block = $block['innerBlocks'][ $block_index ]; 1797 1798 if ( is_callable( $pre_callback ) ) { 1799 $prev = 0 === $block_index 1800 ? null 1801 : $block['innerBlocks'][ $block_index - 1 ]; 1802 1803 $block_content .= call_user_func_array( 1804 $pre_callback, 1805 array( &$inner_block, &$block, $prev ) 1806 ); 1807 } 1808 1809 if ( is_callable( $post_callback ) ) { 1810 $next = count( $block['innerBlocks'] ) - 1 === $block_index 1811 ? null 1812 : $block['innerBlocks'][ $block_index + 1 ]; 1813 1814 $post_markup = call_user_func_array( 1815 $post_callback, 1816 array( &$inner_block, &$block, $next ) 1817 ); 1818 } 1819 1820 $block_content .= traverse_and_serialize_block( $inner_block, $pre_callback, $post_callback ); 1821 $block_content .= $post_markup ?? ''; 1822 1823 ++$block_index; 1824 } 1825 } 1826 1827 if ( ! is_array( $block['attrs'] ) ) { 1828 $block['attrs'] = array(); 1829 } 1830 1831 return get_comment_delimited_block_content( 1832 $block['blockName'], 1833 $block['attrs'], 1834 $block_content 1835 ); 1836 } 1837 1838 /** 1839 * Replaces patterns in a block tree with their content. 1840 * 1841 * @since 6.6.0 1842 * 1843 * @param array $blocks An array blocks. 1844 * 1845 * @return array An array of blocks with patterns replaced by their content. 1846 */ 1847 function resolve_pattern_blocks( $blocks ) { 1848 static $inner_content; 1849 // Keep track of seen references to avoid infinite loops. 1850 static $seen_refs = array(); 1851 $i = 0; 1852 while ( $i < count( $blocks ) ) { 1853 if ( 'core/pattern' === $blocks[ $i ]['blockName'] ) { 1854 $attrs = $blocks[ $i ]['attrs']; 1855 1856 if ( empty( $attrs['slug'] ) ) { 1857 ++$i; 1858 continue; 1859 } 1860 1861 $slug = $attrs['slug']; 1862 1863 if ( isset( $seen_refs[ $slug ] ) ) { 1864 // Skip recursive patterns. 1865 array_splice( $blocks, $i, 1 ); 1866 continue; 1867 } 1868 1869 $registry = WP_Block_Patterns_Registry::get_instance(); 1870 $pattern = $registry->get_registered( $slug ); 1871 1872 // Skip unknown patterns. 1873 if ( ! $pattern ) { 1874 ++$i; 1875 continue; 1876 } 1877 1878 $blocks_to_insert = parse_blocks( $pattern['content'] ); 1879 $seen_refs[ $slug ] = true; 1880 $prev_inner_content = $inner_content; 1881 $inner_content = null; 1882 $blocks_to_insert = resolve_pattern_blocks( $blocks_to_insert ); 1883 $inner_content = $prev_inner_content; 1884 unset( $seen_refs[ $slug ] ); 1885 array_splice( $blocks, $i, 1, $blocks_to_insert ); 1886 1887 // If we have inner content, we need to insert nulls in the 1888 // inner content array, otherwise serialize_blocks will skip 1889 // blocks. 1890 if ( $inner_content ) { 1891 $null_indices = array_keys( $inner_content, null, true ); 1892 $content_index = $null_indices[ $i ]; 1893 $nulls = array_fill( 0, count( $blocks_to_insert ), null ); 1894 array_splice( $inner_content, $content_index, 1, $nulls ); 1895 } 1896 1897 // Skip inserted blocks. 1898 $i += count( $blocks_to_insert ); 1899 } else { 1900 if ( ! empty( $blocks[ $i ]['innerBlocks'] ) ) { 1901 $prev_inner_content = $inner_content; 1902 $inner_content = $blocks[ $i ]['innerContent']; 1903 $blocks[ $i ]['innerBlocks'] = resolve_pattern_blocks( 1904 $blocks[ $i ]['innerBlocks'] 1905 ); 1906 $blocks[ $i ]['innerContent'] = $inner_content; 1907 $inner_content = $prev_inner_content; 1908 } 1909 ++$i; 1910 } 1911 } 1912 return $blocks; 1913 } 1914 1915 /** 1916 * Given an array of parsed block trees, applies callbacks before and after serializing them and 1917 * returns their concatenated output. 1918 * 1919 * Recursively traverses the blocks and their inner blocks and applies the two callbacks provided as 1920 * arguments, the first one before serializing a block, and the second one after serializing. 1921 * If either callback returns a string value, it will be prepended and appended to the serialized 1922 * block markup, respectively. 1923 * 1924 * The callbacks will receive a reference to the current block as their first argument, so that they 1925 * can also modify it, and the current block's parent block as second argument. Finally, the 1926 * `$pre_callback` receives the previous block, whereas the `$post_callback` receives 1927 * the next block as third argument. 1928 * 1929 * Serialized blocks are returned including comment delimiters, and with all attributes serialized. 1930 * 1931 * This function should be used when there is a need to modify the saved blocks, or to inject markup 1932 * into the return value. Prefer `serialize_blocks` when preparing blocks to be saved to post content. 1933 * 1934 * This function is meant for internal use only. 1935 * 1936 * @since 6.4.0 1937 * @access private 1938 * 1939 * @see serialize_blocks() 1940 * 1941 * @param array[] $blocks An array of parsed blocks. See WP_Block_Parser_Block. 1942 * @param callable $pre_callback Callback to run on each block in the tree before it is traversed and serialized. 1943 * It is called with the following arguments: &$block, $parent_block, $previous_block. 1944 * Its string return value will be prepended to the serialized block markup. 1945 * @param callable $post_callback Callback to run on each block in the tree after it is traversed and serialized. 1946 * It is called with the following arguments: &$block, $parent_block, $next_block. 1947 * Its string return value will be appended to the serialized block markup. 1948 * @return string Serialized block markup. 1949 */ 1950 function traverse_and_serialize_blocks( $blocks, $pre_callback = null, $post_callback = null ) { 1951 $result = ''; 1952 $parent_block = null; // At the top level, there is no parent block to pass to the callbacks; yet the callbacks expect a reference. 1953 1954 $pre_callback_is_callable = is_callable( $pre_callback ); 1955 $post_callback_is_callable = is_callable( $post_callback ); 1956 1957 foreach ( $blocks as $index => $block ) { 1958 if ( $pre_callback_is_callable ) { 1959 $prev = 0 === $index 1960 ? null 1961 : $blocks[ $index - 1 ]; 1962 1963 $result .= call_user_func_array( 1964 $pre_callback, 1965 array( &$block, &$parent_block, $prev ) 1966 ); 1967 } 1968 1969 if ( $post_callback_is_callable ) { 1970 $next = count( $blocks ) - 1 === $index 1971 ? null 1972 : $blocks[ $index + 1 ]; 1973 1974 $post_markup = call_user_func_array( 1975 $post_callback, 1976 array( &$block, &$parent_block, $next ) 1977 ); 1978 } 1979 1980 $result .= traverse_and_serialize_block( $block, $pre_callback, $post_callback ); 1981 $result .= $post_markup ?? ''; 1982 } 1983 1984 return $result; 1985 } 1986 1987 /** 1988 * Filters and sanitizes block content to remove non-allowable HTML 1989 * from parsed block attribute values. 1990 * 1991 * @since 5.3.1 1992 * 1993 * @param string $text Text that may contain block content. 1994 * @param array[]|string $allowed_html Optional. An array of allowed HTML elements and attributes, 1995 * or a context name such as 'post'. See wp_kses_allowed_html() 1996 * for the list of accepted context names. Default 'post'. 1997 * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. 1998 * Defaults to the result of wp_allowed_protocols(). 1999 * @return string The filtered and sanitized content result. 2000 */ 2001 function filter_block_content( $text, $allowed_html = 'post', $allowed_protocols = array() ) { 2002 $result = ''; 2003 2004 if ( str_contains( $text, '<!--' ) && str_contains( $text, '--->' ) ) { 2005 $text = preg_replace_callback( '%<!--(.*?)--->%', '_filter_block_content_callback', $text ); 2006 } 2007 2008 $blocks = parse_blocks( $text ); 2009 foreach ( $blocks as $block ) { 2010 $block = filter_block_kses( $block, $allowed_html, $allowed_protocols ); 2011 $result .= serialize_block( $block ); 2012 } 2013 2014 return $result; 2015 } 2016 2017 /** 2018 * Callback used for regular expression replacement in filter_block_content(). 2019 * 2020 * @since 6.2.1 2021 * @access private 2022 * 2023 * @param array $matches Array of preg_replace_callback matches. 2024 * @return string Replacement string. 2025 */ 2026 function _filter_block_content_callback( $matches ) { 2027 return '<!--' . rtrim( $matches[1], '-' ) . '-->'; 2028 } 2029 2030 /** 2031 * Filters and sanitizes a parsed block to remove non-allowable HTML 2032 * from block attribute values. 2033 * 2034 * @since 5.3.1 2035 * 2036 * @param WP_Block_Parser_Block $block The parsed block object. 2037 * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, 2038 * or a context name such as 'post'. See wp_kses_allowed_html() 2039 * for the list of accepted context names. 2040 * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. 2041 * Defaults to the result of wp_allowed_protocols(). 2042 * @return array The filtered and sanitized block object result. 2043 */ 2044 function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) { 2045 $block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols, $block ); 2046 2047 if ( is_array( $block['innerBlocks'] ) ) { 2048 foreach ( $block['innerBlocks'] as $i => $inner_block ) { 2049 $block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols ); 2050 } 2051 } 2052 2053 return $block; 2054 } 2055 2056 /** 2057 * Filters and sanitizes a parsed block attribute value to remove 2058 * non-allowable HTML. 2059 * 2060 * @since 5.3.1 2061 * @since 6.5.5 Added the `$block_context` parameter. 2062 * 2063 * @param string[]|string $value The attribute value to filter. 2064 * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, 2065 * or a context name such as 'post'. See wp_kses_allowed_html() 2066 * for the list of accepted context names. 2067 * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. 2068 * Defaults to the result of wp_allowed_protocols(). 2069 * @param array $block_context Optional. The block the attribute belongs to, in parsed block array format. 2070 * @return string[]|string The filtered and sanitized result. 2071 */ 2072 function filter_block_kses_value( $value, $allowed_html, $allowed_protocols = array(), $block_context = null ) { 2073 if ( is_array( $value ) ) { 2074 foreach ( $value as $key => $inner_value ) { 2075 $filtered_key = filter_block_kses_value( $key, $allowed_html, $allowed_protocols, $block_context ); 2076 $filtered_value = filter_block_kses_value( $inner_value, $allowed_html, $allowed_protocols, $block_context ); 2077 2078 if ( isset( $block_context['blockName'] ) && 'core/template-part' === $block_context['blockName'] ) { 2079 $filtered_value = filter_block_core_template_part_attributes( $filtered_value, $filtered_key, $allowed_html ); 2080 } 2081 if ( $filtered_key !== $key ) { 2082 unset( $value[ $key ] ); 2083 } 2084 2085 $value[ $filtered_key ] = $filtered_value; 2086 } 2087 } elseif ( is_string( $value ) ) { 2088 return wp_kses( $value, $allowed_html, $allowed_protocols ); 2089 } 2090 2091 return $value; 2092 } 2093 2094 /** 2095 * Sanitizes the value of the Template Part block's `tagName` attribute. 2096 * 2097 * @since 6.5.5 2098 * 2099 * @param string $attribute_value The attribute value to filter. 2100 * @param string $attribute_name The attribute name. 2101 * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, 2102 * or a context name such as 'post'. See wp_kses_allowed_html() 2103 * for the list of accepted context names. 2104 * @return string The sanitized attribute value. 2105 */ 2106 function filter_block_core_template_part_attributes( $attribute_value, $attribute_name, $allowed_html ) { 2107 if ( empty( $attribute_value ) || 'tagName' !== $attribute_name ) { 2108 return $attribute_value; 2109 } 2110 if ( ! is_array( $allowed_html ) ) { 2111 $allowed_html = wp_kses_allowed_html( $allowed_html ); 2112 } 2113 return isset( $allowed_html[ $attribute_value ] ) ? $attribute_value : ''; 2114 } 2115 2116 /** 2117 * Parses blocks out of a content string, and renders those appropriate for the excerpt. 2118 * 2119 * As the excerpt should be a small string of text relevant to the full post content, 2120 * this function renders the blocks that are most likely to contain such text. 2121 * 2122 * @since 5.0.0 2123 * 2124 * @param string $content The content to parse. 2125 * @return string The parsed and filtered content. 2126 */ 2127 function excerpt_remove_blocks( $content ) { 2128 if ( ! has_blocks( $content ) ) { 2129 return $content; 2130 } 2131 2132 $allowed_inner_blocks = array( 2133 // Classic blocks have their blockName set to null. 2134 null, 2135 'core/freeform', 2136 'core/heading', 2137 'core/html', 2138 'core/list', 2139 'core/media-text', 2140 'core/paragraph', 2141 'core/preformatted', 2142 'core/pullquote', 2143 'core/quote', 2144 'core/table', 2145 'core/verse', 2146 ); 2147 2148 $allowed_wrapper_blocks = array( 2149 'core/columns', 2150 'core/column', 2151 'core/group', 2152 ); 2153 2154 /** 2155 * Filters the list of blocks that can be used as wrapper blocks, allowing 2156 * excerpts to be generated from the `innerBlocks` of these wrappers. 2157 * 2158 * @since 5.8.0 2159 * 2160 * @param string[] $allowed_wrapper_blocks The list of names of allowed wrapper blocks. 2161 */ 2162 $allowed_wrapper_blocks = apply_filters( 'excerpt_allowed_wrapper_blocks', $allowed_wrapper_blocks ); 2163 2164 $allowed_blocks = array_merge( $allowed_inner_blocks, $allowed_wrapper_blocks ); 2165 2166 /** 2167 * Filters the list of blocks that can contribute to the excerpt. 2168 * 2169 * If a dynamic block is added to this list, it must not generate another 2170 * excerpt, as this will cause an infinite loop to occur. 2171 * 2172 * @since 5.0.0 2173 * 2174 * @param string[] $allowed_blocks The list of names of allowed blocks. 2175 */ 2176 $allowed_blocks = apply_filters( 'excerpt_allowed_blocks', $allowed_blocks ); 2177 $blocks = parse_blocks( $content ); 2178 $output = ''; 2179 2180 foreach ( $blocks as $block ) { 2181 if ( in_array( $block['blockName'], $allowed_blocks, true ) ) { 2182 if ( ! empty( $block['innerBlocks'] ) ) { 2183 if ( in_array( $block['blockName'], $allowed_wrapper_blocks, true ) ) { 2184 $output .= _excerpt_render_inner_blocks( $block, $allowed_blocks ); 2185 continue; 2186 } 2187 2188 // Skip the block if it has disallowed or nested inner blocks. 2189 foreach ( $block['innerBlocks'] as $inner_block ) { 2190 if ( 2191 ! in_array( $inner_block['blockName'], $allowed_inner_blocks, true ) || 2192 ! empty( $inner_block['innerBlocks'] ) 2193 ) { 2194 continue 2; 2195 } 2196 } 2197 } 2198 2199 $output .= render_block( $block ); 2200 } 2201 } 2202 2203 return $output; 2204 } 2205 2206 /** 2207 * Parses footnotes markup out of a content string, 2208 * and renders those appropriate for the excerpt. 2209 * 2210 * @since 6.3.0 2211 * 2212 * @param string $content The content to parse. 2213 * @return string The parsed and filtered content. 2214 */ 2215 function excerpt_remove_footnotes( $content ) { 2216 if ( ! str_contains( $content, 'data-fn=' ) ) { 2217 return $content; 2218 } 2219 2220 return preg_replace( 2221 '_<sup data-fn="[^"]+" class="[^"]+">\s*<a href="[^"]+" id="[^"]+">\d+</a>\s*</sup>_', 2222 '', 2223 $content 2224 ); 2225 } 2226 2227 /** 2228 * Renders inner blocks from the allowed wrapper blocks 2229 * for generating an excerpt. 2230 * 2231 * @since 5.8.0 2232 * @access private 2233 * 2234 * @param array $parsed_block The parsed block. 2235 * @param array $allowed_blocks The list of allowed inner blocks. 2236 * @return string The rendered inner blocks. 2237 */ 2238 function _excerpt_render_inner_blocks( $parsed_block, $allowed_blocks ) { 2239 $output = ''; 2240 2241 foreach ( $parsed_block['innerBlocks'] as $inner_block ) { 2242 if ( ! in_array( $inner_block['blockName'], $allowed_blocks, true ) ) { 2243 continue; 2244 } 2245 2246 if ( empty( $inner_block['innerBlocks'] ) ) { 2247 $output .= render_block( $inner_block ); 2248 } else { 2249 $output .= _excerpt_render_inner_blocks( $inner_block, $allowed_blocks ); 2250 } 2251 } 2252 2253 return $output; 2254 } 2255 2256 /** 2257 * Renders a single block into a HTML string. 2258 * 2259 * @since 5.0.0 2260 * 2261 * @global WP_Post $post The post to edit. 2262 * 2263 * @param array $parsed_block { 2264 * An associative array of the block being rendered. See WP_Block_Parser_Block. 2265 * 2266 * @type string|null $blockName Name of block. 2267 * @type array $attrs Attributes from block comment delimiters. 2268 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 2269 * have the same structure as this one. 2270 * @type string $innerHTML HTML from inside block comment delimiters. 2271 * @type array $innerContent List of string fragments and null markers where 2272 * inner blocks were found. 2273 * } 2274 * @return string String of rendered HTML. 2275 */ 2276 function render_block( $parsed_block ) { 2277 global $post; 2278 $parent_block = null; 2279 2280 /** 2281 * Allows render_block() to be short-circuited, by returning a non-null value. 2282 * 2283 * @since 5.1.0 2284 * @since 5.9.0 The `$parent_block` parameter was added. 2285 * 2286 * @param string|null $pre_render The pre-rendered content. Default null. 2287 * @param array $parsed_block { 2288 * An associative array of the block being rendered. See WP_Block_Parser_Block. 2289 * 2290 * @type string|null $blockName Name of block. 2291 * @type array $attrs Attributes from block comment delimiters. 2292 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 2293 * have the same structure as this one. 2294 * @type string $innerHTML HTML from inside block comment delimiters. 2295 * @type array $innerContent List of string fragments and null markers where 2296 * inner blocks were found. 2297 * } 2298 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. 2299 */ 2300 $pre_render = apply_filters( 'pre_render_block', null, $parsed_block, $parent_block ); 2301 if ( ! is_null( $pre_render ) ) { 2302 return $pre_render; 2303 } 2304 2305 $source_block = $parsed_block; 2306 2307 /** 2308 * Filters the block being rendered in render_block(), before it's processed. 2309 * 2310 * @since 5.1.0 2311 * @since 5.9.0 The `$parent_block` parameter was added. 2312 * 2313 * @param array $parsed_block { 2314 * An associative array of the block being rendered. See WP_Block_Parser_Block. 2315 * 2316 * @type string|null $blockName Name of block. 2317 * @type array $attrs Attributes from block comment delimiters. 2318 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 2319 * have the same structure as this one. 2320 * @type string $innerHTML HTML from inside block comment delimiters. 2321 * @type array $innerContent List of string fragments and null markers where 2322 * inner blocks were found. 2323 * } 2324 * @param array $source_block { 2325 * An un-modified copy of `$parsed_block`, as it appeared in the source content. 2326 * See WP_Block_Parser_Block. 2327 * 2328 * @type string|null $blockName Name of block. 2329 * @type array $attrs Attributes from block comment delimiters. 2330 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 2331 * have the same structure as this one. 2332 * @type string $innerHTML HTML from inside block comment delimiters. 2333 * @type array $innerContent List of string fragments and null markers where 2334 * inner blocks were found. 2335 * } 2336 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. 2337 */ 2338 $parsed_block = apply_filters( 'render_block_data', $parsed_block, $source_block, $parent_block ); 2339 2340 $context = array(); 2341 2342 if ( $post instanceof WP_Post ) { 2343 $context['postId'] = $post->ID; 2344 2345 /* 2346 * The `postType` context is largely unnecessary server-side, since the ID 2347 * is usually sufficient on its own. That being said, since a block's 2348 * manifest is expected to be shared between the server and the client, 2349 * it should be included to consistently fulfill the expectation. 2350 */ 2351 $context['postType'] = $post->post_type; 2352 } 2353 2354 /** 2355 * Filters the default context provided to a rendered block. 2356 * 2357 * @since 5.5.0 2358 * @since 5.9.0 The `$parent_block` parameter was added. 2359 * 2360 * @param array $context Default context. 2361 * @param array $parsed_block { 2362 * An associative array of the block being rendered. See WP_Block_Parser_Block. 2363 * 2364 * @type string|null $blockName Name of block. 2365 * @type array $attrs Attributes from block comment delimiters. 2366 * @type array[] $innerBlocks List of inner blocks. An array of arrays that 2367 * have the same structure as this one. 2368 * @type string $innerHTML HTML from inside block comment delimiters. 2369 * @type array $innerContent List of string fragments and null markers where 2370 * inner blocks were found. 2371 * } 2372 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. 2373 */ 2374 $context = apply_filters( 'render_block_context', $context, $parsed_block, $parent_block ); 2375 2376 $block = new WP_Block( $parsed_block, $context ); 2377 2378 return $block->render(); 2379 } 2380 2381 /** 2382 * Parses blocks out of a content string. 2383 * 2384 * Given an HTML document, this function fully-parses block content, producing 2385 * a tree of blocks and their contents, as well as top-level non-block content, 2386 * which will appear as a block with no `blockName`. 2387 * 2388 * This function can be memory heavy for certain documents, particularly those 2389 * with deeply-nested blocks or blocks with extensive attribute values. Further, 2390 * this function must parse an entire document in one atomic operation. 2391 * 2392 * If the entire parsed document is not necessary, consider using {@see WP_Block_Processor} 2393 * instead, as it provides a streaming and low-overhead interface for finding blocks. 2394 * 2395 * @since 5.0.0 2396 * 2397 * @param string $content Post content. 2398 * @return array[] { 2399 * Array of block structures. 2400 * 2401 * @type array ...$0 { 2402 * An associative array of a single parsed block object. 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 * } 2413 */ 2414 function parse_blocks( $content ) { 2415 /** 2416 * Filter to allow plugins to replace the server-side block parser. 2417 * 2418 * @since 5.0.0 2419 * 2420 * @param string $parser_class Name of block parser class. 2421 */ 2422 $parser_class = apply_filters( 'block_parser_class', 'WP_Block_Parser' ); 2423 2424 $parser = new $parser_class(); 2425 return $parser->parse( $content ); 2426 } 2427 2428 /** 2429 * Parses dynamic blocks out of `post_content` and re-renders them. 2430 * 2431 * @since 5.0.0 2432 * 2433 * @param string $content Post content. 2434 * @return string Updated post content. 2435 */ 2436 function do_blocks( $content ) { 2437 $blocks = parse_blocks( $content ); 2438 $top_level_block_count = count( $blocks ); 2439 $output = ''; 2440 2441 /** 2442 * Parsed blocks consist of a list of top-level blocks. Those top-level 2443 * blocks may themselves contain nested inner blocks. However, every 2444 * top-level block is rendered independently, meaning there are no data 2445 * dependencies between them. 2446 * 2447 * Ideally, therefore, the parser would only need to parse one complete 2448 * top-level block at a time, render it, and move on. Unfortunately, this 2449 * is not possible with {@see \parse_blocks()} because it must parse the 2450 * entire given document at once. 2451 * 2452 * While the current implementation prevents this optimization, it’s still 2453 * possible to reduce the peak memory use when calls to `render_block()` 2454 * on those top-level blocks are memory-heavy (which many of them are). 2455 * By setting each parsed block to `NULL` after rendering it, any memory 2456 * allocated during the render will be freed and reused for the next block. 2457 * Before making this change, that memory was retained and would lead to 2458 * out-of-memory crashes for certain posts that now run with this change. 2459 */ 2460 for ( $i = 0; $i < $top_level_block_count; $i++ ) { 2461 $output .= render_block( $blocks[ $i ] ); 2462 $blocks[ $i ] = null; 2463 } 2464 2465 // If there are blocks in this content, we shouldn't run wpautop() on it later. 2466 $priority = has_filter( 'the_content', 'wpautop' ); 2467 if ( false !== $priority && doing_filter( 'the_content' ) && has_blocks( $content ) ) { 2468 remove_filter( 'the_content', 'wpautop', $priority ); 2469 add_filter( 'the_content', '_restore_wpautop_hook', $priority + 1 ); 2470 } 2471 2472 return $output; 2473 } 2474 2475 /** 2476 * If do_blocks() needs to remove wpautop() from the `the_content` filter, this re-adds it afterwards, 2477 * for subsequent `the_content` usage. 2478 * 2479 * @since 5.0.0 2480 * @access private 2481 * 2482 * @param string $content The post content running through this filter. 2483 * @return string The unmodified content. 2484 */ 2485 function _restore_wpautop_hook( $content ) { 2486 $current_priority = has_filter( 'the_content', '_restore_wpautop_hook' ); 2487 2488 add_filter( 'the_content', 'wpautop', $current_priority - 1 ); 2489 remove_filter( 'the_content', '_restore_wpautop_hook', $current_priority ); 2490 2491 return $content; 2492 } 2493 2494 /** 2495 * Returns the current version of the block format that the content string is using. 2496 * 2497 * If the string doesn't contain blocks, it returns 0. 2498 * 2499 * @since 5.0.0 2500 * 2501 * @param string $content Content to test. 2502 * @return int The block format version is 1 if the content contains one or more blocks, 0 otherwise. 2503 */ 2504 function block_version( $content ) { 2505 return has_blocks( $content ) ? 1 : 0; 2506 } 2507 2508 /** 2509 * Registers a new block style. 2510 * 2511 * @since 5.3.0 2512 * @since 6.6.0 Added support for registering styles for multiple block types. 2513 * 2514 * @link https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/ 2515 * 2516 * @param string|string[] $block_name Block type name including namespace or array of namespaced block type names. 2517 * @param array $style_properties Array containing the properties of the style name, label, 2518 * style_handle (name of the stylesheet to be enqueued), 2519 * inline_style (string containing the CSS to be added), 2520 * style_data (theme.json-like array to generate CSS from). 2521 * See WP_Block_Styles_Registry::register(). 2522 * @return bool True if the block style was registered with success and false otherwise. 2523 */ 2524 function register_block_style( $block_name, $style_properties ) { 2525 return WP_Block_Styles_Registry::get_instance()->register( $block_name, $style_properties ); 2526 } 2527 2528 /** 2529 * Unregisters a block style. 2530 * 2531 * @since 5.3.0 2532 * 2533 * @param string $block_name Block type name including namespace. 2534 * @param string $block_style_name Block style name. 2535 * @return bool True if the block style was unregistered with success and false otherwise. 2536 */ 2537 function unregister_block_style( $block_name, $block_style_name ) { 2538 return WP_Block_Styles_Registry::get_instance()->unregister( $block_name, $block_style_name ); 2539 } 2540 2541 /** 2542 * Checks whether the current block type supports the feature requested. 2543 * 2544 * @since 5.8.0 2545 * @since 6.4.0 The `$feature` parameter now supports a string. 2546 * 2547 * @param WP_Block_Type $block_type Block type to check for support. 2548 * @param string|array $feature Feature slug, or path to a specific feature to check support for. 2549 * @param mixed $default_value Optional. Fallback value for feature support. Default false. 2550 * @return bool Whether the feature is supported. 2551 */ 2552 function block_has_support( $block_type, $feature, $default_value = false ) { 2553 $block_support = $default_value; 2554 if ( $block_type instanceof WP_Block_Type ) { 2555 if ( is_array( $feature ) && count( $feature ) === 1 ) { 2556 $feature = $feature[0]; 2557 } 2558 2559 if ( is_array( $feature ) ) { 2560 $block_support = _wp_array_get( $block_type->supports, $feature, $default_value ); 2561 } elseif ( isset( $block_type->supports[ $feature ] ) ) { 2562 $block_support = $block_type->supports[ $feature ]; 2563 } 2564 } 2565 2566 return true === $block_support || is_array( $block_support ); 2567 } 2568 2569 /** 2570 * Converts typography keys declared under `supports.*` to `supports.typography.*`. 2571 * 2572 * Displays a `_doing_it_wrong()` notice when a block using the older format is detected. 2573 * 2574 * @since 5.8.0 2575 * 2576 * @param array $metadata Metadata for registering a block type. 2577 * @return array Filtered metadata for registering a block type. 2578 */ 2579 function wp_migrate_old_typography_shape( $metadata ) { 2580 if ( ! isset( $metadata['supports'] ) ) { 2581 return $metadata; 2582 } 2583 2584 $typography_keys = array( 2585 '__experimentalFontFamily', 2586 '__experimentalFontStyle', 2587 '__experimentalFontWeight', 2588 '__experimentalLetterSpacing', 2589 '__experimentalTextDecoration', 2590 '__experimentalTextTransform', 2591 'fontSize', 2592 'lineHeight', 2593 ); 2594 2595 foreach ( $typography_keys as $typography_key ) { 2596 $support_for_key = $metadata['supports'][ $typography_key ] ?? null; 2597 2598 if ( null !== $support_for_key ) { 2599 _doing_it_wrong( 2600 'register_block_type_from_metadata()', 2601 sprintf( 2602 /* translators: 1: Block type, 2: Typography supports key, e.g: fontSize, lineHeight, etc. 3: block.json, 4: Old metadata key, 5: New metadata key. */ 2603 __( 'Block "%1$s" is declaring %2$s support in %3$s file under %4$s. %2$s support is now declared under %5$s.' ), 2604 $metadata['name'], 2605 "<code>$typography_key</code>", 2606 '<code>block.json</code>', 2607 "<code>supports.$typography_key</code>", 2608 "<code>supports.typography.$typography_key</code>" 2609 ), 2610 '5.8.0' 2611 ); 2612 2613 _wp_array_set( $metadata['supports'], array( 'typography', $typography_key ), $support_for_key ); 2614 unset( $metadata['supports'][ $typography_key ] ); 2615 } 2616 } 2617 2618 return $metadata; 2619 } 2620 2621 /** 2622 * Helper function that constructs a WP_Query args array from 2623 * a `Query` block properties. 2624 * 2625 * It's used in Query Loop, Query Pagination Numbers and Query Pagination Next blocks. 2626 * 2627 * @since 5.8.0 2628 * @since 6.1.0 Added `query_loop_block_query_vars` filter and `parents` support in query. 2629 * @since 6.7.0 Added support for the `format` property in query. 2630 * 2631 * @param WP_Block $block Block instance. 2632 * @param int $page Current query's page. 2633 * 2634 * @return array Returns the constructed WP_Query arguments. 2635 */ 2636 function build_query_vars_from_query_block( $block, $page ) { 2637 $query = array( 2638 'post_type' => 'post', 2639 'order' => 'DESC', 2640 'orderby' => 'date', 2641 'post__not_in' => array(), 2642 'tax_query' => array(), 2643 ); 2644 2645 if ( isset( $block->context['query'] ) ) { 2646 if ( ! empty( $block->context['query']['postType'] ) ) { 2647 $post_type_param = $block->context['query']['postType']; 2648 if ( is_post_type_viewable( $post_type_param ) ) { 2649 $query['post_type'] = $post_type_param; 2650 } 2651 } 2652 if ( isset( $block->context['query']['sticky'] ) && ! empty( $block->context['query']['sticky'] ) ) { 2653 $sticky = get_option( 'sticky_posts' ); 2654 if ( 'only' === $block->context['query']['sticky'] ) { 2655 /* 2656 * Passing an empty array to post__in will return have_posts() as true (and all posts will be returned). 2657 * Logic should be used before hand to determine if WP_Query should be used in the event that the array 2658 * being passed to post__in is empty. 2659 * 2660 * @see https://core.trac.wordpress.org/ticket/28099 2661 */ 2662 $query['post__in'] = ! empty( $sticky ) ? $sticky : array( 0 ); 2663 $query['ignore_sticky_posts'] = 1; 2664 } elseif ( 'exclude' === $block->context['query']['sticky'] ) { 2665 $query['post__not_in'] = array_merge( $query['post__not_in'], $sticky ); 2666 } elseif ( 'ignore' === $block->context['query']['sticky'] ) { 2667 $query['ignore_sticky_posts'] = 1; 2668 } 2669 } 2670 if ( ! empty( $block->context['query']['exclude'] ) ) { 2671 $excluded_post_ids = array_map( 'intval', $block->context['query']['exclude'] ); 2672 $excluded_post_ids = array_filter( $excluded_post_ids ); 2673 $query['post__not_in'] = array_merge( $query['post__not_in'], $excluded_post_ids ); 2674 } 2675 if ( 2676 isset( $block->context['query']['perPage'] ) && 2677 is_numeric( $block->context['query']['perPage'] ) 2678 ) { 2679 $per_page = absint( $block->context['query']['perPage'] ); 2680 $offset = 0; 2681 2682 if ( 2683 isset( $block->context['query']['offset'] ) && 2684 is_numeric( $block->context['query']['offset'] ) 2685 ) { 2686 $offset = absint( $block->context['query']['offset'] ); 2687 } 2688 2689 $query['offset'] = ( $per_page * ( $page - 1 ) ) + $offset; 2690 $query['posts_per_page'] = $per_page; 2691 } 2692 // Migrate `categoryIds` and `tagIds` to `tax_query` for backwards compatibility. 2693 if ( ! empty( $block->context['query']['categoryIds'] ) || ! empty( $block->context['query']['tagIds'] ) ) { 2694 $tax_query_back_compat = array(); 2695 if ( ! empty( $block->context['query']['categoryIds'] ) ) { 2696 $tax_query_back_compat[] = array( 2697 'taxonomy' => 'category', 2698 'terms' => array_filter( array_map( 'intval', $block->context['query']['categoryIds'] ) ), 2699 'include_children' => false, 2700 ); 2701 } 2702 if ( ! empty( $block->context['query']['tagIds'] ) ) { 2703 $tax_query_back_compat[] = array( 2704 'taxonomy' => 'post_tag', 2705 'terms' => array_filter( array_map( 'intval', $block->context['query']['tagIds'] ) ), 2706 'include_children' => false, 2707 ); 2708 } 2709 $query['tax_query'] = array_merge( $query['tax_query'], $tax_query_back_compat ); 2710 } 2711 if ( ! empty( $block->context['query']['taxQuery'] ) ) { 2712 $tax_query = array(); 2713 foreach ( $block->context['query']['taxQuery'] as $taxonomy => $terms ) { 2714 if ( is_taxonomy_viewable( $taxonomy ) && ! empty( $terms ) ) { 2715 $tax_query[] = array( 2716 'taxonomy' => $taxonomy, 2717 'terms' => array_filter( array_map( 'intval', $terms ) ), 2718 'include_children' => false, 2719 ); 2720 } 2721 } 2722 $query['tax_query'] = array_merge( $query['tax_query'], $tax_query ); 2723 } 2724 if ( ! empty( $block->context['query']['format'] ) && is_array( $block->context['query']['format'] ) ) { 2725 $formats = $block->context['query']['format']; 2726 /* 2727 * Validate that the format is either `standard` or a supported post format. 2728 * - First, add `standard` to the array of valid formats. 2729 * - Then, remove any invalid formats. 2730 */ 2731 $valid_formats = array_merge( array( 'standard' ), get_post_format_slugs() ); 2732 $formats = array_intersect( $formats, $valid_formats ); 2733 2734 /* 2735 * The relation needs to be set to `OR` since the request can contain 2736 * two separate conditions. The user may be querying for items that have 2737 * either the `standard` format or a specific format. 2738 */ 2739 $formats_query = array( 'relation' => 'OR' ); 2740 2741 /* 2742 * The default post format, `standard`, is not stored in the database. 2743 * If `standard` is part of the request, the query needs to exclude all post items that 2744 * have a format assigned. 2745 */ 2746 if ( in_array( 'standard', $formats, true ) ) { 2747 $formats_query[] = array( 2748 'taxonomy' => 'post_format', 2749 'field' => 'slug', 2750 'operator' => 'NOT EXISTS', 2751 ); 2752 // Remove the `standard` format, since it cannot be queried. 2753 unset( $formats[ array_search( 'standard', $formats, true ) ] ); 2754 } 2755 // Add any remaining formats to the formats query. 2756 if ( ! empty( $formats ) ) { 2757 // Add the `post-format-` prefix. 2758 $terms = array_map( 2759 static function ( $format ) { 2760 return "post-format-$format"; 2761 }, 2762 $formats 2763 ); 2764 $formats_query[] = array( 2765 'taxonomy' => 'post_format', 2766 'field' => 'slug', 2767 'terms' => $terms, 2768 'operator' => 'IN', 2769 ); 2770 } 2771 2772 /* 2773 * Add `$formats_query` to `$query`, as long as it contains more than one key: 2774 * If `$formats_query` only contains the initial `relation` key, there are no valid formats to query, 2775 * and the query should not be modified. 2776 */ 2777 if ( count( $formats_query ) > 1 ) { 2778 // Enable filtering by both post formats and other taxonomies by combining them with `AND`. 2779 if ( empty( $query['tax_query'] ) ) { 2780 $query['tax_query'] = $formats_query; 2781 } else { 2782 $query['tax_query'] = array( 2783 'relation' => 'AND', 2784 $query['tax_query'], 2785 $formats_query, 2786 ); 2787 } 2788 } 2789 } 2790 2791 if ( 2792 isset( $block->context['query']['order'] ) && 2793 in_array( strtoupper( $block->context['query']['order'] ), array( 'ASC', 'DESC' ), true ) 2794 ) { 2795 $query['order'] = strtoupper( $block->context['query']['order'] ); 2796 } 2797 if ( isset( $block->context['query']['orderBy'] ) ) { 2798 $query['orderby'] = $block->context['query']['orderBy']; 2799 } 2800 if ( 2801 isset( $block->context['query']['author'] ) 2802 ) { 2803 if ( is_array( $block->context['query']['author'] ) ) { 2804 $query['author__in'] = array_filter( array_map( 'intval', $block->context['query']['author'] ) ); 2805 } elseif ( is_string( $block->context['query']['author'] ) ) { 2806 $query['author__in'] = array_filter( array_map( 'intval', explode( ',', $block->context['query']['author'] ) ) ); 2807 } elseif ( is_int( $block->context['query']['author'] ) && $block->context['query']['author'] > 0 ) { 2808 $query['author'] = $block->context['query']['author']; 2809 } 2810 } 2811 if ( ! empty( $block->context['query']['search'] ) ) { 2812 $query['s'] = $block->context['query']['search']; 2813 } 2814 if ( ! empty( $block->context['query']['parents'] ) && is_post_type_hierarchical( $query['post_type'] ) ) { 2815 $query['post_parent__in'] = array_unique( array_map( 'intval', $block->context['query']['parents'] ) ); 2816 } 2817 } 2818 2819 /** 2820 * Filters the arguments which will be passed to `WP_Query` for the Query Loop Block. 2821 * 2822 * Anything to this filter should be compatible with the `WP_Query` API to form 2823 * the query context which will be passed down to the Query Loop Block's children. 2824 * This can help, for example, to include additional settings or meta queries not 2825 * directly supported by the core Query Loop Block, and extend its capabilities. 2826 * 2827 * Please note that this will only influence the query that will be rendered on the 2828 * front-end. The editor preview is not affected by this filter. Also, worth noting 2829 * that the editor preview uses the REST API, so, ideally, one should aim to provide 2830 * attributes which are also compatible with the REST API, in order to be able to 2831 * implement identical queries on both sides. 2832 * 2833 * @since 6.1.0 2834 * 2835 * @param array $query Array containing parameters for `WP_Query` as parsed by the block context. 2836 * @param WP_Block $block Block instance. 2837 * @param int $page Current query's page. 2838 */ 2839 return apply_filters( 'query_loop_block_query_vars', $query, $block, $page ); 2840 } 2841 2842 /** 2843 * Helper function that returns the proper pagination arrow HTML for 2844 * `QueryPaginationNext` and `QueryPaginationPrevious` blocks based 2845 * on the provided `paginationArrow` from `QueryPagination` context. 2846 * 2847 * It's used in QueryPaginationNext and QueryPaginationPrevious blocks. 2848 * 2849 * @since 5.9.0 2850 * 2851 * @param WP_Block $block Block instance. 2852 * @param bool $is_next Flag for handling `next/previous` blocks. 2853 * @return string|null The pagination arrow HTML or null if there is none. 2854 */ 2855 function get_query_pagination_arrow( $block, $is_next ) { 2856 $arrow_map = array( 2857 'none' => '', 2858 'arrow' => array( 2859 'next' => '→', 2860 'previous' => '←', 2861 ), 2862 'chevron' => array( 2863 'next' => '»', 2864 'previous' => '«', 2865 ), 2866 ); 2867 if ( ! empty( $block->context['paginationArrow'] ) && array_key_exists( $block->context['paginationArrow'], $arrow_map ) && ! empty( $arrow_map[ $block->context['paginationArrow'] ] ) ) { 2868 $pagination_type = $is_next ? 'next' : 'previous'; 2869 $arrow_attribute = $block->context['paginationArrow']; 2870 $arrow = $arrow_map[ $block->context['paginationArrow'] ][ $pagination_type ]; 2871 $arrow_classes = "wp-block-query-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; 2872 return "<span class='$arrow_classes' aria-hidden='true'>$arrow</span>"; 2873 } 2874 return null; 2875 } 2876 2877 /** 2878 * Helper function that constructs a comment query vars array from the passed 2879 * block properties. 2880 * 2881 * It's used with the Comment Query Loop inner blocks. 2882 * 2883 * @since 6.0.0 2884 * 2885 * @param WP_Block $block Block instance. 2886 * @return array Returns the comment query parameters to use with the 2887 * WP_Comment_Query constructor. 2888 */ 2889 function build_comment_query_vars_from_block( $block ) { 2890 2891 $comment_args = array( 2892 'orderby' => 'comment_date_gmt', 2893 'order' => 'ASC', 2894 'status' => 'approve', 2895 'no_found_rows' => false, 2896 ); 2897 2898 if ( is_user_logged_in() ) { 2899 $comment_args['include_unapproved'] = array( get_current_user_id() ); 2900 } else { 2901 $unapproved_email = wp_get_unapproved_comment_author_email(); 2902 2903 if ( $unapproved_email ) { 2904 $comment_args['include_unapproved'] = array( $unapproved_email ); 2905 } 2906 } 2907 2908 if ( ! empty( $block->context['postId'] ) ) { 2909 $comment_args['post_id'] = (int) $block->context['postId']; 2910 } 2911 2912 if ( get_option( 'thread_comments' ) ) { 2913 $comment_args['hierarchical'] = 'threaded'; 2914 } else { 2915 $comment_args['hierarchical'] = false; 2916 } 2917 2918 if ( get_option( 'page_comments' ) === '1' || get_option( 'page_comments' ) === true ) { 2919 $per_page = get_option( 'comments_per_page' ); 2920 $default_page = get_option( 'default_comments_page' ); 2921 if ( $per_page > 0 ) { 2922 $comment_args['number'] = $per_page; 2923 2924 $page = (int) get_query_var( 'cpage' ); 2925 if ( $page ) { 2926 $comment_args['paged'] = $page; 2927 } elseif ( 'oldest' === $default_page ) { 2928 $comment_args['paged'] = 1; 2929 } elseif ( 'newest' === $default_page ) { 2930 $max_num_pages = (int) ( new WP_Comment_Query( $comment_args ) )->max_num_pages; 2931 if ( 0 !== $max_num_pages ) { 2932 $comment_args['paged'] = $max_num_pages; 2933 } 2934 } 2935 } 2936 } 2937 2938 return $comment_args; 2939 } 2940 2941 /** 2942 * Helper function that returns the proper pagination arrow HTML for 2943 * `CommentsPaginationNext` and `CommentsPaginationPrevious` blocks based on the 2944 * provided `paginationArrow` from `CommentsPagination` context. 2945 * 2946 * It's used in CommentsPaginationNext and CommentsPaginationPrevious blocks. 2947 * 2948 * @since 6.0.0 2949 * 2950 * @param WP_Block $block Block instance. 2951 * @param string $pagination_type Optional. Type of the arrow we will be rendering. 2952 * Accepts 'next' or 'previous'. Default 'next'. 2953 * @return string|null The pagination arrow HTML or null if there is none. 2954 */ 2955 function get_comments_pagination_arrow( $block, $pagination_type = 'next' ) { 2956 $arrow_map = array( 2957 'none' => '', 2958 'arrow' => array( 2959 'next' => '→', 2960 'previous' => '←', 2961 ), 2962 'chevron' => array( 2963 'next' => '»', 2964 'previous' => '«', 2965 ), 2966 ); 2967 if ( ! empty( $block->context['comments/paginationArrow'] ) && ! empty( $arrow_map[ $block->context['comments/paginationArrow'] ][ $pagination_type ] ) ) { 2968 $arrow_attribute = $block->context['comments/paginationArrow']; 2969 $arrow = $arrow_map[ $block->context['comments/paginationArrow'] ][ $pagination_type ]; 2970 $arrow_classes = "wp-block-comments-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; 2971 return "<span class='$arrow_classes' aria-hidden='true'>$arrow</span>"; 2972 } 2973 return null; 2974 } 2975 2976 /** 2977 * Strips all HTML from the content of footnotes, and sanitizes the ID. 2978 * 2979 * This function expects slashed data on the footnotes content. 2980 * 2981 * @access private 2982 * @since 6.3.2 2983 * 2984 * @param string $footnotes JSON-encoded string of an array containing the content and ID of each footnote. 2985 * @return string Filtered content without any HTML on the footnote content and with the sanitized ID. 2986 */ 2987 function _wp_filter_post_meta_footnotes( $footnotes ) { 2988 $footnotes_decoded = json_decode( $footnotes, true ); 2989 if ( ! is_array( $footnotes_decoded ) ) { 2990 return ''; 2991 } 2992 $footnotes_sanitized = array(); 2993 foreach ( $footnotes_decoded as $footnote ) { 2994 if ( ! empty( $footnote['content'] ) && ! empty( $footnote['id'] ) ) { 2995 $footnotes_sanitized[] = array( 2996 'id' => sanitize_key( $footnote['id'] ), 2997 'content' => wp_unslash( wp_filter_post_kses( wp_slash( $footnote['content'] ) ) ), 2998 ); 2999 } 3000 } 3001 return wp_json_encode( $footnotes_sanitized ); 3002 } 3003 3004 /** 3005 * Adds the filters for footnotes meta field. 3006 * 3007 * @access private 3008 * @since 6.3.2 3009 */ 3010 function _wp_footnotes_kses_init_filters() { 3011 add_filter( 'sanitize_post_meta_footnotes', '_wp_filter_post_meta_footnotes' ); 3012 } 3013 3014 /** 3015 * Removes the filters for footnotes meta field. 3016 * 3017 * @access private 3018 * @since 6.3.2 3019 */ 3020 function _wp_footnotes_remove_filters() { 3021 remove_filter( 'sanitize_post_meta_footnotes', '_wp_filter_post_meta_footnotes' ); 3022 } 3023 3024 /** 3025 * Registers the filter of footnotes meta field if the user does not have `unfiltered_html` capability. 3026 * 3027 * @access private 3028 * @since 6.3.2 3029 */ 3030 function _wp_footnotes_kses_init() { 3031 _wp_footnotes_remove_filters(); 3032 if ( ! current_user_can( 'unfiltered_html' ) ) { 3033 _wp_footnotes_kses_init_filters(); 3034 } 3035 } 3036 3037 /** 3038 * Initializes the filters for footnotes meta field when imported data should be filtered. 3039 * 3040 * This filter is the last one being executed on {@see 'force_filtered_html_on_import'}. 3041 * If the input of the filter is true, it means we are in an import situation and should 3042 * enable kses, independently of the user capabilities. So in that case we call 3043 * _wp_footnotes_kses_init_filters(). 3044 * 3045 * @access private 3046 * @since 6.3.2 3047 * 3048 * @param string $arg Input argument of the filter. 3049 * @return string Input argument of the filter. 3050 */ 3051 function _wp_footnotes_force_filtered_html_on_import_filter( $arg ) { 3052 // If `force_filtered_html_on_import` is true, we need to init the global styles kses filters. 3053 if ( $arg ) { 3054 _wp_footnotes_kses_init_filters(); 3055 } 3056 return $arg; 3057 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Wed May 6 08:20:15 2026 | Cross-referenced by PHPXref |