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 https://cardgames4educators.com Wed, 16 Oct 2024 22:24:18 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.4 https://cardgames4educators.com/wp-content/uploads/2024/06/cropped-Card-4-Educators-logo-32x32.png Ui Ux Design – Teachers Night Out https://cardgames4educators.com 32 32 Masters In English How English Speaker https://cardgames4educators.com/masters-in-english-how-english-speaker/ https://cardgames4educators.com/masters-in-english-how-english-speaker/#comments Mon, 27 May 2024 08:54:45 +0000 https://themexriver.com/wp/kadu/?p=1

Erat himenaeos neque id sagittis massa. Hac suscipit pulvinar dignissim platea magnis eu. Don tellus a pharetra inceptos efficitur dui pulvinar. Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent pulvinar odio volutpat parturient. Quisque risus finibus suspendisse mus purus magnis facilisi condimentum consectetur dui. Curae elit suspendisse cursus vehicula.

Turpis taciti class non vel pretium quis pulvinar tempor lobortis nunc. Libero phasellus parturient sapien volutpat malesuada ornare. Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae. Porta est tempor ex eget feugiat vulputate ipsum. Justo nec iaculis habitant diam arcu fermentum.

We offer comprehen sive emplo ment services such as assistance wit employer compliance.Our company is your strategic HR partner as instead of HR. john smithson

Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae.

Exploring Learning Landscapes in Academic

Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent.

]]>
https://cardgames4educators.com/masters-in-english-how-english-speaker/feed/ 1