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

Get in Touch

© 2024 Teachers Night Out. All Rights Reserved.