Completed
Push — fix/parent-selector-for-premiu... ( cac9af...425486 )
by Jeremy
20:54 queued 09:58
created

WPCOM_REST_API_V2_Endpoint_Admin_Menu   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 417
Duplicated Lines 5.76 %

Coupling/Cohesion

Components 2
Dependencies 1

Importance

Changes 0
Metric Value
dl 24
loc 417
rs 5.5199
c 0
b 0
f 0
wmc 56
lcom 2
cbo 1

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A register_routes() 0 14 1
A get_item_permissions_check() 0 11 2
A get_item() 0 15 3
B prepare_menu_for_response() 0 33 6
B get_item_schema() 0 61 1
A prepare_submenu_item() 0 26 5
A prepare_menu_item_icon() 0 15 6
C prepare_menu_item_url() 0 48 14
A parse_menu_item() 24 35 5
C prepare_menu_item() 0 60 12

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WPCOM_REST_API_V2_Endpoint_Admin_Menu often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WPCOM_REST_API_V2_Endpoint_Admin_Menu, and based on these observations, apply Extract Interface, too.

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
	 * WPCOM_REST_API_V2_Endpoint_Admin_Menu constructor.
30
	 */
31
	public function __construct() {
32
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
33
	}
34
35
	/**
36
	 * Register routes.
37
	 */
38
	public function register_routes() {
39
		register_rest_route(
40
			$this->namespace,
41
			$this->rest_base . '/',
42
			array(
43
				array(
44
					'methods'             => WP_REST_Server::READABLE,
45
					'callback'            => array( $this, 'get_item' ),
46
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
47
				),
48
				'schema' => array( $this, 'get_public_item_schema' ),
49
			)
50
		);
51
	}
52
53
	/**
54
	 * Checks if a given request has access to admin menus.
55
	 *
56
	 * @param WP_REST_Request $request Full details about the request.
57
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
58
	 */
59
	public function get_item_permissions_check( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
60
		if ( ! current_user_can( 'read' ) ) {
61
			return new WP_Error(
62
				'rest_forbidden',
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'rest_forbidden'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

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