PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 12.4.2
Jetpack – WP Security, Backup, Speed, & Growth v12.4.2
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
487 lines 14.3 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 require_once JETPACK__PLUGIN_DIR . '/modules/masterbar/admin-menu/load.php';
87
88 // All globals need to be declared for menu items to properly register.
89 global $admin_page_hooks, $menu, $menu_order, $submenu, $_wp_menu_nopriv, $_wp_submenu_nopriv; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
90
91 require_once ABSPATH . 'wp-admin/includes/admin.php';
92 require_once ABSPATH . 'wp-admin/menu.php';
93
94 return rest_ensure_response( $this->prepare_menu_for_response( $menu ) );
95 }
96
97 /**
98 * Prepares the admin menu for the REST response.
99 *
100 * @param array $menu Admin menu.
101 * @return array Admin menu
102 */
103 public function prepare_menu_for_response( array $menu ) {
104 global $submenu;
105
106 $data = array();
107
108 /**
109 * Note: if the shape of the API endpoint data changes it is important to also update
110 * the corresponding schema.js file.
111 * See: https://github.com/Automattic/wp-calypso/blob/ebde236ec9b21ea9621c0b0523bd5ea185523731/client/state/admin-menu/schema.js
112 */
113 foreach ( $menu as $menu_item ) {
114 $item = $this->prepare_menu_item( $menu_item );
115
116 // Are there submenu items to process?
117 if ( ! empty( $submenu[ $menu_item[2] ] ) ) {
118 $submenu_items = array_values( $submenu[ $menu_item[2] ] );
119
120 // Add submenu items.
121 foreach ( $submenu_items as $submenu_item ) {
122 $submenu_item = $this->prepare_submenu_item( $submenu_item, $menu_item );
123 if ( ! empty( $submenu_item ) ) {
124 $item['children'][] = $submenu_item;
125 }
126 }
127 }
128
129 if ( ! empty( $item ) ) {
130 $data[] = $item;
131 }
132 }
133
134 return array_filter( $data );
135 }
136
137 /**
138 * Retrieves the admin menu's schema, conforming to JSON Schema.
139 *
140 * Note: if the shape of the API endpoint data changes it is important to also update
141 * the corresponding schema.js file.
142 *
143 * @see https://github.com/Automattic/wp-calypso/blob/ebde236ec9b21ea9621c0b0523bd5ea185523731/client/state/admin-menu/schema.js
144 *
145 * @return array Item schema data.
146 */
147 public function get_item_schema() {
148 return array(
149 '$schema' => 'http://json-schema.org/draft-04/schema#',
150 'title' => 'Admin Menu',
151 'type' => 'object',
152 'properties' => array(
153 'count' => array(
154 'description' => 'Core/Plugin/Theme update count or unread comments count.',
155 'type' => 'integer',
156 ),
157 'icon' => array(
158 'description' => 'Menu item icon. Dashicon slug or base64-encoded SVG.',
159 'type' => 'string',
160 ),
161 'inlineText' => array(
162 'description' => 'Additional text to be added inline with the menu title.',
163 'type' => 'string',
164 ),
165 'badge' => array(
166 'description' => 'Badge to be added inline with the menu title.',
167 'type' => 'string',
168 ),
169 'slug' => array(
170 'type' => 'string',
171 ),
172 'children' => array(
173 'items' => array(
174 'count' => array(
175 'description' => 'Core/Plugin/Theme update count or unread comments count.',
176 'type' => 'integer',
177 ),
178 'parent' => array(
179 'type' => 'string',
180 ),
181 'slug' => array(
182 'type' => 'string',
183 ),
184 'title' => array(
185 'type' => 'string',
186 ),
187 'type' => array(
188 'enum' => array( 'submenu-item' ),
189 'type' => 'string',
190 ),
191 'url' => array(
192 'format' => 'uri',
193 'type' => 'string',
194 ),
195 ),
196 'type' => 'array',
197 ),
198 'title' => array(
199 'type' => 'string',
200 ),
201 'type' => array(
202 'enum' => array( 'separator', 'menu-item' ),
203 'type' => 'string',
204 ),
205 'url' => array(
206 'format' => 'uri',
207 'type' => 'string',
208 ),
209 ),
210 );
211 }
212
213 /**
214 * Sets up a menu item for consumption by Calypso.
215 *
216 * @param array $menu_item Menu item.
217 * @return array Prepared menu item.
218 */
219 private function prepare_menu_item( array $menu_item ) {
220 global $submenu;
221
222 $current_user_can_access_menu = current_user_can( $menu_item[1] );
223 $submenu_items = isset( $submenu[ $menu_item[2] ] ) ? array_values( $submenu[ $menu_item[2] ] ) : array();
224 $has_first_menu_item = isset( $submenu_items[0] );
225
226 // Exclude unauthorized menu items when the user does not have access to the menu and the first submenu item.
227 if ( ! $current_user_can_access_menu && $has_first_menu_item && ! current_user_can( $submenu_items[0][1] ) ) {
228 return array();
229 }
230
231 // Exclude unauthorized menu items that don't have submenus.
232 if ( ! $current_user_can_access_menu && ! $has_first_menu_item ) {
233 return array();
234 }
235
236 // Exclude hidden menu items.
237 if ( false !== strpos( $menu_item[4], 'hide-if-js' ) ) {
238 // Exclude submenu items as well.
239 if ( ! empty( $submenu[ $menu_item[2] ] ) ) {
240 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
241 $submenu[ $menu_item[2] ] = array();
242 }
243 return array();
244 }
245
246 // Handle menu separators.
247 if ( false !== strpos( $menu_item[4], 'wp-menu-separator' ) ) {
248 return array(
249 'type' => 'separator',
250 );
251 }
252
253 $url = $menu_item[2];
254 $parent_slug = '';
255
256 // If there are submenus, the parent menu should always link to the first submenu.
257 // @see https://core.trac.wordpress.org/browser/trunk/src/wp-admin/menu-header.php?rev=49193#L152.
258 if ( ! empty( $submenu[ $menu_item[2] ] ) ) {
259 $parent_slug = $url;
260 $first_submenu_item = reset( $submenu[ $menu_item[2] ] );
261 $url = $first_submenu_item[2];
262 }
263
264 $item = array(
265 'icon' => $this->prepare_menu_item_icon( $menu_item[6] ),
266 'slug' => sanitize_title_with_dashes( $menu_item[2] ),
267 'title' => $menu_item[0],
268 'type' => 'menu-item',
269 'url' => $this->prepare_menu_item_url( $url, $parent_slug ),
270 );
271
272 $parsed_item = $this->parse_menu_item( $item['title'] );
273 if ( ! empty( $parsed_item ) ) {
274 $item = array_merge( $item, $parsed_item );
275 }
276
277 return $item;
278 }
279
280 /**
281 * Sets up a submenu item for consumption by Calypso.
282 *
283 * @param array $submenu_item Submenu item.
284 * @param array $menu_item Menu item.
285 * @return array Prepared submenu item.
286 */
287 private function prepare_submenu_item( array $submenu_item, array $menu_item ) {
288 // Exclude unauthorized submenu items.
289 if ( ! current_user_can( $submenu_item[1] ) ) {
290 return array();
291 }
292
293 // Exclude hidden submenu items.
294 if ( isset( $submenu_item[4] ) && false !== strpos( $submenu_item[4], 'hide-if-js' ) ) {
295 return array();
296 }
297
298 $item = array(
299 'parent' => sanitize_title_with_dashes( $menu_item[2] ),
300 'slug' => sanitize_title_with_dashes( $submenu_item[2] ),
301 'title' => $submenu_item[0],
302 'type' => 'submenu-item',
303 'url' => $this->prepare_menu_item_url( $submenu_item[2], $menu_item[2] ),
304 );
305
306 $parsed_item = $this->parse_menu_item( $item['title'] );
307 if ( ! empty( $parsed_item ) ) {
308 $item = array_merge( $item, $parsed_item );
309 }
310
311 return $item;
312 }
313
314 /**
315 * Prepares a menu icon for consumption by Calypso.
316 *
317 * @param string $icon Menu icon.
318 * @return string
319 */
320 private function prepare_menu_item_icon( $icon ) {
321 $img = 'dashicons-admin-generic';
322
323 if ( ! empty( $icon ) && 'none' !== $icon && 'div' !== $icon ) {
324 $img = esc_url( $icon );
325
326 if ( 0 === strpos( $icon, 'data:image/svg+xml' ) ) {
327 $img = $icon;
328 } elseif ( 0 === strpos( $icon, 'dashicons-' ) ) {
329 $img = $this->prepare_dashicon( $icon );
330 }
331 }
332
333 return $img;
334 }
335
336 /**
337 * Prepares the dashicon for consumption by Calypso. If the dashicon isn't found in a list of known icons
338 * we will return the default dashicon.
339 *
340 * @param string $icon The dashicon string to check.
341 *
342 * @return string If the dashicon exists in core we return the dashicon, otherwise we return the default dashicon.
343 */
344 private function prepare_dashicon( $icon ) {
345 if ( empty( $this->dashicon_set ) ) {
346 $this->dashicon_list = include JETPACK__PLUGIN_DIR . '/modules/masterbar/admin-menu/dashicon-set.php';
347 }
348
349 if ( isset( $this->dashicon_list[ $icon ] ) && $this->dashicon_list[ $icon ] ) {
350 return $icon;
351 }
352
353 return 'dashicons-admin-generic';
354 }
355
356 /**
357 * Prepares a menu item url for consumption by Calypso.
358 *
359 * @param string $url Menu slug.
360 * @param string $parent_slug Optional. Parent menu item slug. Default empty string.
361 * @return string
362 */
363 private function prepare_menu_item_url( $url, $parent_slug = '' ) {
364 // External URLS.
365 if ( preg_match( '/^https?:\/\//', $url ) ) {
366 // Allow URLs pointing to WordPress.com.
367 if ( 0 === strpos( $url, 'https://wordpress.com/' ) ) {
368 // Calypso needs the domain removed so they're not interpreted as external links.
369 $url = str_replace( 'https://wordpress.com', '', $url );
370 // Replace special characters with their correct entities e.g. &amp; to &.
371 return wp_specialchars_decode( esc_url_raw( $url ) );
372 }
373
374 // Allow URLs pointing to Jetpack.com.
375 if ( 0 === strpos( $url, 'https://jetpack.com/' ) ) {
376 // Replace special characters with their correct entities e.g. &amp; to &.
377 return wp_specialchars_decode( esc_url_raw( $url ) );
378 }
379
380 // Disallow other external URLs.
381 if ( 0 !== strpos( $url, get_site_url() ) ) {
382 return '';
383 }
384 // The URL matches that of the site, treat it as an internal URL.
385 }
386
387 // Internal URLs.
388 $menu_hook = get_plugin_page_hook( $url, $parent_slug );
389 $menu_file = wp_parse_url( $url, PHP_URL_PATH ); // Removes query args to get a file name.
390 $parent_file = wp_parse_url( $parent_slug, PHP_URL_PATH );
391
392 if (
393 ! empty( $menu_hook ) ||
394 (
395 'index.php' !== $url &&
396 file_exists( WP_PLUGIN_DIR . "/$menu_file" ) &&
397 ! file_exists( ABSPATH . "/wp-admin/$menu_file" )
398 )
399 ) {
400 $admin_is_parent = false;
401 if ( ! empty( $parent_slug ) ) {
402 $menu_hook = get_plugin_page_hook( $parent_slug, 'admin.php' );
403 $admin_is_parent = ! empty( $menu_hook ) || ( ( 'index.php' !== $parent_slug ) && file_exists( WP_PLUGIN_DIR . "/$parent_file" ) && ! file_exists( ABSPATH . "/wp-admin/$parent_file" ) );
404 }
405
406 if (
407 ( false === $admin_is_parent && file_exists( WP_PLUGIN_DIR . "/$parent_file" ) && ! is_dir( WP_PLUGIN_DIR . "/$parent_file" ) ) ||
408 ( file_exists( ABSPATH . "/wp-admin/$parent_file" ) && ! is_dir( ABSPATH . "/wp-admin/$parent_file" ) )
409 ) {
410 $url = add_query_arg( array( 'page' => $url ), admin_url( $parent_slug ) );
411 } else {
412 $url = add_query_arg( array( 'page' => $url ), admin_url( 'admin.php' ) );
413 }
414 } elseif ( file_exists( ABSPATH . "/wp-admin/$menu_file" ) ) {
415 $url = admin_url( $url );
416 }
417
418 return wp_specialchars_decode( esc_url_raw( $url ) );
419 }
420
421 /**
422 * "Plugins", "Comments", "Updates" menu items have a count badge when there are updates available.
423 * This method parses that information, removes the associated markup and adds it to the response.
424 *
425 * Also sanitizes the titles from remaining unexpected markup.
426 *
427 * @param string $title Title to parse.
428 * @return array
429 */
430 private function parse_menu_item( $title ) {
431 $item = array();
432
433 if (
434 false !== strpos( $title, 'count-' )
435 && preg_match( '/<span class=".+\s?count-(\d*).+\s?<\/span><\/span>/', $title, $matches )
436 ) {
437
438 $count = (int) ( $matches[1] );
439 if ( $count > 0 ) {
440 // Keep the counter in the item array.
441 $item['count'] = $count;
442 }
443
444 // Finally remove the markup.
445 $title = trim( str_replace( $matches[0], '', $title ) );
446 }
447
448 if (
449 false !== strpos( $title, 'inline-text' )
450 && preg_match( '/<span class="inline-text".+\s?>(.+)<\/span>/', $title, $matches )
451 ) {
452
453 $text = $matches[1];
454 if ( $text ) {
455 // Keep the text in the item array.
456 $item['inlineText'] = $text;
457 }
458
459 // Finally remove the markup.
460 $title = trim( str_replace( $matches[0], '', $title ) );
461 }
462
463 if (
464 false !== strpos( $title, 'awaiting-mod' )
465 && preg_match( '/<span class="awaiting-mod">(.+)<\/span>/', $title, $matches )
466 ) {
467
468 $text = $matches[1];
469 if ( $text ) {
470 // Keep the text in the item array.
471 $item['badge'] = $text;
472 }
473
474 // Finally remove the markup.
475 $title = trim( str_replace( $matches[0], '', $title ) );
476 }
477
478 // It's important we sanitize the title after parsing data to remove any unexpected markup but keep the content.
479 // We are also capitalizing the first letter in case there was a counter (now parsed) in front of the title.
480 $item['title'] = ucfirst( wp_strip_all_tags( $title ) );
481
482 return $item;
483 }
484 }
485
486 wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Admin_Menu' );
487