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