[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Functions related to registering and parsing blocks. 4 * 5 * @package WordPress 6 * @subpackage Blocks 7 * @since 5.0.0 8 */ 9 10 /** 11 * Removes the block asset's path prefix if provided. 12 * 13 * @since 5.5.0 14 * 15 * @param string $asset_handle_or_path Asset handle or prefixed path. 16 * @return string Path without the prefix or the original value. 17 */ 18 function remove_block_asset_path_prefix( $asset_handle_or_path ) { 19 $path_prefix = 'file:'; 20 if ( ! str_starts_with( $asset_handle_or_path, $path_prefix ) ) { 21 return $asset_handle_or_path; 22 } 23 $path = substr( 24 $asset_handle_or_path, 25 strlen( $path_prefix ) 26 ); 27 if ( str_starts_with( $path, './' ) ) { 28 $path = substr( $path, 2 ); 29 } 30 return $path; 31 } 32 33 /** 34 * Generates the name for an asset based on the name of the block 35 * and the field name provided. 36 * 37 * @since 5.5.0 38 * @since 6.1.0 Added `$index` parameter. 39 * @since 6.5.0 Added support for `viewScriptModule` field. 40 * 41 * @param string $block_name Name of the block. 42 * @param string $field_name Name of the metadata field. 43 * @param int $index Optional. Index of the asset when multiple items passed. 44 * Default 0. 45 * @return string Generated asset name for the block's field. 46 */ 47 function generate_block_asset_handle( $block_name, $field_name, $index = 0 ) { 48 if ( str_starts_with( $block_name, 'core/' ) ) { 49 $asset_handle = str_replace( 'core/', 'wp-block-', $block_name ); 50 if ( str_starts_with( $field_name, 'editor' ) ) { 51 $asset_handle .= '-editor'; 52 } 53 if ( str_starts_with( $field_name, 'view' ) ) { 54 $asset_handle .= '-view'; 55 } 56 if ( str_ends_with( strtolower( $field_name ), 'scriptmodule' ) ) { 57 $asset_handle .= '-script-module'; 58 } 59 if ( $index > 0 ) { 60 $asset_handle .= '-' . ( $index + 1 ); 61 } 62 return $asset_handle; 63 } 64 65 $field_mappings = array( 66 'editorScript' => 'editor-script', 67 'editorStyle' => 'editor-style', 68 'script' => 'script', 69 'style' => 'style', 70 'viewScript' => 'view-script', 71 'viewScriptModule' => 'view-script-module', 72 'viewStyle' => 'view-style', 73 ); 74 $asset_handle = str_replace( '/', '-', $block_name ) . 75 '-' . $field_mappings[ $field_name ]; 76 if ( $index > 0 ) { 77 $asset_handle .= '-' . ( $index + 1 ); 78 } 79 return $asset_handle; 80 } 81 82 /** 83 * Gets the URL to a block asset. 84 * 85 * @since 6.4.0 86 * 87 * @param string $path A normalized path to a block asset. 88 * @return string|false The URL to the block asset or false on failure. 89 */ 90 function get_block_asset_url( $path ) { 91 if ( empty( $path ) ) { 92 return false; 93 } 94 95 // Path needs to be normalized to work in Windows env. 96 static $wpinc_path_norm = ''; 97 if ( ! $wpinc_path_norm ) { 98 $wpinc_path_norm = wp_normalize_path( realpath( ABSPATH . WPINC ) ); 99 } 100 101 if ( str_starts_with( $path, $wpinc_path_norm ) ) { 102 return includes_url( str_replace( $wpinc_path_norm, '', $path ) ); 103 } 104 105 static $template_paths_norm = array(); 106 107 $template = get_template(); 108 if ( ! isset( $template_paths_norm[ $template ] ) ) { 109 $template_paths_norm[ $template ] = wp_normalize_path( realpath( get_template_directory() ) ); 110 } 111 112 if ( str_starts_with( $path, trailingslashit( $template_paths_norm[ $template ] ) ) ) { 113 return get_theme_file_uri( str_replace( $template_paths_norm[ $template ], '', $path ) ); 114 } 115 116 if ( is_child_theme() ) { 117 $stylesheet = get_stylesheet(); 118 if ( ! isset( $template_paths_norm[ $stylesheet ] ) ) { 119 $template_paths_norm[ $stylesheet ] = wp_normalize_path( realpath( get_stylesheet_directory() ) ); 120 } 121 122 if ( str_starts_with( $path, trailingslashit( $template_paths_norm[ $stylesheet ] ) ) ) { 123 return get_theme_file_uri( str_replace( $template_paths_norm[ $stylesheet ], '', $path ) ); 124 } 125 } 126 127 return plugins_url( basename( $path ), $path ); 128 } 129 130 /** 131 * Finds a script module ID for the selected block metadata field. It detects 132 * when a path to file was provided and optionally finds a corresponding asset 133 * file with details necessary to register the script module under with an 134 * automatically generated module ID. It returns unprocessed script module 135 * ID otherwise. 136 * 137 * @since 6.5.0 138 * 139 * @param array $metadata Block metadata. 140 * @param string $field_name Field name to pick from metadata. 141 * @param int $index Optional. Index of the script module ID to register when multiple 142 * items passed. Default 0. 143 * @return string|false Script module ID or false on failure. 144 */ 145 function register_block_script_module_id( $metadata, $field_name, $index = 0 ) { 146 if ( empty( $metadata[ $field_name ] ) ) { 147 return false; 148 } 149 150 $module_id = $metadata[ $field_name ]; 151 if ( is_array( $module_id ) ) { 152 if ( empty( $module_id[ $index ] ) ) { 153 return false; 154 } 155 $module_id = $module_id[ $index ]; 156 } 157 158 $module_path = remove_block_asset_path_prefix( $module_id ); 159 if ( $module_id === $module_path ) { 160 return $module_id; 161 } 162 163 $path = dirname( $metadata['file'] ); 164 $module_asset_raw_path = $path . '/' . substr_replace( $module_path, '.asset.php', - strlen( '.js' ) ); 165 $module_id = generate_block_asset_handle( $metadata['name'], $field_name, $index ); 166 $module_asset_path = wp_normalize_path( 167 realpath( $module_asset_raw_path ) 168 ); 169 170 $module_path_norm = wp_normalize_path( realpath( $path . '/' . $module_path ) ); 171 $module_uri = get_block_asset_url( $module_path_norm ); 172 173 $module_asset = ! empty( $module_asset_path ) ? require $module_asset_path : array(); 174 $module_dependencies = isset( $module_asset['dependencies'] ) ? $module_asset['dependencies'] : array(); 175 $block_version = isset( $metadata['version'] ) ? $metadata['version'] : false; 176 $module_version = isset( $module_asset['version'] ) ? $module_asset['version'] : $block_version; 177 178 wp_register_script_module( 179 $module_id, 180 $module_uri, 181 $module_dependencies, 182 $module_version 183 ); 184 185 return $module_id; 186 } 187 188 /** 189 * Finds a script handle for the selected block metadata field. It detects 190 * when a path to file was provided and optionally finds a corresponding asset 191 * file with details necessary to register the script under automatically 192 * generated handle name. It returns unprocessed script handle otherwise. 193 * 194 * @since 5.5.0 195 * @since 6.1.0 Added `$index` parameter. 196 * @since 6.5.0 The asset file is optional. Added script handle support in the asset file. 197 * 198 * @param array $metadata Block metadata. 199 * @param string $field_name Field name to pick from metadata. 200 * @param int $index Optional. Index of the script to register when multiple items passed. 201 * Default 0. 202 * @return string|false Script handle provided directly or created through 203 * script's registration, or false on failure. 204 */ 205 function register_block_script_handle( $metadata, $field_name, $index = 0 ) { 206 if ( empty( $metadata[ $field_name ] ) ) { 207 return false; 208 } 209 210 $script_handle_or_path = $metadata[ $field_name ]; 211 if ( is_array( $script_handle_or_path ) ) { 212 if ( empty( $script_handle_or_path[ $index ] ) ) { 213 return false; 214 } 215 $script_handle_or_path = $script_handle_or_path[ $index ]; 216 } 217 218 $script_path = remove_block_asset_path_prefix( $script_handle_or_path ); 219 if ( $script_handle_or_path === $script_path ) { 220 return $script_handle_or_path; 221 } 222 223 $path = dirname( $metadata['file'] ); 224 $script_asset_raw_path = $path . '/' . substr_replace( $script_path, '.asset.php', - strlen( '.js' ) ); 225 $script_asset_path = wp_normalize_path( 226 realpath( $script_asset_raw_path ) 227 ); 228 229 // Asset file for blocks is optional. See https://core.trac.wordpress.org/ticket/60460. 230 $script_asset = ! empty( $script_asset_path ) ? require $script_asset_path : array(); 231 $script_handle = isset( $script_asset['handle'] ) ? 232 $script_asset['handle'] : 233 generate_block_asset_handle( $metadata['name'], $field_name, $index ); 234 if ( wp_script_is( $script_handle, 'registered' ) ) { 235 return $script_handle; 236 } 237 238 $script_path_norm = wp_normalize_path( realpath( $path . '/' . $script_path ) ); 239 $script_uri = get_block_asset_url( $script_path_norm ); 240 $script_dependencies = isset( $script_asset['dependencies'] ) ? $script_asset['dependencies'] : array(); 241 $block_version = isset( $metadata['version'] ) ? $metadata['version'] : false; 242 $script_version = isset( $script_asset['version'] ) ? $script_asset['version'] : $block_version; 243 $script_args = array(); 244 if ( 'viewScript' === $field_name && $script_uri ) { 245 $script_args['strategy'] = 'defer'; 246 } 247 248 $result = wp_register_script( 249 $script_handle, 250 $script_uri, 251 $script_dependencies, 252 $script_version, 253 $script_args 254 ); 255 if ( ! $result ) { 256 return false; 257 } 258 259 if ( ! empty( $metadata['textdomain'] ) && in_array( 'wp-i18n', $script_dependencies, true ) ) { 260 wp_set_script_translations( $script_handle, $metadata['textdomain'] ); 261 } 262 263 return $script_handle; 264 } 265 266 /** 267 * Finds a style handle for the block metadata field. It detects when a path 268 * to file was provided and registers the style under automatically 269 * generated handle name. It returns unprocessed style handle otherwise. 270 * 271 * @since 5.5.0 272 * @since 6.1.0 Added `$index` parameter. 273 * 274 * @param array $metadata Block metadata. 275 * @param string $field_name Field name to pick from metadata. 276 * @param int $index Optional. Index of the style to register when multiple items passed. 277 * Default 0. 278 * @return string|false Style handle provided directly or created through 279 * style's registration, or false on failure. 280 */ 281 function register_block_style_handle( $metadata, $field_name, $index = 0 ) { 282 if ( empty( $metadata[ $field_name ] ) ) { 283 return false; 284 } 285 286 $style_handle = $metadata[ $field_name ]; 287 if ( is_array( $style_handle ) ) { 288 if ( empty( $style_handle[ $index ] ) ) { 289 return false; 290 } 291 $style_handle = $style_handle[ $index ]; 292 } 293 294 $style_handle_name = generate_block_asset_handle( $metadata['name'], $field_name, $index ); 295 // If the style handle is already registered, skip re-registering. 296 if ( wp_style_is( $style_handle_name, 'registered' ) ) { 297 return $style_handle_name; 298 } 299 300 static $wpinc_path_norm = ''; 301 if ( ! $wpinc_path_norm ) { 302 $wpinc_path_norm = wp_normalize_path( realpath( ABSPATH . WPINC ) ); 303 } 304 305 $is_core_block = isset( $metadata['file'] ) && str_starts_with( $metadata['file'], $wpinc_path_norm ); 306 // Skip registering individual styles for each core block when a bundled version provided. 307 if ( $is_core_block && ! wp_should_load_separate_core_block_assets() ) { 308 return false; 309 } 310 311 $style_path = remove_block_asset_path_prefix( $style_handle ); 312 $is_style_handle = $style_handle === $style_path; 313 // Allow only passing style handles for core blocks. 314 if ( $is_core_block && ! $is_style_handle ) { 315 return false; 316 } 317 // Return the style handle unless it's the first item for every core block that requires special treatment. 318 if ( $is_style_handle && ! ( $is_core_block && 0 === $index ) ) { 319 return $style_handle; 320 } 321 322 // Check whether styles should have a ".min" suffix or not. 323 $suffix = SCRIPT_DEBUG ? '' : '.min'; 324 if ( $is_core_block ) { 325 $style_path = ( 'editorStyle' === $field_name ) ? "editor{$suffix}.css" : "style{$suffix}.css"; 326 } 327 328 $style_path_norm = wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . $style_path ) ); 329 $style_uri = get_block_asset_url( $style_path_norm ); 330 331 $version = ! $is_core_block && isset( $metadata['version'] ) ? $metadata['version'] : false; 332 $result = wp_register_style( 333 $style_handle_name, 334 $style_uri, 335 array(), 336 $version 337 ); 338 if ( ! $result ) { 339 return false; 340 } 341 342 if ( $style_uri ) { 343 wp_style_add_data( $style_handle_name, 'path', $style_path_norm ); 344 345 if ( $is_core_block ) { 346 $rtl_file = str_replace( "{$suffix}.css", "-rtl{$suffix}.css", $style_path_norm ); 347 } else { 348 $rtl_file = str_replace( '.css', '-rtl.css', $style_path_norm ); 349 } 350 351 if ( is_rtl() && file_exists( $rtl_file ) ) { 352 wp_style_add_data( $style_handle_name, 'rtl', 'replace' ); 353 wp_style_add_data( $style_handle_name, 'suffix', $suffix ); 354 wp_style_add_data( $style_handle_name, 'path', $rtl_file ); 355 } 356 } 357 358 return $style_handle_name; 359 } 360 361 /** 362 * Gets i18n schema for block's metadata read from `block.json` file. 363 * 364 * @since 5.9.0 365 * 366 * @return object The schema for block's metadata. 367 */ 368 function get_block_metadata_i18n_schema() { 369 static $i18n_block_schema; 370 371 if ( ! isset( $i18n_block_schema ) ) { 372 $i18n_block_schema = wp_json_file_decode( __DIR__ . '/block-i18n.json' ); 373 } 374 375 return $i18n_block_schema; 376 } 377 378 /** 379 * Registers a block type from the metadata stored in the `block.json` file. 380 * 381 * @since 5.5.0 382 * @since 5.7.0 Added support for `textdomain` field and i18n handling for all translatable fields. 383 * @since 5.9.0 Added support for `variations` and `viewScript` fields. 384 * @since 6.1.0 Added support for `render` field. 385 * @since 6.3.0 Added `selectors` field. 386 * @since 6.4.0 Added support for `blockHooks` field. 387 * @since 6.5.0 Added support for `allowedBlocks`, `viewScriptModule`, and `viewStyle` fields. 388 * 389 * @param string $file_or_folder Path to the JSON file with metadata definition for 390 * the block or path to the folder where the `block.json` file is located. 391 * If providing the path to a JSON file, the filename must end with `block.json`. 392 * @param array $args Optional. Array of block type arguments. Accepts any public property 393 * of `WP_Block_Type`. See WP_Block_Type::__construct() for information 394 * on accepted arguments. Default empty array. 395 * @return WP_Block_Type|false The registered block type on success, or false on failure. 396 */ 397 function register_block_type_from_metadata( $file_or_folder, $args = array() ) { 398 /* 399 * Get an array of metadata from a PHP file. 400 * This improves performance for core blocks as it's only necessary to read a single PHP file 401 * instead of reading a JSON file per-block, and then decoding from JSON to PHP. 402 * Using a static variable ensures that the metadata is only read once per request. 403 */ 404 static $core_blocks_meta; 405 if ( ! $core_blocks_meta ) { 406 $core_blocks_meta = require ABSPATH . WPINC . '/blocks/blocks-json.php'; 407 } 408 409 $metadata_file = ( ! str_ends_with( $file_or_folder, 'block.json' ) ) ? 410 trailingslashit( $file_or_folder ) . 'block.json' : 411 $file_or_folder; 412 413 $is_core_block = str_starts_with( $file_or_folder, ABSPATH . WPINC ); 414 // If the block is not a core block, the metadata file must exist. 415 $metadata_file_exists = $is_core_block || file_exists( $metadata_file ); 416 if ( ! $metadata_file_exists && empty( $args['name'] ) ) { 417 return false; 418 } 419 420 // Try to get metadata from the static cache for core blocks. 421 $metadata = array(); 422 if ( $is_core_block ) { 423 $core_block_name = str_replace( ABSPATH . WPINC . '/blocks/', '', $file_or_folder ); 424 if ( ! empty( $core_blocks_meta[ $core_block_name ] ) ) { 425 $metadata = $core_blocks_meta[ $core_block_name ]; 426 } 427 } 428 429 // If metadata is not found in the static cache, read it from the file. 430 if ( $metadata_file_exists && empty( $metadata ) ) { 431 $metadata = wp_json_file_decode( $metadata_file, array( 'associative' => true ) ); 432 } 433 434 if ( ! is_array( $metadata ) || ( empty( $metadata['name'] ) && empty( $args['name'] ) ) ) { 435 return false; 436 } 437 438 $metadata['file'] = $metadata_file_exists ? wp_normalize_path( realpath( $metadata_file ) ) : null; 439 440 /** 441 * Filters the metadata provided for registering a block type. 442 * 443 * @since 5.7.0 444 * 445 * @param array $metadata Metadata for registering a block type. 446 */ 447 $metadata = apply_filters( 'block_type_metadata', $metadata ); 448 449 // Add `style` and `editor_style` for core blocks if missing. 450 if ( ! empty( $metadata['name'] ) && str_starts_with( $metadata['name'], 'core/' ) ) { 451 $block_name = str_replace( 'core/', '', $metadata['name'] ); 452 453 if ( ! isset( $metadata['style'] ) ) { 454 $metadata['style'] = "wp-block-$block_name"; 455 } 456 if ( current_theme_supports( 'wp-block-styles' ) && wp_should_load_separate_core_block_assets() ) { 457 $metadata['style'] = (array) $metadata['style']; 458 $metadata['style'][] = "wp-block-{$block_name}-theme"; 459 } 460 if ( ! isset( $metadata['editorStyle'] ) ) { 461 $metadata['editorStyle'] = "wp-block-{$block_name}-editor"; 462 } 463 } 464 465 $settings = array(); 466 $property_mappings = array( 467 'apiVersion' => 'api_version', 468 'name' => 'name', 469 'title' => 'title', 470 'category' => 'category', 471 'parent' => 'parent', 472 'ancestor' => 'ancestor', 473 'icon' => 'icon', 474 'description' => 'description', 475 'keywords' => 'keywords', 476 'attributes' => 'attributes', 477 'providesContext' => 'provides_context', 478 'usesContext' => 'uses_context', 479 'selectors' => 'selectors', 480 'supports' => 'supports', 481 'styles' => 'styles', 482 'variations' => 'variations', 483 'example' => 'example', 484 'allowedBlocks' => 'allowed_blocks', 485 ); 486 $textdomain = ! empty( $metadata['textdomain'] ) ? $metadata['textdomain'] : null; 487 $i18n_schema = get_block_metadata_i18n_schema(); 488 489 foreach ( $property_mappings as $key => $mapped_key ) { 490 if ( isset( $metadata[ $key ] ) ) { 491 $settings[ $mapped_key ] = $metadata[ $key ]; 492 if ( $metadata_file_exists && $textdomain && isset( $i18n_schema->$key ) ) { 493 $settings[ $mapped_key ] = translate_settings_using_i18n_schema( $i18n_schema->$key, $settings[ $key ], $textdomain ); 494 } 495 } 496 } 497 498 if ( ! empty( $metadata['render'] ) ) { 499 $template_path = wp_normalize_path( 500 realpath( 501 dirname( $metadata['file'] ) . '/' . 502 remove_block_asset_path_prefix( $metadata['render'] ) 503 ) 504 ); 505 if ( $template_path ) { 506 /** 507 * Renders the block on the server. 508 * 509 * @since 6.1.0 510 * 511 * @param array $attributes Block attributes. 512 * @param string $content Block default content. 513 * @param WP_Block $block Block instance. 514 * 515 * @return string Returns the block content. 516 */ 517 $settings['render_callback'] = static function ( $attributes, $content, $block ) use ( $template_path ) { 518 ob_start(); 519 require $template_path; 520 return ob_get_clean(); 521 }; 522 } 523 } 524 525 $settings = array_merge( $settings, $args ); 526 527 $script_fields = array( 528 'editorScript' => 'editor_script_handles', 529 'script' => 'script_handles', 530 'viewScript' => 'view_script_handles', 531 ); 532 foreach ( $script_fields as $metadata_field_name => $settings_field_name ) { 533 if ( ! empty( $settings[ $metadata_field_name ] ) ) { 534 $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; 535 } 536 if ( ! empty( $metadata[ $metadata_field_name ] ) ) { 537 $scripts = $metadata[ $metadata_field_name ]; 538 $processed_scripts = array(); 539 if ( is_array( $scripts ) ) { 540 for ( $index = 0; $index < count( $scripts ); $index++ ) { 541 $result = register_block_script_handle( 542 $metadata, 543 $metadata_field_name, 544 $index 545 ); 546 if ( $result ) { 547 $processed_scripts[] = $result; 548 } 549 } 550 } else { 551 $result = register_block_script_handle( 552 $metadata, 553 $metadata_field_name 554 ); 555 if ( $result ) { 556 $processed_scripts[] = $result; 557 } 558 } 559 $settings[ $settings_field_name ] = $processed_scripts; 560 } 561 } 562 563 $module_fields = array( 564 'viewScriptModule' => 'view_script_module_ids', 565 ); 566 foreach ( $module_fields as $metadata_field_name => $settings_field_name ) { 567 if ( ! empty( $settings[ $metadata_field_name ] ) ) { 568 $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; 569 } 570 if ( ! empty( $metadata[ $metadata_field_name ] ) ) { 571 $modules = $metadata[ $metadata_field_name ]; 572 $processed_modules = array(); 573 if ( is_array( $modules ) ) { 574 for ( $index = 0; $index < count( $modules ); $index++ ) { 575 $result = register_block_script_module_id( 576 $metadata, 577 $metadata_field_name, 578 $index 579 ); 580 if ( $result ) { 581 $processed_modules[] = $result; 582 } 583 } 584 } else { 585 $result = register_block_script_module_id( 586 $metadata, 587 $metadata_field_name 588 ); 589 if ( $result ) { 590 $processed_modules[] = $result; 591 } 592 } 593 $settings[ $settings_field_name ] = $processed_modules; 594 } 595 } 596 597 $style_fields = array( 598 'editorStyle' => 'editor_style_handles', 599 'style' => 'style_handles', 600 'viewStyle' => 'view_style_handles', 601 ); 602 foreach ( $style_fields as $metadata_field_name => $settings_field_name ) { 603 if ( ! empty( $settings[ $metadata_field_name ] ) ) { 604 $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; 605 } 606 if ( ! empty( $metadata[ $metadata_field_name ] ) ) { 607 $styles = $metadata[ $metadata_field_name ]; 608 $processed_styles = array(); 609 if ( is_array( $styles ) ) { 610 for ( $index = 0; $index < count( $styles ); $index++ ) { 611 $result = register_block_style_handle( 612 $metadata, 613 $metadata_field_name, 614 $index 615 ); 616 if ( $result ) { 617 $processed_styles[] = $result; 618 } 619 } 620 } else { 621 $result = register_block_style_handle( 622 $metadata, 623 $metadata_field_name 624 ); 625 if ( $result ) { 626 $processed_styles[] = $result; 627 } 628 } 629 $settings[ $settings_field_name ] = $processed_styles; 630 } 631 } 632 633 if ( ! empty( $metadata['blockHooks'] ) ) { 634 /** 635 * Map camelCased position string (from block.json) to snake_cased block type position. 636 * 637 * @var array 638 */ 639 $position_mappings = array( 640 'before' => 'before', 641 'after' => 'after', 642 'firstChild' => 'first_child', 643 'lastChild' => 'last_child', 644 ); 645 646 $settings['block_hooks'] = array(); 647 foreach ( $metadata['blockHooks'] as $anchor_block_name => $position ) { 648 // Avoid infinite recursion (hooking to itself). 649 if ( $metadata['name'] === $anchor_block_name ) { 650 _doing_it_wrong( 651 __METHOD__, 652 __( 'Cannot hook block to itself.' ), 653 '6.4.0' 654 ); 655 continue; 656 } 657 658 if ( ! isset( $position_mappings[ $position ] ) ) { 659 continue; 660 } 661 662 $settings['block_hooks'][ $anchor_block_name ] = $position_mappings[ $position ]; 663 } 664 } 665 666 /** 667 * Filters the settings determined from the block type metadata. 668 * 669 * @since 5.7.0 670 * 671 * @param array $settings Array of determined settings for registering a block type. 672 * @param array $metadata Metadata provided for registering a block type. 673 */ 674 $settings = apply_filters( 'block_type_metadata_settings', $settings, $metadata ); 675 676 $metadata['name'] = ! empty( $settings['name'] ) ? $settings['name'] : $metadata['name']; 677 678 return WP_Block_Type_Registry::get_instance()->register( 679 $metadata['name'], 680 $settings 681 ); 682 } 683 684 /** 685 * Registers a block type. The recommended way is to register a block type using 686 * the metadata stored in the `block.json` file. 687 * 688 * @since 5.0.0 689 * @since 5.8.0 First parameter now accepts a path to the `block.json` file. 690 * 691 * @param string|WP_Block_Type $block_type Block type name including namespace, or alternatively 692 * a path to the JSON file with metadata definition for the block, 693 * or a path to the folder where the `block.json` file is located, 694 * or a complete WP_Block_Type instance. 695 * In case a WP_Block_Type is provided, the $args parameter will be ignored. 696 * @param array $args Optional. Array of block type arguments. Accepts any public property 697 * of `WP_Block_Type`. See WP_Block_Type::__construct() for information 698 * on accepted arguments. Default empty array. 699 * 700 * @return WP_Block_Type|false The registered block type on success, or false on failure. 701 */ 702 function register_block_type( $block_type, $args = array() ) { 703 if ( is_string( $block_type ) && file_exists( $block_type ) ) { 704 return register_block_type_from_metadata( $block_type, $args ); 705 } 706 707 return WP_Block_Type_Registry::get_instance()->register( $block_type, $args ); 708 } 709 710 /** 711 * Unregisters a block type. 712 * 713 * @since 5.0.0 714 * 715 * @param string|WP_Block_Type $name Block type name including namespace, or alternatively 716 * a complete WP_Block_Type instance. 717 * @return WP_Block_Type|false The unregistered block type on success, or false on failure. 718 */ 719 function unregister_block_type( $name ) { 720 return WP_Block_Type_Registry::get_instance()->unregister( $name ); 721 } 722 723 /** 724 * Determines whether a post or content string has blocks. 725 * 726 * This test optimizes for performance rather than strict accuracy, detecting 727 * the pattern of a block but not validating its structure. For strict accuracy, 728 * you should use the block parser on post content. 729 * 730 * @since 5.0.0 731 * 732 * @see parse_blocks() 733 * 734 * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. 735 * Defaults to global $post. 736 * @return bool Whether the post has blocks. 737 */ 738 function has_blocks( $post = null ) { 739 if ( ! is_string( $post ) ) { 740 $wp_post = get_post( $post ); 741 742 if ( ! $wp_post instanceof WP_Post ) { 743 return false; 744 } 745 746 $post = $wp_post->post_content; 747 } 748 749 return str_contains( (string) $post, '<!-- wp:' ); 750 } 751 752 /** 753 * Determines whether a $post or a string contains a specific block type. 754 * 755 * This test optimizes for performance rather than strict accuracy, detecting 756 * whether the block type exists but not validating its structure and not checking 757 * synced patterns (formerly called reusable blocks). For strict accuracy, 758 * you should use the block parser on post content. 759 * 760 * @since 5.0.0 761 * 762 * @see parse_blocks() 763 * 764 * @param string $block_name Full block type to look for. 765 * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. 766 * Defaults to global $post. 767 * @return bool Whether the post content contains the specified block. 768 */ 769 function has_block( $block_name, $post = null ) { 770 if ( ! has_blocks( $post ) ) { 771 return false; 772 } 773 774 if ( ! is_string( $post ) ) { 775 $wp_post = get_post( $post ); 776 if ( $wp_post instanceof WP_Post ) { 777 $post = $wp_post->post_content; 778 } 779 } 780 781 /* 782 * Normalize block name to include namespace, if provided as non-namespaced. 783 * This matches behavior for WordPress 5.0.0 - 5.3.0 in matching blocks by 784 * their serialized names. 785 */ 786 if ( ! str_contains( $block_name, '/' ) ) { 787 $block_name = 'core/' . $block_name; 788 } 789 790 // Test for existence of block by its fully qualified name. 791 $has_block = str_contains( $post, '<!-- wp:' . $block_name . ' ' ); 792 793 if ( ! $has_block ) { 794 /* 795 * If the given block name would serialize to a different name, test for 796 * existence by the serialized form. 797 */ 798 $serialized_block_name = strip_core_block_namespace( $block_name ); 799 if ( $serialized_block_name !== $block_name ) { 800 $has_block = str_contains( $post, '<!-- wp:' . $serialized_block_name . ' ' ); 801 } 802 } 803 804 return $has_block; 805 } 806 807 /** 808 * Returns an array of the names of all registered dynamic block types. 809 * 810 * @since 5.0.0 811 * 812 * @return string[] Array of dynamic block names. 813 */ 814 function get_dynamic_block_names() { 815 $dynamic_block_names = array(); 816 817 $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); 818 foreach ( $block_types as $block_type ) { 819 if ( $block_type->is_dynamic() ) { 820 $dynamic_block_names[] = $block_type->name; 821 } 822 } 823 824 return $dynamic_block_names; 825 } 826 827 /** 828 * Retrieves block types hooked into the given block, grouped by anchor block type and the relative position. 829 * 830 * @since 6.4.0 831 * 832 * @return array[] Array of block types grouped by anchor block type and the relative position. 833 */ 834 function get_hooked_blocks() { 835 $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); 836 $hooked_blocks = array(); 837 foreach ( $block_types as $block_type ) { 838 if ( ! ( $block_type instanceof WP_Block_Type ) || ! is_array( $block_type->block_hooks ) ) { 839 continue; 840 } 841 foreach ( $block_type->block_hooks as $anchor_block_type => $relative_position ) { 842 if ( ! isset( $hooked_blocks[ $anchor_block_type ] ) ) { 843 $hooked_blocks[ $anchor_block_type ] = array(); 844 } 845 if ( ! isset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) ) { 846 $hooked_blocks[ $anchor_block_type ][ $relative_position ] = array(); 847 } 848 $hooked_blocks[ $anchor_block_type ][ $relative_position ][] = $block_type->name; 849 } 850 } 851 852 return $hooked_blocks; 853 } 854 855 /** 856 * Returns the markup for blocks hooked to the given anchor block in a specific relative position. 857 * 858 * @since 6.5.0 859 * @access private 860 * 861 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 862 * @param string $relative_position The relative position of the hooked blocks. 863 * Can be one of 'before', 'after', 'first_child', or 'last_child'. 864 * @param array $hooked_blocks An array of hooked block types, grouped by anchor block and relative position. 865 * @param WP_Block_Template|array $context The block template, template part, or pattern that the anchor block belongs to. 866 * @return string 867 */ 868 function insert_hooked_blocks( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) { 869 $anchor_block_type = $parsed_anchor_block['blockName']; 870 $hooked_block_types = isset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) 871 ? $hooked_blocks[ $anchor_block_type ][ $relative_position ] 872 : array(); 873 874 /** 875 * Filters the list of hooked block types for a given anchor block type and relative position. 876 * 877 * @since 6.4.0 878 * 879 * @param string[] $hooked_block_types The list of hooked block types. 880 * @param string $relative_position The relative position of the hooked blocks. 881 * Can be one of 'before', 'after', 'first_child', or 'last_child'. 882 * @param string $anchor_block_type The anchor block type. 883 * @param WP_Block_Template|WP_Post|array $context The block template, template part, `wp_navigation` post type, 884 * or pattern that the anchor block belongs to. 885 */ 886 $hooked_block_types = apply_filters( 'hooked_block_types', $hooked_block_types, $relative_position, $anchor_block_type, $context ); 887 888 $markup = ''; 889 foreach ( $hooked_block_types as $hooked_block_type ) { 890 $parsed_hooked_block = array( 891 'blockName' => $hooked_block_type, 892 'attrs' => array(), 893 'innerBlocks' => array(), 894 'innerContent' => array(), 895 ); 896 897 /** 898 * Filters the parsed block array for a given hooked block. 899 * 900 * @since 6.5.0 901 * 902 * @param array|null $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block. 903 * @param string $hooked_block_type The hooked block type name. 904 * @param string $relative_position The relative position of the hooked block. 905 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 906 * @param WP_Block_Template|WP_Post|array $context The block template, template part, `wp_navigation` post type, 907 * or pattern that the anchor block belongs to. 908 */ 909 $parsed_hooked_block = apply_filters( 'hooked_block', $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 910 911 /** 912 * Filters the parsed block array for a given hooked block. 913 * 914 * The dynamic portion of the hook name, `$hooked_block_type`, refers to the block type name of the specific hooked block. 915 * 916 * @since 6.5.0 917 * 918 * @param array|null $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block. 919 * @param string $hooked_block_type The hooked block type name. 920 * @param string $relative_position The relative position of the hooked block. 921 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 922 * @param WP_Block_Template|WP_Post|array $context The block template, template part, `wp_navigation` post type, 923 * or pattern that the anchor block belongs to. 924 */ 925 $parsed_hooked_block = apply_filters( "hooked_block_{$hooked_block_type}", $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 926 927 if ( null === $parsed_hooked_block ) { 928 continue; 929 } 930 931 // It's possible that the filter returned a block of a different type, so we explicitly 932 // look for the original `$hooked_block_type` in the `ignoredHookedBlocks` metadata. 933 if ( 934 ! isset( $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] ) || 935 ! in_array( $hooked_block_type, $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'], true ) 936 ) { 937 $markup .= serialize_block( $parsed_hooked_block ); 938 } 939 } 940 941 return $markup; 942 } 943 944 /** 945 * Adds a list of hooked block types to an anchor block's ignored hooked block types. 946 * 947 * This function is meant for internal use only. 948 * 949 * @since 6.5.0 950 * @access private 951 * 952 * @param array $parsed_anchor_block The anchor block, in parsed block array format. 953 * @param string $relative_position The relative position of the hooked blocks. 954 * Can be one of 'before', 'after', 'first_child', or 'last_child'. 955 * @param array $hooked_blocks An array of hooked block types, grouped by anchor block and relative position. 956 * @param WP_Block_Template|array $context The block template, template part, or pattern that the anchor block belongs to. 957 * @return string An empty string. 958 */ 959 function set_ignored_hooked_blocks_metadata( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) { 960 $anchor_block_type = $parsed_anchor_block['blockName']; 961 $hooked_block_types = isset( $hooked_blocks[ $anchor_block_type ][ $relative_position ] ) 962 ? $hooked_blocks[ $anchor_block_type ][ $relative_position ] 963 : array(); 964 965 /** This filter is documented in wp-includes/blocks.php */ 966 $hooked_block_types = apply_filters( 'hooked_block_types', $hooked_block_types, $relative_position, $anchor_block_type, $context ); 967 if ( empty( $hooked_block_types ) ) { 968 return ''; 969 } 970 971 foreach ( $hooked_block_types as $index => $hooked_block_type ) { 972 $parsed_hooked_block = array( 973 'blockName' => $hooked_block_type, 974 'attrs' => array(), 975 'innerBlocks' => array(), 976 'innerContent' => array(), 977 ); 978 979 /** This filter is documented in wp-includes/blocks.php */ 980 $parsed_hooked_block = apply_filters( 'hooked_block', $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 981 982 /** This filter is documented in wp-includes/blocks.php */ 983 $parsed_hooked_block = apply_filters( "hooked_block_{$hooked_block_type}", $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ); 984 985 if ( null === $parsed_hooked_block ) { 986 unset( $hooked_block_types[ $index ] ); 987 } 988 } 989 990 $previously_ignored_hooked_blocks = isset( $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] ) 991 ? $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] 992 : array(); 993 994 $parsed_anchor_block['attrs']['metadata']['ignoredHookedBlocks'] = array_unique( 995 array_merge( 996 $previously_ignored_hooked_blocks, 997 $hooked_block_types 998 ) 999 ); 1000 1001 // Markup for the hooked blocks has already been created (in `insert_hooked_blocks`). 1002 return ''; 1003 } 1004 1005 /** 1006 * Returns a function that injects the theme attribute into, and hooked blocks before, a given block. 1007 * 1008 * The returned function can be used as `$pre_callback` argument to `traverse_and_serialize_block(s)`, 1009 * where it will inject the `theme` attribute into all Template Part blocks, and prepend the markup for 1010 * any blocks hooked `before` the given block and as its parent's `first_child`, respectively. 1011 * 1012 * This function is meant for internal use only. 1013 * 1014 * @since 6.4.0 1015 * @since 6.5.0 Added $callback argument. 1016 * @access private 1017 * 1018 * @param array $hooked_blocks An array of blocks hooked to another given block. 1019 * @param WP_Block_Template|WP_Post|array $context A block template, template part, `wp_navigation` post object, 1020 * or pattern that the blocks belong to. 1021 * @param callable $callback A function that will be called for each block to generate 1022 * the markup for a given list of blocks that are hooked to it. 1023 * Default: 'insert_hooked_blocks'. 1024 * @return callable A function that returns the serialized markup for the given block, 1025 * including the markup for any hooked blocks before it. 1026 */ 1027 function make_before_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) { 1028 /** 1029 * Injects hooked blocks before the given block, injects the `theme` attribute into Template Part blocks, and returns the serialized markup. 1030 * 1031 * If the current block is a Template Part block, inject the `theme` attribute. 1032 * Furthermore, prepend the markup for any blocks hooked `before` the given block and as its parent's 1033 * `first_child`, respectively, to the serialized markup for the given block. 1034 * 1035 * @param array $block The block to inject the theme attribute into, and hooked blocks before. Passed by reference. 1036 * @param array $parent_block The parent block of the given block. Passed by reference. Default null. 1037 * @param array $prev The previous sibling block of the given block. Default null. 1038 * @return string The serialized markup for the given block, with the markup for any hooked blocks prepended to it. 1039 */ 1040 return function ( &$block, &$parent_block = null, $prev = null ) use ( $hooked_blocks, $context, $callback ) { 1041 _inject_theme_attribute_in_template_part_block( $block ); 1042 1043 $markup = ''; 1044 1045 if ( $parent_block && ! $prev ) { 1046 // Candidate for first-child insertion. 1047 $markup .= call_user_func_array( 1048 $callback, 1049 array( &$parent_block, 'first_child', $hooked_blocks, $context ) 1050 ); 1051 } 1052 1053 $markup .= call_user_func_array( 1054 $callback, 1055 array( &$block, 'before', $hooked_blocks, $context ) 1056 ); 1057 1058 return $markup; 1059 }; 1060 } 1061 1062 /** 1063 * Returns a function that injects the hooked blocks after a given block. 1064 * 1065 * The returned function can be used as `$post_callback` argument to `traverse_and_serialize_block(s)`, 1066 * where it will append the markup for any blocks hooked `after` the given block and as its parent's 1067 * `last_child`, respectively. 1068 * 1069 * This function is meant for internal use only. 1070 * 1071 * @since 6.4.0 1072 * @since 6.5.0 Added $callback argument. 1073 * @access private 1074 * 1075 * @param array $hooked_blocks An array of blocks hooked to another block. 1076 * @param WP_Block_Template|WP_Post|array $context A block template, template part, `wp_navigation` post object, 1077 * or pattern that the blocks belong to. 1078 * @param callable $callback A function that will be called for each block to generate 1079 * the markup for a given list of blocks that are hooked to it. 1080 * Default: 'insert_hooked_blocks'. 1081 * @return callable A function that returns the serialized markup for the given block, 1082 * including the markup for any hooked blocks after it. 1083 */ 1084 function make_after_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) { 1085 /** 1086 * Injects hooked blocks after the given block, and returns the serialized markup. 1087 * 1088 * Append the markup for any blocks hooked `after` the given block and as its parent's 1089 * `last_child`, respectively, to the serialized markup for the given block. 1090 * 1091 * @param array $block The block to inject the hooked blocks after. Passed by reference. 1092 * @param array $parent_block The parent block of the given block. Passed by reference. Default null. 1093 * @param array $next The next sibling block of the given block. Default null. 1094 * @return string The serialized markup for the given block, with the markup for any hooked blocks appended to it. 1095 */ 1096 return function ( &$block, &$parent_block = null, $next = null ) use ( $hooked_blocks, $context, $callback ) { 1097 $markup = call_user_func_array( 1098 $callback, 1099 array( &$block, 'after', $hooked_blocks, $context ) 1100 ); 1101 1102 if ( $parent_block && ! $next ) { 1103 // Candidate for last-child insertion. 1104 $markup .= call_user_func_array( 1105 $callback, 1106 array( &$parent_block, 'last_child', $hooked_blocks, $context ) 1107 ); 1108 } 1109 1110 return $markup; 1111 }; 1112 } 1113 1114 /** 1115 * Given an array of attributes, returns a string in the serialized attributes 1116 * format prepared for post content. 1117 * 1118 * The serialized result is a JSON-encoded string, with unicode escape sequence 1119 * substitution for characters which might otherwise interfere with embedding 1120 * the result in an HTML comment. 1121 * 1122 * This function must produce output that remains in sync with the output of 1123 * the serializeAttributes JavaScript function in the block editor in order 1124 * to ensure consistent operation between PHP and JavaScript. 1125 * 1126 * @since 5.3.1 1127 * 1128 * @param array $block_attributes Attributes object. 1129 * @return string Serialized attributes. 1130 */ 1131 function serialize_block_attributes( $block_attributes ) { 1132 $encoded_attributes = wp_json_encode( $block_attributes, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); 1133 $encoded_attributes = preg_replace( '/--/', '\\u002d\\u002d', $encoded_attributes ); 1134 $encoded_attributes = preg_replace( '/</', '\\u003c', $encoded_attributes ); 1135 $encoded_attributes = preg_replace( '/>/', '\\u003e', $encoded_attributes ); 1136 $encoded_attributes = preg_replace( '/&/', '\\u0026', $encoded_attributes ); 1137 // Regex: /\\"/ 1138 $encoded_attributes = preg_replace( '/\\\\"/', '\\u0022', $encoded_attributes ); 1139 1140 return $encoded_attributes; 1141 } 1142 1143 /** 1144 * Returns the block name to use for serialization. This will remove the default 1145 * "core/" namespace from a block name. 1146 * 1147 * @since 5.3.1 1148 * 1149 * @param string|null $block_name Optional. Original block name. Null if the block name is unknown, 1150 * e.g. Classic blocks have their name set to null. Default null. 1151 * @return string Block name to use for serialization. 1152 */ 1153 function strip_core_block_namespace( $block_name = null ) { 1154 if ( is_string( $block_name ) && str_starts_with( $block_name, 'core/' ) ) { 1155 return substr( $block_name, 5 ); 1156 } 1157 1158 return $block_name; 1159 } 1160 1161 /** 1162 * Returns the content of a block, including comment delimiters. 1163 * 1164 * @since 5.3.1 1165 * 1166 * @param string|null $block_name Block name. Null if the block name is unknown, 1167 * e.g. Classic blocks have their name set to null. 1168 * @param array $block_attributes Block attributes. 1169 * @param string $block_content Block save content. 1170 * @return string Comment-delimited block content. 1171 */ 1172 function get_comment_delimited_block_content( $block_name, $block_attributes, $block_content ) { 1173 if ( is_null( $block_name ) ) { 1174 return $block_content; 1175 } 1176 1177 $serialized_block_name = strip_core_block_namespace( $block_name ); 1178 $serialized_attributes = empty( $block_attributes ) ? '' : serialize_block_attributes( $block_attributes ) . ' '; 1179 1180 if ( empty( $block_content ) ) { 1181 return sprintf( '<!-- wp:%s %s/-->', $serialized_block_name, $serialized_attributes ); 1182 } 1183 1184 return sprintf( 1185 '<!-- wp:%s %s-->%s<!-- /wp:%s -->', 1186 $serialized_block_name, 1187 $serialized_attributes, 1188 $block_content, 1189 $serialized_block_name 1190 ); 1191 } 1192 1193 /** 1194 * Returns the content of a block, including comment delimiters, serializing all 1195 * attributes from the given parsed block. 1196 * 1197 * This should be used when preparing a block to be saved to post content. 1198 * Prefer `render_block` when preparing a block for display. Unlike 1199 * `render_block`, this does not evaluate a block's `render_callback`, and will 1200 * instead preserve the markup as parsed. 1201 * 1202 * @since 5.3.1 1203 * 1204 * @param array $block A representative array of a single parsed block object. See WP_Block_Parser_Block. 1205 * @return string String of rendered HTML. 1206 */ 1207 function serialize_block( $block ) { 1208 $block_content = ''; 1209 1210 $index = 0; 1211 foreach ( $block['innerContent'] as $chunk ) { 1212 $block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] ); 1213 } 1214 1215 if ( ! is_array( $block['attrs'] ) ) { 1216 $block['attrs'] = array(); 1217 } 1218 1219 return get_comment_delimited_block_content( 1220 $block['blockName'], 1221 $block['attrs'], 1222 $block_content 1223 ); 1224 } 1225 1226 /** 1227 * Returns a joined string of the aggregate serialization of the given 1228 * parsed blocks. 1229 * 1230 * @since 5.3.1 1231 * 1232 * @param array[] $blocks An array of representative arrays of parsed block objects. See serialize_block(). 1233 * @return string String of rendered HTML. 1234 */ 1235 function serialize_blocks( $blocks ) { 1236 return implode( '', array_map( 'serialize_block', $blocks ) ); 1237 } 1238 1239 /** 1240 * Traverses a parsed block tree and applies callbacks before and after serializing it. 1241 * 1242 * Recursively traverses the block and its inner blocks and applies the two callbacks provided as 1243 * arguments, the first one before serializing the block, and the second one after serializing it. 1244 * If either callback returns a string value, it will be prepended and appended to the serialized 1245 * block markup, respectively. 1246 * 1247 * The callbacks will receive a reference to the current block as their first argument, so that they 1248 * can also modify it, and the current block's parent block as second argument. Finally, the 1249 * `$pre_callback` receives the previous block, whereas the `$post_callback` receives 1250 * the next block as third argument. 1251 * 1252 * Serialized blocks are returned including comment delimiters, and with all attributes serialized. 1253 * 1254 * This function should be used when there is a need to modify the saved block, or to inject markup 1255 * into the return value. Prefer `serialize_block` when preparing a block to be saved to post content. 1256 * 1257 * This function is meant for internal use only. 1258 * 1259 * @since 6.4.0 1260 * @access private 1261 * 1262 * @see serialize_block() 1263 * 1264 * @param array $block A representative array of a single parsed block object. See WP_Block_Parser_Block. 1265 * @param callable $pre_callback Callback to run on each block in the tree before it is traversed and serialized. 1266 * It is called with the following arguments: &$block, $parent_block, $previous_block. 1267 * Its string return value will be prepended to the serialized block markup. 1268 * @param callable $post_callback Callback to run on each block in the tree after it is traversed and serialized. 1269 * It is called with the following arguments: &$block, $parent_block, $next_block. 1270 * Its string return value will be appended to the serialized block markup. 1271 * @return string Serialized block markup. 1272 */ 1273 function traverse_and_serialize_block( $block, $pre_callback = null, $post_callback = null ) { 1274 $block_content = ''; 1275 $block_index = 0; 1276 1277 foreach ( $block['innerContent'] as $chunk ) { 1278 if ( is_string( $chunk ) ) { 1279 $block_content .= $chunk; 1280 } else { 1281 $inner_block = $block['innerBlocks'][ $block_index ]; 1282 1283 if ( is_callable( $pre_callback ) ) { 1284 $prev = 0 === $block_index 1285 ? null 1286 : $block['innerBlocks'][ $block_index - 1 ]; 1287 1288 $block_content .= call_user_func_array( 1289 $pre_callback, 1290 array( &$inner_block, &$block, $prev ) 1291 ); 1292 } 1293 1294 if ( is_callable( $post_callback ) ) { 1295 $next = count( $block['innerBlocks'] ) - 1 === $block_index 1296 ? null 1297 : $block['innerBlocks'][ $block_index + 1 ]; 1298 1299 $post_markup = call_user_func_array( 1300 $post_callback, 1301 array( &$inner_block, &$block, $next ) 1302 ); 1303 } 1304 1305 $block_content .= traverse_and_serialize_block( $inner_block, $pre_callback, $post_callback ); 1306 $block_content .= isset( $post_markup ) ? $post_markup : ''; 1307 1308 ++$block_index; 1309 } 1310 } 1311 1312 if ( ! is_array( $block['attrs'] ) ) { 1313 $block['attrs'] = array(); 1314 } 1315 1316 return get_comment_delimited_block_content( 1317 $block['blockName'], 1318 $block['attrs'], 1319 $block_content 1320 ); 1321 } 1322 1323 /** 1324 * Given an array of parsed block trees, applies callbacks before and after serializing them and 1325 * returns their concatenated output. 1326 * 1327 * Recursively traverses the blocks and their inner blocks and applies the two callbacks provided as 1328 * arguments, the first one before serializing a block, and the second one after serializing. 1329 * If either callback returns a string value, it will be prepended and appended to the serialized 1330 * block markup, respectively. 1331 * 1332 * The callbacks will receive a reference to the current block as their first argument, so that they 1333 * can also modify it, and the current block's parent block as second argument. Finally, the 1334 * `$pre_callback` receives the previous block, whereas the `$post_callback` receives 1335 * the next block as third argument. 1336 * 1337 * Serialized blocks are returned including comment delimiters, and with all attributes serialized. 1338 * 1339 * This function should be used when there is a need to modify the saved blocks, or to inject markup 1340 * into the return value. Prefer `serialize_blocks` when preparing blocks to be saved to post content. 1341 * 1342 * This function is meant for internal use only. 1343 * 1344 * @since 6.4.0 1345 * @access private 1346 * 1347 * @see serialize_blocks() 1348 * 1349 * @param array[] $blocks An array of parsed blocks. See WP_Block_Parser_Block. 1350 * @param callable $pre_callback Callback to run on each block in the tree before it is traversed and serialized. 1351 * It is called with the following arguments: &$block, $parent_block, $previous_block. 1352 * Its string return value will be prepended to the serialized block markup. 1353 * @param callable $post_callback Callback to run on each block in the tree after it is traversed and serialized. 1354 * It is called with the following arguments: &$block, $parent_block, $next_block. 1355 * Its string return value will be appended to the serialized block markup. 1356 * @return string Serialized block markup. 1357 */ 1358 function traverse_and_serialize_blocks( $blocks, $pre_callback = null, $post_callback = null ) { 1359 $result = ''; 1360 $parent_block = null; // At the top level, there is no parent block to pass to the callbacks; yet the callbacks expect a reference. 1361 1362 foreach ( $blocks as $index => $block ) { 1363 if ( is_callable( $pre_callback ) ) { 1364 $prev = 0 === $index 1365 ? null 1366 : $blocks[ $index - 1 ]; 1367 1368 $result .= call_user_func_array( 1369 $pre_callback, 1370 array( &$block, &$parent_block, $prev ) 1371 ); 1372 } 1373 1374 if ( is_callable( $post_callback ) ) { 1375 $next = count( $blocks ) - 1 === $index 1376 ? null 1377 : $blocks[ $index + 1 ]; 1378 1379 $post_markup = call_user_func_array( 1380 $post_callback, 1381 array( &$block, &$parent_block, $next ) 1382 ); 1383 } 1384 1385 $result .= traverse_and_serialize_block( $block, $pre_callback, $post_callback ); 1386 $result .= isset( $post_markup ) ? $post_markup : ''; 1387 } 1388 1389 return $result; 1390 } 1391 1392 /** 1393 * Filters and sanitizes block content to remove non-allowable HTML 1394 * from parsed block attribute values. 1395 * 1396 * @since 5.3.1 1397 * 1398 * @param string $text Text that may contain block content. 1399 * @param array[]|string $allowed_html Optional. An array of allowed HTML elements and attributes, 1400 * or a context name such as 'post'. See wp_kses_allowed_html() 1401 * for the list of accepted context names. Default 'post'. 1402 * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. 1403 * Defaults to the result of wp_allowed_protocols(). 1404 * @return string The filtered and sanitized content result. 1405 */ 1406 function filter_block_content( $text, $allowed_html = 'post', $allowed_protocols = array() ) { 1407 $result = ''; 1408 1409 if ( str_contains( $text, '<!--' ) && str_contains( $text, '--->' ) ) { 1410 $text = preg_replace_callback( '%<!--(.*?)--->%', '_filter_block_content_callback', $text ); 1411 } 1412 1413 $blocks = parse_blocks( $text ); 1414 foreach ( $blocks as $block ) { 1415 $block = filter_block_kses( $block, $allowed_html, $allowed_protocols ); 1416 $result .= serialize_block( $block ); 1417 } 1418 1419 return $result; 1420 } 1421 1422 /** 1423 * Callback used for regular expression replacement in filter_block_content(). 1424 * 1425 * @since 6.2.1 1426 * @access private 1427 * 1428 * @param array $matches Array of preg_replace_callback matches. 1429 * @return string Replacement string. 1430 */ 1431 function _filter_block_content_callback( $matches ) { 1432 return '<!--' . rtrim( $matches[1], '-' ) . '-->'; 1433 } 1434 1435 /** 1436 * Filters and sanitizes a parsed block to remove non-allowable HTML 1437 * from block attribute values. 1438 * 1439 * @since 5.3.1 1440 * 1441 * @param WP_Block_Parser_Block $block The parsed block object. 1442 * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, 1443 * or a context name such as 'post'. See wp_kses_allowed_html() 1444 * for the list of accepted context names. 1445 * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. 1446 * Defaults to the result of wp_allowed_protocols(). 1447 * @return array The filtered and sanitized block object result. 1448 */ 1449 function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) { 1450 $block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols ); 1451 1452 if ( is_array( $block['innerBlocks'] ) ) { 1453 foreach ( $block['innerBlocks'] as $i => $inner_block ) { 1454 $block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols ); 1455 } 1456 } 1457 1458 return $block; 1459 } 1460 1461 /** 1462 * Filters and sanitizes a parsed block attribute value to remove 1463 * non-allowable HTML. 1464 * 1465 * @since 5.3.1 1466 * 1467 * @param string[]|string $value The attribute value to filter. 1468 * @param array[]|string $allowed_html An array of allowed HTML elements and attributes, 1469 * or a context name such as 'post'. See wp_kses_allowed_html() 1470 * for the list of accepted context names. 1471 * @param string[] $allowed_protocols Optional. Array of allowed URL protocols. 1472 * Defaults to the result of wp_allowed_protocols(). 1473 * @return string[]|string The filtered and sanitized result. 1474 */ 1475 function filter_block_kses_value( $value, $allowed_html, $allowed_protocols = array() ) { 1476 if ( is_array( $value ) ) { 1477 foreach ( $value as $key => $inner_value ) { 1478 $filtered_key = filter_block_kses_value( $key, $allowed_html, $allowed_protocols ); 1479 $filtered_value = filter_block_kses_value( $inner_value, $allowed_html, $allowed_protocols ); 1480 1481 if ( $filtered_key !== $key ) { 1482 unset( $value[ $key ] ); 1483 } 1484 1485 $value[ $filtered_key ] = $filtered_value; 1486 } 1487 } elseif ( is_string( $value ) ) { 1488 return wp_kses( $value, $allowed_html, $allowed_protocols ); 1489 } 1490 1491 return $value; 1492 } 1493 1494 /** 1495 * Parses blocks out of a content string, and renders those appropriate for the excerpt. 1496 * 1497 * As the excerpt should be a small string of text relevant to the full post content, 1498 * this function renders the blocks that are most likely to contain such text. 1499 * 1500 * @since 5.0.0 1501 * 1502 * @param string $content The content to parse. 1503 * @return string The parsed and filtered content. 1504 */ 1505 function excerpt_remove_blocks( $content ) { 1506 if ( ! has_blocks( $content ) ) { 1507 return $content; 1508 } 1509 1510 $allowed_inner_blocks = array( 1511 // Classic blocks have their blockName set to null. 1512 null, 1513 'core/freeform', 1514 'core/heading', 1515 'core/html', 1516 'core/list', 1517 'core/media-text', 1518 'core/paragraph', 1519 'core/preformatted', 1520 'core/pullquote', 1521 'core/quote', 1522 'core/table', 1523 'core/verse', 1524 ); 1525 1526 $allowed_wrapper_blocks = array( 1527 'core/columns', 1528 'core/column', 1529 'core/group', 1530 ); 1531 1532 /** 1533 * Filters the list of blocks that can be used as wrapper blocks, allowing 1534 * excerpts to be generated from the `innerBlocks` of these wrappers. 1535 * 1536 * @since 5.8.0 1537 * 1538 * @param string[] $allowed_wrapper_blocks The list of names of allowed wrapper blocks. 1539 */ 1540 $allowed_wrapper_blocks = apply_filters( 'excerpt_allowed_wrapper_blocks', $allowed_wrapper_blocks ); 1541 1542 $allowed_blocks = array_merge( $allowed_inner_blocks, $allowed_wrapper_blocks ); 1543 1544 /** 1545 * Filters the list of blocks that can contribute to the excerpt. 1546 * 1547 * If a dynamic block is added to this list, it must not generate another 1548 * excerpt, as this will cause an infinite loop to occur. 1549 * 1550 * @since 5.0.0 1551 * 1552 * @param string[] $allowed_blocks The list of names of allowed blocks. 1553 */ 1554 $allowed_blocks = apply_filters( 'excerpt_allowed_blocks', $allowed_blocks ); 1555 $blocks = parse_blocks( $content ); 1556 $output = ''; 1557 1558 foreach ( $blocks as $block ) { 1559 if ( in_array( $block['blockName'], $allowed_blocks, true ) ) { 1560 if ( ! empty( $block['innerBlocks'] ) ) { 1561 if ( in_array( $block['blockName'], $allowed_wrapper_blocks, true ) ) { 1562 $output .= _excerpt_render_inner_blocks( $block, $allowed_blocks ); 1563 continue; 1564 } 1565 1566 // Skip the block if it has disallowed or nested inner blocks. 1567 foreach ( $block['innerBlocks'] as $inner_block ) { 1568 if ( 1569 ! in_array( $inner_block['blockName'], $allowed_inner_blocks, true ) || 1570 ! empty( $inner_block['innerBlocks'] ) 1571 ) { 1572 continue 2; 1573 } 1574 } 1575 } 1576 1577 $output .= render_block( $block ); 1578 } 1579 } 1580 1581 return $output; 1582 } 1583 1584 /** 1585 * Parses footnotes markup out of a content string, 1586 * and renders those appropriate for the excerpt. 1587 * 1588 * @since 6.3.0 1589 * 1590 * @param string $content The content to parse. 1591 * @return string The parsed and filtered content. 1592 */ 1593 function excerpt_remove_footnotes( $content ) { 1594 if ( ! str_contains( $content, 'data-fn=' ) ) { 1595 return $content; 1596 } 1597 1598 return preg_replace( 1599 '_<sup data-fn="[^"]+" class="[^"]+">\s*<a href="[^"]+" id="[^"]+">\d+</a>\s*</sup>_', 1600 '', 1601 $content 1602 ); 1603 } 1604 1605 /** 1606 * Renders inner blocks from the allowed wrapper blocks 1607 * for generating an excerpt. 1608 * 1609 * @since 5.8.0 1610 * @access private 1611 * 1612 * @param array $parsed_block The parsed block. 1613 * @param array $allowed_blocks The list of allowed inner blocks. 1614 * @return string The rendered inner blocks. 1615 */ 1616 function _excerpt_render_inner_blocks( $parsed_block, $allowed_blocks ) { 1617 $output = ''; 1618 1619 foreach ( $parsed_block['innerBlocks'] as $inner_block ) { 1620 if ( ! in_array( $inner_block['blockName'], $allowed_blocks, true ) ) { 1621 continue; 1622 } 1623 1624 if ( empty( $inner_block['innerBlocks'] ) ) { 1625 $output .= render_block( $inner_block ); 1626 } else { 1627 $output .= _excerpt_render_inner_blocks( $inner_block, $allowed_blocks ); 1628 } 1629 } 1630 1631 return $output; 1632 } 1633 1634 /** 1635 * Renders a single block into a HTML string. 1636 * 1637 * @since 5.0.0 1638 * 1639 * @global WP_Post $post The post to edit. 1640 * 1641 * @param array $parsed_block A single parsed block object. 1642 * @return string String of rendered HTML. 1643 */ 1644 function render_block( $parsed_block ) { 1645 global $post; 1646 $parent_block = null; 1647 1648 /** 1649 * Allows render_block() to be short-circuited, by returning a non-null value. 1650 * 1651 * @since 5.1.0 1652 * @since 5.9.0 The `$parent_block` parameter was added. 1653 * 1654 * @param string|null $pre_render The pre-rendered content. Default null. 1655 * @param array $parsed_block The block being rendered. 1656 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. 1657 */ 1658 $pre_render = apply_filters( 'pre_render_block', null, $parsed_block, $parent_block ); 1659 if ( ! is_null( $pre_render ) ) { 1660 return $pre_render; 1661 } 1662 1663 $source_block = $parsed_block; 1664 1665 /** 1666 * Filters the block being rendered in render_block(), before it's processed. 1667 * 1668 * @since 5.1.0 1669 * @since 5.9.0 The `$parent_block` parameter was added. 1670 * 1671 * @param array $parsed_block The block being rendered. 1672 * @param array $source_block An un-modified copy of $parsed_block, as it appeared in the source content. 1673 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. 1674 */ 1675 $parsed_block = apply_filters( 'render_block_data', $parsed_block, $source_block, $parent_block ); 1676 1677 $context = array(); 1678 1679 if ( $post instanceof WP_Post ) { 1680 $context['postId'] = $post->ID; 1681 1682 /* 1683 * The `postType` context is largely unnecessary server-side, since the ID 1684 * is usually sufficient on its own. That being said, since a block's 1685 * manifest is expected to be shared between the server and the client, 1686 * it should be included to consistently fulfill the expectation. 1687 */ 1688 $context['postType'] = $post->post_type; 1689 } 1690 1691 /** 1692 * Filters the default context provided to a rendered block. 1693 * 1694 * @since 5.5.0 1695 * @since 5.9.0 The `$parent_block` parameter was added. 1696 * 1697 * @param array $context Default context. 1698 * @param array $parsed_block Block being rendered, filtered by `render_block_data`. 1699 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. 1700 */ 1701 $context = apply_filters( 'render_block_context', $context, $parsed_block, $parent_block ); 1702 1703 $block = new WP_Block( $parsed_block, $context ); 1704 1705 return $block->render(); 1706 } 1707 1708 /** 1709 * Parses blocks out of a content string. 1710 * 1711 * @since 5.0.0 1712 * 1713 * @param string $content Post content. 1714 * @return array[] Array of parsed block objects. 1715 */ 1716 function parse_blocks( $content ) { 1717 /** 1718 * Filter to allow plugins to replace the server-side block parser. 1719 * 1720 * @since 5.0.0 1721 * 1722 * @param string $parser_class Name of block parser class. 1723 */ 1724 $parser_class = apply_filters( 'block_parser_class', 'WP_Block_Parser' ); 1725 1726 $parser = new $parser_class(); 1727 return $parser->parse( $content ); 1728 } 1729 1730 /** 1731 * Parses dynamic blocks out of `post_content` and re-renders them. 1732 * 1733 * @since 5.0.0 1734 * 1735 * @param string $content Post content. 1736 * @return string Updated post content. 1737 */ 1738 function do_blocks( $content ) { 1739 $blocks = parse_blocks( $content ); 1740 $output = ''; 1741 1742 foreach ( $blocks as $block ) { 1743 $output .= render_block( $block ); 1744 } 1745 1746 // If there are blocks in this content, we shouldn't run wpautop() on it later. 1747 $priority = has_filter( 'the_content', 'wpautop' ); 1748 if ( false !== $priority && doing_filter( 'the_content' ) && has_blocks( $content ) ) { 1749 remove_filter( 'the_content', 'wpautop', $priority ); 1750 add_filter( 'the_content', '_restore_wpautop_hook', $priority + 1 ); 1751 } 1752 1753 return $output; 1754 } 1755 1756 /** 1757 * If do_blocks() needs to remove wpautop() from the `the_content` filter, this re-adds it afterwards, 1758 * for subsequent `the_content` usage. 1759 * 1760 * @since 5.0.0 1761 * @access private 1762 * 1763 * @param string $content The post content running through this filter. 1764 * @return string The unmodified content. 1765 */ 1766 function _restore_wpautop_hook( $content ) { 1767 $current_priority = has_filter( 'the_content', '_restore_wpautop_hook' ); 1768 1769 add_filter( 'the_content', 'wpautop', $current_priority - 1 ); 1770 remove_filter( 'the_content', '_restore_wpautop_hook', $current_priority ); 1771 1772 return $content; 1773 } 1774 1775 /** 1776 * Returns the current version of the block format that the content string is using. 1777 * 1778 * If the string doesn't contain blocks, it returns 0. 1779 * 1780 * @since 5.0.0 1781 * 1782 * @param string $content Content to test. 1783 * @return int The block format version is 1 if the content contains one or more blocks, 0 otherwise. 1784 */ 1785 function block_version( $content ) { 1786 return has_blocks( $content ) ? 1 : 0; 1787 } 1788 1789 /** 1790 * Registers a new block style. 1791 * 1792 * @since 5.3.0 1793 * 1794 * @link https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/ 1795 * 1796 * @param string $block_name Block type name including namespace. 1797 * @param array $style_properties Array containing the properties of the style name, label, 1798 * style_handle (name of the stylesheet to be enqueued), 1799 * inline_style (string containing the CSS to be added). 1800 * See WP_Block_Styles_Registry::register(). 1801 * @return bool True if the block style was registered with success and false otherwise. 1802 */ 1803 function register_block_style( $block_name, $style_properties ) { 1804 return WP_Block_Styles_Registry::get_instance()->register( $block_name, $style_properties ); 1805 } 1806 1807 /** 1808 * Unregisters a block style. 1809 * 1810 * @since 5.3.0 1811 * 1812 * @param string $block_name Block type name including namespace. 1813 * @param string $block_style_name Block style name. 1814 * @return bool True if the block style was unregistered with success and false otherwise. 1815 */ 1816 function unregister_block_style( $block_name, $block_style_name ) { 1817 return WP_Block_Styles_Registry::get_instance()->unregister( $block_name, $block_style_name ); 1818 } 1819 1820 /** 1821 * Checks whether the current block type supports the feature requested. 1822 * 1823 * @since 5.8.0 1824 * @since 6.4.0 The `$feature` parameter now supports a string. 1825 * 1826 * @param WP_Block_Type $block_type Block type to check for support. 1827 * @param string|array $feature Feature slug, or path to a specific feature to check support for. 1828 * @param mixed $default_value Optional. Fallback value for feature support. Default false. 1829 * @return bool Whether the feature is supported. 1830 */ 1831 function block_has_support( $block_type, $feature, $default_value = false ) { 1832 $block_support = $default_value; 1833 if ( $block_type instanceof WP_Block_Type ) { 1834 if ( is_array( $feature ) && count( $feature ) === 1 ) { 1835 $feature = $feature[0]; 1836 } 1837 1838 if ( is_array( $feature ) ) { 1839 $block_support = _wp_array_get( $block_type->supports, $feature, $default_value ); 1840 } elseif ( isset( $block_type->supports[ $feature ] ) ) { 1841 $block_support = $block_type->supports[ $feature ]; 1842 } 1843 } 1844 1845 return true === $block_support || is_array( $block_support ); 1846 } 1847 1848 /** 1849 * Converts typography keys declared under `supports.*` to `supports.typography.*`. 1850 * 1851 * Displays a `_doing_it_wrong()` notice when a block using the older format is detected. 1852 * 1853 * @since 5.8.0 1854 * 1855 * @param array $metadata Metadata for registering a block type. 1856 * @return array Filtered metadata for registering a block type. 1857 */ 1858 function wp_migrate_old_typography_shape( $metadata ) { 1859 if ( ! isset( $metadata['supports'] ) ) { 1860 return $metadata; 1861 } 1862 1863 $typography_keys = array( 1864 '__experimentalFontFamily', 1865 '__experimentalFontStyle', 1866 '__experimentalFontWeight', 1867 '__experimentalLetterSpacing', 1868 '__experimentalTextDecoration', 1869 '__experimentalTextTransform', 1870 'fontSize', 1871 'lineHeight', 1872 ); 1873 1874 foreach ( $typography_keys as $typography_key ) { 1875 $support_for_key = isset( $metadata['supports'][ $typography_key ] ) ? $metadata['supports'][ $typography_key ] : null; 1876 1877 if ( null !== $support_for_key ) { 1878 _doing_it_wrong( 1879 'register_block_type_from_metadata()', 1880 sprintf( 1881 /* translators: 1: Block type, 2: Typography supports key, e.g: fontSize, lineHeight, etc. 3: block.json, 4: Old metadata key, 5: New metadata key. */ 1882 __( 'Block "%1$s" is declaring %2$s support in %3$s file under %4$s. %2$s support is now declared under %5$s.' ), 1883 $metadata['name'], 1884 "<code>$typography_key</code>", 1885 '<code>block.json</code>', 1886 "<code>supports.$typography_key</code>", 1887 "<code>supports.typography.$typography_key</code>" 1888 ), 1889 '5.8.0' 1890 ); 1891 1892 _wp_array_set( $metadata['supports'], array( 'typography', $typography_key ), $support_for_key ); 1893 unset( $metadata['supports'][ $typography_key ] ); 1894 } 1895 } 1896 1897 return $metadata; 1898 } 1899 1900 /** 1901 * Helper function that constructs a WP_Query args array from 1902 * a `Query` block properties. 1903 * 1904 * It's used in Query Loop, Query Pagination Numbers and Query Pagination Next blocks. 1905 * 1906 * @since 5.8.0 1907 * @since 6.1.0 Added `query_loop_block_query_vars` filter and `parents` support in query. 1908 * 1909 * @param WP_Block $block Block instance. 1910 * @param int $page Current query's page. 1911 * 1912 * @return array Returns the constructed WP_Query arguments. 1913 */ 1914 function build_query_vars_from_query_block( $block, $page ) { 1915 $query = array( 1916 'post_type' => 'post', 1917 'order' => 'DESC', 1918 'orderby' => 'date', 1919 'post__not_in' => array(), 1920 ); 1921 1922 if ( isset( $block->context['query'] ) ) { 1923 if ( ! empty( $block->context['query']['postType'] ) ) { 1924 $post_type_param = $block->context['query']['postType']; 1925 if ( is_post_type_viewable( $post_type_param ) ) { 1926 $query['post_type'] = $post_type_param; 1927 } 1928 } 1929 if ( isset( $block->context['query']['sticky'] ) && ! empty( $block->context['query']['sticky'] ) ) { 1930 $sticky = get_option( 'sticky_posts' ); 1931 if ( 'only' === $block->context['query']['sticky'] ) { 1932 /* 1933 * Passing an empty array to post__in will return have_posts() as true (and all posts will be returned). 1934 * Logic should be used before hand to determine if WP_Query should be used in the event that the array 1935 * being passed to post__in is empty. 1936 * 1937 * @see https://core.trac.wordpress.org/ticket/28099 1938 */ 1939 $query['post__in'] = ! empty( $sticky ) ? $sticky : array( 0 ); 1940 $query['ignore_sticky_posts'] = 1; 1941 } else { 1942 $query['post__not_in'] = array_merge( $query['post__not_in'], $sticky ); 1943 } 1944 } 1945 if ( ! empty( $block->context['query']['exclude'] ) ) { 1946 $excluded_post_ids = array_map( 'intval', $block->context['query']['exclude'] ); 1947 $excluded_post_ids = array_filter( $excluded_post_ids ); 1948 $query['post__not_in'] = array_merge( $query['post__not_in'], $excluded_post_ids ); 1949 } 1950 if ( 1951 isset( $block->context['query']['perPage'] ) && 1952 is_numeric( $block->context['query']['perPage'] ) 1953 ) { 1954 $per_page = absint( $block->context['query']['perPage'] ); 1955 $offset = 0; 1956 1957 if ( 1958 isset( $block->context['query']['offset'] ) && 1959 is_numeric( $block->context['query']['offset'] ) 1960 ) { 1961 $offset = absint( $block->context['query']['offset'] ); 1962 } 1963 1964 $query['offset'] = ( $per_page * ( $page - 1 ) ) + $offset; 1965 $query['posts_per_page'] = $per_page; 1966 } 1967 // Migrate `categoryIds` and `tagIds` to `tax_query` for backwards compatibility. 1968 if ( ! empty( $block->context['query']['categoryIds'] ) || ! empty( $block->context['query']['tagIds'] ) ) { 1969 $tax_query = array(); 1970 if ( ! empty( $block->context['query']['categoryIds'] ) ) { 1971 $tax_query[] = array( 1972 'taxonomy' => 'category', 1973 'terms' => array_filter( array_map( 'intval', $block->context['query']['categoryIds'] ) ), 1974 'include_children' => false, 1975 ); 1976 } 1977 if ( ! empty( $block->context['query']['tagIds'] ) ) { 1978 $tax_query[] = array( 1979 'taxonomy' => 'post_tag', 1980 'terms' => array_filter( array_map( 'intval', $block->context['query']['tagIds'] ) ), 1981 'include_children' => false, 1982 ); 1983 } 1984 $query['tax_query'] = $tax_query; 1985 } 1986 if ( ! empty( $block->context['query']['taxQuery'] ) ) { 1987 $query['tax_query'] = array(); 1988 foreach ( $block->context['query']['taxQuery'] as $taxonomy => $terms ) { 1989 if ( is_taxonomy_viewable( $taxonomy ) && ! empty( $terms ) ) { 1990 $query['tax_query'][] = array( 1991 'taxonomy' => $taxonomy, 1992 'terms' => array_filter( array_map( 'intval', $terms ) ), 1993 'include_children' => false, 1994 ); 1995 } 1996 } 1997 } 1998 if ( 1999 isset( $block->context['query']['order'] ) && 2000 in_array( strtoupper( $block->context['query']['order'] ), array( 'ASC', 'DESC' ), true ) 2001 ) { 2002 $query['order'] = strtoupper( $block->context['query']['order'] ); 2003 } 2004 if ( isset( $block->context['query']['orderBy'] ) ) { 2005 $query['orderby'] = $block->context['query']['orderBy']; 2006 } 2007 if ( 2008 isset( $block->context['query']['author'] ) 2009 ) { 2010 if ( is_array( $block->context['query']['author'] ) ) { 2011 $query['author__in'] = array_filter( array_map( 'intval', $block->context['query']['author'] ) ); 2012 } elseif ( is_string( $block->context['query']['author'] ) ) { 2013 $query['author__in'] = array_filter( array_map( 'intval', explode( ',', $block->context['query']['author'] ) ) ); 2014 } elseif ( is_int( $block->context['query']['author'] ) && $block->context['query']['author'] > 0 ) { 2015 $query['author'] = $block->context['query']['author']; 2016 } 2017 } 2018 if ( ! empty( $block->context['query']['search'] ) ) { 2019 $query['s'] = $block->context['query']['search']; 2020 } 2021 if ( ! empty( $block->context['query']['parents'] ) && is_post_type_hierarchical( $query['post_type'] ) ) { 2022 $query['post_parent__in'] = array_filter( array_map( 'intval', $block->context['query']['parents'] ) ); 2023 } 2024 } 2025 2026 /** 2027 * Filters the arguments which will be passed to `WP_Query` for the Query Loop Block. 2028 * 2029 * Anything to this filter should be compatible with the `WP_Query` API to form 2030 * the query context which will be passed down to the Query Loop Block's children. 2031 * This can help, for example, to include additional settings or meta queries not 2032 * directly supported by the core Query Loop Block, and extend its capabilities. 2033 * 2034 * Please note that this will only influence the query that will be rendered on the 2035 * front-end. The editor preview is not affected by this filter. Also, worth noting 2036 * that the editor preview uses the REST API, so, ideally, one should aim to provide 2037 * attributes which are also compatible with the REST API, in order to be able to 2038 * implement identical queries on both sides. 2039 * 2040 * @since 6.1.0 2041 * 2042 * @param array $query Array containing parameters for `WP_Query` as parsed by the block context. 2043 * @param WP_Block $block Block instance. 2044 * @param int $page Current query's page. 2045 */ 2046 return apply_filters( 'query_loop_block_query_vars', $query, $block, $page ); 2047 } 2048 2049 /** 2050 * Helper function that returns the proper pagination arrow HTML for 2051 * `QueryPaginationNext` and `QueryPaginationPrevious` blocks based 2052 * on the provided `paginationArrow` from `QueryPagination` context. 2053 * 2054 * It's used in QueryPaginationNext and QueryPaginationPrevious blocks. 2055 * 2056 * @since 5.9.0 2057 * 2058 * @param WP_Block $block Block instance. 2059 * @param bool $is_next Flag for handling `next/previous` blocks. 2060 * @return string|null The pagination arrow HTML or null if there is none. 2061 */ 2062 function get_query_pagination_arrow( $block, $is_next ) { 2063 $arrow_map = array( 2064 'none' => '', 2065 'arrow' => array( 2066 'next' => '→', 2067 'previous' => '←', 2068 ), 2069 'chevron' => array( 2070 'next' => '»', 2071 'previous' => '«', 2072 ), 2073 ); 2074 if ( ! empty( $block->context['paginationArrow'] ) && array_key_exists( $block->context['paginationArrow'], $arrow_map ) && ! empty( $arrow_map[ $block->context['paginationArrow'] ] ) ) { 2075 $pagination_type = $is_next ? 'next' : 'previous'; 2076 $arrow_attribute = $block->context['paginationArrow']; 2077 $arrow = $arrow_map[ $block->context['paginationArrow'] ][ $pagination_type ]; 2078 $arrow_classes = "wp-block-query-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; 2079 return "<span class='$arrow_classes' aria-hidden='true'>$arrow</span>"; 2080 } 2081 return null; 2082 } 2083 2084 /** 2085 * Helper function that constructs a comment query vars array from the passed 2086 * block properties. 2087 * 2088 * It's used with the Comment Query Loop inner blocks. 2089 * 2090 * @since 6.0.0 2091 * 2092 * @param WP_Block $block Block instance. 2093 * @return array Returns the comment query parameters to use with the 2094 * WP_Comment_Query constructor. 2095 */ 2096 function build_comment_query_vars_from_block( $block ) { 2097 2098 $comment_args = array( 2099 'orderby' => 'comment_date_gmt', 2100 'order' => 'ASC', 2101 'status' => 'approve', 2102 'no_found_rows' => false, 2103 ); 2104 2105 if ( is_user_logged_in() ) { 2106 $comment_args['include_unapproved'] = array( get_current_user_id() ); 2107 } else { 2108 $unapproved_email = wp_get_unapproved_comment_author_email(); 2109 2110 if ( $unapproved_email ) { 2111 $comment_args['include_unapproved'] = array( $unapproved_email ); 2112 } 2113 } 2114 2115 if ( ! empty( $block->context['postId'] ) ) { 2116 $comment_args['post_id'] = (int) $block->context['postId']; 2117 } 2118 2119 if ( get_option( 'thread_comments' ) ) { 2120 $comment_args['hierarchical'] = 'threaded'; 2121 } else { 2122 $comment_args['hierarchical'] = false; 2123 } 2124 2125 if ( get_option( 'page_comments' ) === '1' || get_option( 'page_comments' ) === true ) { 2126 $per_page = get_option( 'comments_per_page' ); 2127 $default_page = get_option( 'default_comments_page' ); 2128 if ( $per_page > 0 ) { 2129 $comment_args['number'] = $per_page; 2130 2131 $page = (int) get_query_var( 'cpage' ); 2132 if ( $page ) { 2133 $comment_args['paged'] = $page; 2134 } elseif ( 'oldest' === $default_page ) { 2135 $comment_args['paged'] = 1; 2136 } elseif ( 'newest' === $default_page ) { 2137 $max_num_pages = (int) ( new WP_Comment_Query( $comment_args ) )->max_num_pages; 2138 if ( 0 !== $max_num_pages ) { 2139 $comment_args['paged'] = $max_num_pages; 2140 } 2141 } 2142 // Set the `cpage` query var to ensure the previous and next pagination links are correct 2143 // when inheriting the Discussion Settings. 2144 if ( 0 === $page && isset( $comment_args['paged'] ) && $comment_args['paged'] > 0 ) { 2145 set_query_var( 'cpage', $comment_args['paged'] ); 2146 } 2147 } 2148 } 2149 2150 return $comment_args; 2151 } 2152 2153 /** 2154 * Helper function that returns the proper pagination arrow HTML for 2155 * `CommentsPaginationNext` and `CommentsPaginationPrevious` blocks based on the 2156 * provided `paginationArrow` from `CommentsPagination` context. 2157 * 2158 * It's used in CommentsPaginationNext and CommentsPaginationPrevious blocks. 2159 * 2160 * @since 6.0.0 2161 * 2162 * @param WP_Block $block Block instance. 2163 * @param string $pagination_type Optional. Type of the arrow we will be rendering. 2164 * Accepts 'next' or 'previous'. Default 'next'. 2165 * @return string|null The pagination arrow HTML or null if there is none. 2166 */ 2167 function get_comments_pagination_arrow( $block, $pagination_type = 'next' ) { 2168 $arrow_map = array( 2169 'none' => '', 2170 'arrow' => array( 2171 'next' => '→', 2172 'previous' => '←', 2173 ), 2174 'chevron' => array( 2175 'next' => '»', 2176 'previous' => '«', 2177 ), 2178 ); 2179 if ( ! empty( $block->context['comments/paginationArrow'] ) && ! empty( $arrow_map[ $block->context['comments/paginationArrow'] ][ $pagination_type ] ) ) { 2180 $arrow_attribute = $block->context['comments/paginationArrow']; 2181 $arrow = $arrow_map[ $block->context['comments/paginationArrow'] ][ $pagination_type ]; 2182 $arrow_classes = "wp-block-comments-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; 2183 return "<span class='$arrow_classes' aria-hidden='true'>$arrow</span>"; 2184 } 2185 return null; 2186 } 2187 2188 /** 2189 * Strips all HTML from the content of footnotes, and sanitizes the ID. 2190 * 2191 * This function expects slashed data on the footnotes content. 2192 * 2193 * @access private 2194 * @since 6.3.2 2195 * 2196 * @param string $footnotes JSON-encoded string of an array containing the content and ID of each footnote. 2197 * @return string Filtered content without any HTML on the footnote content and with the sanitized ID. 2198 */ 2199 function _wp_filter_post_meta_footnotes( $footnotes ) { 2200 $footnotes_decoded = json_decode( $footnotes, true ); 2201 if ( ! is_array( $footnotes_decoded ) ) { 2202 return ''; 2203 } 2204 $footnotes_sanitized = array(); 2205 foreach ( $footnotes_decoded as $footnote ) { 2206 if ( ! empty( $footnote['content'] ) && ! empty( $footnote['id'] ) ) { 2207 $footnotes_sanitized[] = array( 2208 'id' => sanitize_key( $footnote['id'] ), 2209 'content' => wp_unslash( wp_filter_post_kses( wp_slash( $footnote['content'] ) ) ), 2210 ); 2211 } 2212 } 2213 return wp_json_encode( $footnotes_sanitized ); 2214 } 2215 2216 /** 2217 * Adds the filters for footnotes meta field. 2218 * 2219 * @access private 2220 * @since 6.3.2 2221 */ 2222 function _wp_footnotes_kses_init_filters() { 2223 add_filter( 'sanitize_post_meta_footnotes', '_wp_filter_post_meta_footnotes' ); 2224 } 2225 2226 /** 2227 * Removes the filters for footnotes meta field. 2228 * 2229 * @access private 2230 * @since 6.3.2 2231 */ 2232 function _wp_footnotes_remove_filters() { 2233 remove_filter( 'sanitize_post_meta_footnotes', '_wp_filter_post_meta_footnotes' ); 2234 } 2235 2236 /** 2237 * Registers the filter of footnotes meta field if the user does not have `unfiltered_html` capability. 2238 * 2239 * @access private 2240 * @since 6.3.2 2241 */ 2242 function _wp_footnotes_kses_init() { 2243 _wp_footnotes_remove_filters(); 2244 if ( ! current_user_can( 'unfiltered_html' ) ) { 2245 _wp_footnotes_kses_init_filters(); 2246 } 2247 } 2248 2249 /** 2250 * Initializes the filters for footnotes meta field when imported data should be filtered. 2251 * 2252 * This filter is the last one being executed on {@see 'force_filtered_html_on_import'}. 2253 * If the input of the filter is true, it means we are in an import situation and should 2254 * enable kses, independently of the user capabilities. So in that case we call 2255 * _wp_footnotes_kses_init_filters(). 2256 * 2257 * @access private 2258 * @since 6.3.2 2259 * 2260 * @param string $arg Input argument of the filter. 2261 * @return string Input argument of the filter. 2262 */ 2263 function _wp_footnotes_force_filtered_html_on_import_filter( $arg ) { 2264 // If `force_filtered_html_on_import` is true, we need to init the global styles kses filters. 2265 if ( $arg ) { 2266 _wp_footnotes_kses_init_filters(); 2267 } 2268 return $arg; 2269 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Sat Apr 20 08:20:01 2024 | Cross-referenced by PHPXref |