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