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