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