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