Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
95.33% |
204 / 214 |
|
83.33% |
10 / 12 |
CRAP | |
0.00% |
0 / 1 |
| WPCOM_REST_API_V2_Endpoint_Admin_Menu | |
96.68% |
204 / 211 |
|
83.33% |
10 / 12 |
71 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| register_routes | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
| get_item_permissions_check | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| get_item | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| prepare_menu_for_response | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
7 | |||
| get_item_schema | |
100.00% |
63 / 63 |
|
100.00% |
1 / 1 |
1 | |||
| prepare_menu_item | |
93.75% |
30 / 32 |
|
0.00% |
0 / 1 |
12.04 | |||
| prepare_submenu_item | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
5 | |||
| prepare_menu_item_icon | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
6 | |||
| prepare_dashicon | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
| prepare_menu_item_url | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
19 | |||
| parse_menu_item | |
78.26% |
18 / 23 |
|
0.00% |
0 / 1 |
12.24 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * REST API endpoint for admin menus. |
| 4 | * |
| 5 | * @package automattic/jetpack |
| 6 | * @since 9.1.0 |
| 7 | */ |
| 8 | |
| 9 | use Automattic\Jetpack\Status\Host; |
| 10 | |
| 11 | if ( ! defined( 'ABSPATH' ) ) { |
| 12 | exit( 0 ); |
| 13 | } |
| 14 | |
| 15 | /** |
| 16 | * Class WPCOM_REST_API_V2_Endpoint_Admin_Menu |
| 17 | */ |
| 18 | class WPCOM_REST_API_V2_Endpoint_Admin_Menu extends WP_REST_Controller { |
| 19 | |
| 20 | /** |
| 21 | * Namespace prefix. |
| 22 | * |
| 23 | * @var string |
| 24 | */ |
| 25 | public $namespace = 'wpcom/v2'; |
| 26 | |
| 27 | /** |
| 28 | * Endpoint base route. |
| 29 | * |
| 30 | * @var string |
| 31 | */ |
| 32 | public $rest_base = 'admin-menu'; |
| 33 | |
| 34 | /** |
| 35 | * |
| 36 | * Set of core dashicons. |
| 37 | * |
| 38 | * @var array |
| 39 | */ |
| 40 | private $dashicon_list; |
| 41 | |
| 42 | /** |
| 43 | * WPCOM_REST_API_V2_Endpoint_Admin_Menu constructor. |
| 44 | */ |
| 45 | public function __construct() { |
| 46 | add_action( 'rest_api_init', array( $this, 'register_routes' ) ); |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * Register routes. |
| 51 | */ |
| 52 | public function register_routes() { |
| 53 | register_rest_route( |
| 54 | $this->namespace, |
| 55 | $this->rest_base . '/', |
| 56 | array( |
| 57 | array( |
| 58 | 'methods' => WP_REST_Server::READABLE, |
| 59 | 'callback' => array( $this, 'get_item' ), |
| 60 | 'permission_callback' => array( $this, 'get_item_permissions_check' ), |
| 61 | ), |
| 62 | 'schema' => array( $this, 'get_public_item_schema' ), |
| 63 | ) |
| 64 | ); |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * Checks if a given request has access to admin menus. |
| 69 | * |
| 70 | * @param WP_REST_Request $request Full details about the request. |
| 71 | * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. |
| 72 | */ |
| 73 | public function get_item_permissions_check( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable |
| 74 | if ( ! current_user_can( 'read' ) ) { |
| 75 | return new WP_Error( |
| 76 | 'rest_forbidden', |
| 77 | __( 'Sorry, you are not allowed to view menus on this site.', 'jetpack' ), |
| 78 | array( 'status' => rest_authorization_required_code() ) |
| 79 | ); |
| 80 | } |
| 81 | |
| 82 | return true; |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * Retrieves the admin menu. |
| 87 | * |
| 88 | * @param WP_REST_Request $request Full details about the request. |
| 89 | * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. |
| 90 | */ |
| 91 | public function get_item( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable |
| 92 | |
| 93 | /* |
| 94 | * Load the `Jetpack_Admin` class, since it's only loaded on admin requests (not on API requests), and this is where |
| 95 | * many Jetpack menus are registered. We don't need to run this on WPCOM because we replicate an admin request there. |
| 96 | * |
| 97 | * @see https://github.com/Automattic/jetpack/blob/dcdeb8fe772215b514bbbd6c4ddb38f6446e7ea1/projects/plugins/jetpack/load-jetpack.php#L61-L64 |
| 98 | * @see https://github.com/Automattic/jetpack/blob/dcdeb8fe772215b514bbbd6c4ddb38f6446e7ea1/projects/plugins/wpcomsh/feature-plugins/masterbar.php#L29 |
| 99 | */ |
| 100 | if ( ! ( new Host() )->is_wpcom_platform() ) { |
| 101 | require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php'; |
| 102 | } |
| 103 | |
| 104 | // All globals need to be declared for menu items to properly register. |
| 105 | global $admin_page_hooks, $menu, $menu_order, $submenu, $_wp_menu_nopriv, $_wp_submenu_nopriv; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable |
| 106 | |
| 107 | require_once ABSPATH . 'wp-admin/includes/admin.php'; |
| 108 | require_once ABSPATH . 'wp-admin/menu.php'; |
| 109 | |
| 110 | return rest_ensure_response( $this->prepare_menu_for_response( $menu ) ); |
| 111 | } |
| 112 | |
| 113 | /** |
| 114 | * Prepares the admin menu for the REST response. |
| 115 | * |
| 116 | * @param array $menu Admin menu. |
| 117 | * @return array Admin menu |
| 118 | */ |
| 119 | public function prepare_menu_for_response( array $menu ) { |
| 120 | global $submenu; |
| 121 | |
| 122 | $data = array(); |
| 123 | |
| 124 | /** |
| 125 | * Note: if the shape of the API endpoint data changes it is important to also update |
| 126 | * the corresponding schema.js file. |
| 127 | * See: https://github.com/Automattic/wp-calypso/blob/ebde236ec9b21ea9621c0b0523bd5ea185523731/client/state/admin-menu/schema.js |
| 128 | */ |
| 129 | foreach ( $menu as $menu_item ) { |
| 130 | $item = $this->prepare_menu_item( $menu_item ); |
| 131 | |
| 132 | // Are there submenu items to process? |
| 133 | if ( ! empty( $submenu[ $menu_item[2] ] ) ) { |
| 134 | $submenu_items = array_values( $submenu[ $menu_item[2] ] ); |
| 135 | |
| 136 | // Add submenu items. |
| 137 | foreach ( $submenu_items as $submenu_item ) { |
| 138 | // As $submenu_item can be null or false due to combination of plugins/themes, its value |
| 139 | // must be checked before passing it to the prepare_submenu_item method. It may be related |
| 140 | // to the common usage of null as a "hidden" submenu item like was fixed in CRM in #29945. |
| 141 | if ( ! is_array( $submenu_item ) ) { |
| 142 | continue; |
| 143 | } |
| 144 | $submenu_item = $this->prepare_submenu_item( $submenu_item, $menu_item ); |
| 145 | if ( ! empty( $submenu_item ) ) { |
| 146 | $item['children'][] = $submenu_item; |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | if ( ! empty( $item ) ) { |
| 152 | $data[] = $item; |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | return array_filter( $data ); |
| 157 | } |
| 158 | |
| 159 | /** |
| 160 | * Retrieves the admin menu's schema, conforming to JSON Schema. |
| 161 | * |
| 162 | * Note: if the shape of the API endpoint data changes it is important to also update |
| 163 | * the corresponding schema.js file. |
| 164 | * |
| 165 | * @see https://github.com/Automattic/wp-calypso/blob/ebde236ec9b21ea9621c0b0523bd5ea185523731/client/state/admin-menu/schema.js |
| 166 | * |
| 167 | * @return array Item schema data. |
| 168 | */ |
| 169 | public function get_item_schema() { |
| 170 | return array( |
| 171 | '$schema' => 'http://json-schema.org/draft-04/schema#', |
| 172 | 'title' => 'Admin Menu', |
| 173 | 'type' => 'object', |
| 174 | 'properties' => array( |
| 175 | 'count' => array( |
| 176 | 'description' => 'Core/Plugin/Theme update count or unread comments count.', |
| 177 | 'type' => 'integer', |
| 178 | ), |
| 179 | 'icon' => array( |
| 180 | 'description' => 'Menu item icon. Dashicon slug or base64-encoded SVG.', |
| 181 | 'type' => 'string', |
| 182 | ), |
| 183 | 'inlineText' => array( |
| 184 | 'description' => 'Additional text to be added inline with the menu title.', |
| 185 | 'type' => 'string', |
| 186 | ), |
| 187 | 'badge' => array( |
| 188 | 'description' => 'Badge to be added inline with the menu title.', |
| 189 | 'type' => 'string', |
| 190 | ), |
| 191 | 'slug' => array( |
| 192 | 'type' => 'string', |
| 193 | ), |
| 194 | 'children' => array( |
| 195 | 'items' => array( |
| 196 | 'count' => array( |
| 197 | 'description' => 'Core/Plugin/Theme update count or unread comments count.', |
| 198 | 'type' => 'integer', |
| 199 | ), |
| 200 | 'parent' => array( |
| 201 | 'type' => 'string', |
| 202 | ), |
| 203 | 'slug' => array( |
| 204 | 'type' => 'string', |
| 205 | ), |
| 206 | 'title' => array( |
| 207 | 'type' => 'string', |
| 208 | ), |
| 209 | 'type' => array( |
| 210 | 'enum' => array( 'submenu-item' ), |
| 211 | 'type' => 'string', |
| 212 | ), |
| 213 | 'url' => array( |
| 214 | 'format' => 'uri', |
| 215 | 'type' => 'string', |
| 216 | ), |
| 217 | ), |
| 218 | 'type' => 'array', |
| 219 | ), |
| 220 | 'title' => array( |
| 221 | 'type' => 'string', |
| 222 | ), |
| 223 | 'type' => array( |
| 224 | 'enum' => array( 'separator', 'menu-item' ), |
| 225 | 'type' => 'string', |
| 226 | ), |
| 227 | 'url' => array( |
| 228 | 'format' => 'uri', |
| 229 | 'type' => 'string', |
| 230 | ), |
| 231 | ), |
| 232 | ); |
| 233 | } |
| 234 | |
| 235 | /** |
| 236 | * Sets up a menu item for consumption by Calypso. |
| 237 | * |
| 238 | * @param array $menu_item Menu item. |
| 239 | * @return array Prepared menu item. |
| 240 | */ |
| 241 | private function prepare_menu_item( array $menu_item ) { |
| 242 | global $submenu; |
| 243 | |
| 244 | $current_user_can_access_menu = current_user_can( $menu_item[1] ); |
| 245 | $submenu_items = isset( $submenu[ $menu_item[2] ] ) ? array_values( $submenu[ $menu_item[2] ] ) : array(); |
| 246 | $has_first_menu_item = isset( $submenu_items[0] ); |
| 247 | |
| 248 | // Exclude unauthorized menu items when the user does not have access to the menu and the first submenu item. |
| 249 | if ( ! $current_user_can_access_menu && $has_first_menu_item && ! current_user_can( $submenu_items[0][1] ) ) { |
| 250 | return array(); |
| 251 | } |
| 252 | |
| 253 | // Exclude unauthorized menu items that don't have submenus. |
| 254 | if ( ! $current_user_can_access_menu && ! $has_first_menu_item ) { |
| 255 | return array(); |
| 256 | } |
| 257 | |
| 258 | // Exclude hidden menu items. |
| 259 | if ( str_contains( $menu_item[4], 'hide-if-js' ) ) { |
| 260 | // Exclude submenu items as well. |
| 261 | if ( ! empty( $submenu[ $menu_item[2] ] ) ) { |
| 262 | // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited |
| 263 | $submenu[ $menu_item[2] ] = array(); |
| 264 | } |
| 265 | return array(); |
| 266 | } |
| 267 | |
| 268 | // Handle menu separators. |
| 269 | if ( str_contains( $menu_item[4], 'wp-menu-separator' ) ) { |
| 270 | return array( |
| 271 | 'type' => 'separator', |
| 272 | ); |
| 273 | } |
| 274 | |
| 275 | $url = $menu_item[2]; |
| 276 | $parent_slug = ''; |
| 277 | |
| 278 | // If there are submenus, the parent menu should always link to the first submenu. |
| 279 | // @see https://core.trac.wordpress.org/browser/trunk/src/wp-admin/menu-header.php?rev=49193#L152. |
| 280 | if ( ! empty( $submenu[ $menu_item[2] ] ) ) { |
| 281 | $parent_slug = $url; |
| 282 | $first_submenu_item = reset( $submenu[ $menu_item[2] ] ); |
| 283 | $url = $first_submenu_item[2]; |
| 284 | } |
| 285 | |
| 286 | $item = array( |
| 287 | 'icon' => $this->prepare_menu_item_icon( $menu_item[6] ), |
| 288 | 'slug' => sanitize_title_with_dashes( $menu_item[2] ), |
| 289 | 'title' => $menu_item[0], |
| 290 | 'type' => 'menu-item', |
| 291 | 'url' => $this->prepare_menu_item_url( $url, $parent_slug ), |
| 292 | ); |
| 293 | |
| 294 | $parsed_item = $this->parse_menu_item( $item['title'] ); |
| 295 | if ( ! empty( $parsed_item ) ) { |
| 296 | $item = array_merge( $item, $parsed_item ); |
| 297 | } |
| 298 | |
| 299 | return $item; |
| 300 | } |
| 301 | |
| 302 | /** |
| 303 | * Sets up a submenu item for consumption by Calypso. |
| 304 | * |
| 305 | * @param array $submenu_item Submenu item. |
| 306 | * @param array $menu_item Menu item. |
| 307 | * @return array Prepared submenu item. |
| 308 | */ |
| 309 | private function prepare_submenu_item( array $submenu_item, array $menu_item ) { |
| 310 | // Exclude unauthorized submenu items. |
| 311 | if ( ! current_user_can( $submenu_item[1] ) ) { |
| 312 | return array(); |
| 313 | } |
| 314 | |
| 315 | // Exclude hidden submenu items. |
| 316 | if ( isset( $submenu_item[4] ) && str_contains( $submenu_item[4], 'hide-if-js' ) ) { |
| 317 | return array(); |
| 318 | } |
| 319 | |
| 320 | $item = array( |
| 321 | 'parent' => sanitize_title_with_dashes( $menu_item[2] ), |
| 322 | 'slug' => sanitize_title_with_dashes( $submenu_item[2] ), |
| 323 | 'title' => $submenu_item[0], |
| 324 | 'type' => 'submenu-item', |
| 325 | 'url' => $this->prepare_menu_item_url( $submenu_item[2], $menu_item[2] ), |
| 326 | ); |
| 327 | |
| 328 | $parsed_item = $this->parse_menu_item( $item['title'] ); |
| 329 | if ( ! empty( $parsed_item ) ) { |
| 330 | $item = array_merge( $item, $parsed_item ); |
| 331 | } |
| 332 | |
| 333 | return $item; |
| 334 | } |
| 335 | |
| 336 | /** |
| 337 | * Prepares a menu icon for consumption by Calypso. |
| 338 | * |
| 339 | * @param string $icon Menu icon. |
| 340 | * @return string |
| 341 | */ |
| 342 | private function prepare_menu_item_icon( $icon ) { |
| 343 | $img = 'dashicons-admin-generic'; |
| 344 | |
| 345 | if ( ! empty( $icon ) && 'none' !== $icon && 'div' !== $icon ) { |
| 346 | $img = esc_url( $icon ); |
| 347 | |
| 348 | if ( str_starts_with( $icon, 'data:image/svg+xml' ) ) { |
| 349 | $img = $icon; |
| 350 | } elseif ( str_starts_with( $icon, 'dashicons-' ) ) { |
| 351 | $img = $this->prepare_dashicon( $icon ); |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | return $img; |
| 356 | } |
| 357 | |
| 358 | /** |
| 359 | * Prepares the dashicon for consumption by Calypso. If the dashicon isn't found in a list of known icons |
| 360 | * we will return the default dashicon. |
| 361 | * |
| 362 | * @param string $icon The dashicon string to check. |
| 363 | * |
| 364 | * @return string If the dashicon exists in core we return the dashicon, otherwise we return the default dashicon. |
| 365 | */ |
| 366 | private function prepare_dashicon( $icon ) { |
| 367 | if ( empty( $this->dashicon_set ) ) { |
| 368 | $this->dashicon_list = include JETPACK__PLUGIN_DIR . 'jetpack_vendor/automattic/jetpack-masterbar/src/admin-menu/dashicon-set.php'; |
| 369 | } |
| 370 | |
| 371 | if ( isset( $this->dashicon_list[ $icon ] ) && $this->dashicon_list[ $icon ] ) { |
| 372 | return $icon; |
| 373 | } |
| 374 | |
| 375 | return 'dashicons-admin-generic'; |
| 376 | } |
| 377 | |
| 378 | /** |
| 379 | * Prepares a menu item url for consumption by Calypso. |
| 380 | * |
| 381 | * @param string $url Menu slug. |
| 382 | * @param string $parent_slug Optional. Parent menu item slug. Default empty string. |
| 383 | * @return string |
| 384 | */ |
| 385 | private function prepare_menu_item_url( $url, $parent_slug = '' ) { |
| 386 | // External URLS. |
| 387 | if ( preg_match( '/^https?:\/\//', $url ) ) { |
| 388 | // Allow URLs pointing to WordPress.com. |
| 389 | if ( str_starts_with( $url, 'https://wordpress.com/' ) ) { |
| 390 | // Calypso needs the domain removed so they're not interpreted as external links. |
| 391 | $url = str_replace( 'https://wordpress.com', '', $url ); |
| 392 | // Replace special characters with their correct entities e.g. & to &. |
| 393 | return wp_specialchars_decode( esc_url_raw( $url ) ); |
| 394 | } |
| 395 | |
| 396 | // Allow URLs pointing to Jetpack.com. |
| 397 | if ( str_starts_with( $url, 'https://jetpack.com/' ) ) { |
| 398 | // Replace special characters with their correct entities e.g. & to &. |
| 399 | return wp_specialchars_decode( esc_url_raw( $url ) ); |
| 400 | } |
| 401 | |
| 402 | // Disallow other external URLs. |
| 403 | if ( ! str_starts_with( $url, get_site_url() ) ) { |
| 404 | return ''; |
| 405 | } |
| 406 | // The URL matches that of the site, treat it as an internal URL. |
| 407 | } |
| 408 | |
| 409 | // Internal URLs. |
| 410 | $menu_hook = get_plugin_page_hook( $url, $parent_slug ); |
| 411 | $menu_file = wp_parse_url( $url, PHP_URL_PATH ); // Removes query args to get a file name. |
| 412 | $parent_file = wp_parse_url( $parent_slug, PHP_URL_PATH ); |
| 413 | |
| 414 | if ( |
| 415 | ! empty( $menu_hook ) || |
| 416 | ( |
| 417 | 'index.php' !== $url && |
| 418 | file_exists( WP_PLUGIN_DIR . "/$menu_file" ) && |
| 419 | ! file_exists( ABSPATH . "/wp-admin/$menu_file" ) |
| 420 | ) |
| 421 | ) { |
| 422 | $admin_is_parent = false; |
| 423 | if ( ! empty( $parent_slug ) ) { |
| 424 | $menu_hook = get_plugin_page_hook( $parent_slug, 'admin.php' ); |
| 425 | $admin_is_parent = ! empty( $menu_hook ) || ( ( 'index.php' !== $parent_slug ) && file_exists( WP_PLUGIN_DIR . "/$parent_file" ) && ! file_exists( ABSPATH . "/wp-admin/$parent_file" ) ); |
| 426 | } |
| 427 | |
| 428 | if ( |
| 429 | ( false === $admin_is_parent && file_exists( WP_PLUGIN_DIR . "/$parent_file" ) && ! is_dir( WP_PLUGIN_DIR . "/$parent_file" ) ) || |
| 430 | ( file_exists( ABSPATH . "/wp-admin/$parent_file" ) && ! is_dir( ABSPATH . "/wp-admin/$parent_file" ) ) |
| 431 | ) { |
| 432 | $url = add_query_arg( array( 'page' => $url ), admin_url( $parent_slug ) ); |
| 433 | } else { |
| 434 | $url = add_query_arg( array( 'page' => $url ), admin_url( 'admin.php' ) ); |
| 435 | } |
| 436 | } elseif ( file_exists( ABSPATH . "/wp-admin/$menu_file" ) ) { |
| 437 | $url = admin_url( $url ); |
| 438 | } |
| 439 | |
| 440 | return wp_specialchars_decode( esc_url_raw( $url ) ); |
| 441 | } |
| 442 | |
| 443 | /** |
| 444 | * "Plugins", "Comments", "Updates" menu items have a count badge when there are updates available. |
| 445 | * This method parses that information, removes the associated markup and adds it to the response. |
| 446 | * |
| 447 | * Also sanitizes the titles from remaining unexpected markup. |
| 448 | * |
| 449 | * @param string $title Title to parse. |
| 450 | * @return array |
| 451 | */ |
| 452 | private function parse_menu_item( $title ) { |
| 453 | // Handle non-string input |
| 454 | if ( ! is_string( $title ) ) { |
| 455 | return array(); |
| 456 | } |
| 457 | |
| 458 | $item = array(); |
| 459 | |
| 460 | if ( |
| 461 | str_contains( $title, 'count-' ) |
| 462 | && preg_match( '/<span class=".+\s?count-(\d*).+\s?<\/span><\/span>/', $title, $matches ) |
| 463 | ) { |
| 464 | |
| 465 | $count = (int) ( $matches[1] ); |
| 466 | if ( $count > 0 ) { |
| 467 | // Keep the counter in the item array. |
| 468 | $item['count'] = $count; |
| 469 | } |
| 470 | |
| 471 | // Finally remove the markup. |
| 472 | $title = trim( str_replace( $matches[0], '', $title ) ); |
| 473 | } |
| 474 | |
| 475 | if ( |
| 476 | str_contains( $title, 'inline-text' ) |
| 477 | && preg_match( '/<span class="inline-text".+\s?>(.+)<\/span>/', $title, $matches ) |
| 478 | ) { |
| 479 | |
| 480 | $text = $matches[1]; |
| 481 | if ( $text ) { |
| 482 | // Keep the text in the item array. |
| 483 | $item['inlineText'] = $text; |
| 484 | } |
| 485 | |
| 486 | // Finally remove the markup. |
| 487 | $title = trim( str_replace( $matches[0], '', $title ) ); |
| 488 | } |
| 489 | |
| 490 | if ( |
| 491 | str_contains( $title, 'awaiting-mod' ) |
| 492 | && preg_match( '/<span class="awaiting-mod">(.+)<\/span>/', $title, $matches ) |
| 493 | ) { |
| 494 | |
| 495 | $text = $matches[1]; |
| 496 | if ( $text ) { |
| 497 | // Keep the text in the item array. |
| 498 | $item['badge'] = $text; |
| 499 | } |
| 500 | |
| 501 | // Finally remove the markup. |
| 502 | $title = trim( str_replace( $matches[0], '', $title ) ); |
| 503 | } |
| 504 | |
| 505 | // It's important we sanitize the title after parsing data to remove any unexpected markup but keep the content. |
| 506 | // We are also capitalizing the first letter in case there was a counter (now parsed) in front of the title. |
| 507 | $item['title'] = ucfirst( wp_strip_all_tags( $title ) ); |
| 508 | |
| 509 | return $item; |
| 510 | } |
| 511 | } |
| 512 | |
| 513 | wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Admin_Menu' ); |