Completed
Push — renovate/major-react-monorepo ( 880c2b...a6f86c )
by
unknown
355:09 queued 345:32
created

WPCOM_REST_API_V2_Endpoint_Admin_Menu   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 381
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 1

Importance

Changes 0
Metric Value
wmc 45
lcom 2
cbo 1
dl 0
loc 381
rs 8.8
c 0
b 0
f 0

13 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
A prepare_menu_for_response() 0 35 5
B get_item_schema() 0 57 1
A prepare_menu_item() 0 26 4
A prepare_submenu_item() 0 20 3
A prepare_menu_item_icon() 0 15 6
C prepare_menu_item_url() 0 41 13
A parse_count_data() 0 14 3
A sanitize_title() 0 9 2
A parse_markup_data() 0 11 1

How to fix   Complexity   

Complex Class

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
				// If the user doesn't have the caps for the top level menu item, let's promote the first submenu item.
117
				if ( empty( $item ) ) {
118
					$menu_item[1] = $submenu_items[0][1]; // Capability.
119
					$menu_item[2] = $submenu_items[0][2]; // Menu slug.
120
					$item         = $this->prepare_menu_item( $menu_item );
121
				}
122
123
				// Add submenu items.
124
				foreach ( $submenu_items as $submenu_item ) {
125
					$item['children'][] = $this->prepare_submenu_item( $submenu_item, $menu_item );
126
				}
127
			}
128
129
			$data[] = $item;
130
		}
131
132
		return array_filter( $data );
133
	}
134
135
	/**
136
	 * Retrieves the admin menu's schema, conforming to JSON Schema.
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
	 *
141
	 * @see https://github.com/Automattic/wp-calypso/blob/ebde236ec9b21ea9621c0b0523bd5ea185523731/client/state/admin-menu/schema.js
142
	 *
143
	 * @return array Item schema data.
144
	 */
145
	public function get_item_schema() {
146
		return array(
147
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
148
			'title'      => 'Admin Menu',
149
			'type'       => 'object',
150
			'properties' => array(
151
				'count'    => array(
152
					'description' => 'Core/Plugin/Theme update count or unread comments count.',
153
					'type'        => 'integer',
154
				),
155
				'icon'     => array(
156
					'description' => 'Menu item icon. Dashicon slug or base64-encoded SVG.',
157
					'type'        => 'string',
158
				),
159
				'slug'     => array(
160
					'type' => 'string',
161
				),
162
				'children' => array(
163
					'items' => array(
164
						'count'  => array(
165
							'description' => 'Core/Plugin/Theme update count or unread comments count.',
166
							'type'        => 'integer',
167
						),
168
						'parent' => array(
169
							'type' => 'string',
170
						),
171
						'slug'   => array(
172
							'type' => 'string',
173
						),
174
						'title'  => array(
175
							'type' => 'string',
176
						),
177
						'type'   => array(
178
							'enum' => array( 'submenu-item' ),
179
							'type' => 'string',
180
						),
181
						'url'    => array(
182
							'format' => 'uri',
183
							'type'   => 'string',
184
						),
185
					),
186
					'type'  => 'array',
187
				),
188
				'title'    => array(
189
					'type' => 'string',
190
				),
191
				'type'     => array(
192
					'enum' => array( 'separator', 'menu-item' ),
193
					'type' => 'string',
194
				),
195
				'url'      => array(
196
					'format' => 'uri',
197
					'type'   => 'string',
198
				),
199
			),
200
		);
201
	}
202
203
	/**
204
	 * Sets up a menu item for consumption by Calypso.
205
	 *
206
	 * @param array $menu_item Menu item.
207
	 * @return array Prepared menu item.
208
	 */
209
	private function prepare_menu_item( array $menu_item ) {
210
		if ( ! current_user_can( $menu_item[1] ) ) {
211
			return array();
212
		}
213
214
		if ( false !== strpos( $menu_item[4], 'wp-menu-separator' ) ) {
215
			return array(
216
				'type' => 'separator',
217
			);
218
		}
219
220
		$item = array(
221
			'icon'  => $this->prepare_menu_item_icon( $menu_item[6] ),
222
			'slug'  => sanitize_title_with_dashes( $menu_item[2] ),
223
			'title' => $menu_item[0],
224
			'type'  => 'menu-item',
225
			'url'   => $this->prepare_menu_item_url( $menu_item[2] ),
226
		);
227
228
		$parsed_item = $this->parse_markup_data( $item['title'] );
229
		if ( ! empty( $parsed_item ) ) {
230
			$item = array_merge( $item, $parsed_item );
231
		}
232
233
		return $item;
234
	}
235
236
	/**
237
	 * Sets up a submenu item for consumption by Calypso.
238
	 *
239
	 * @param array $submenu_item Submenu item.
240
	 * @param array $menu_item    Menu item.
241
	 * @return array Prepared submenu item.
242
	 */
243
	private function prepare_submenu_item( array $submenu_item, array $menu_item ) {
244
		$item = array();
245
246
		if ( current_user_can( $submenu_item[1] ) ) {
247
			$item = array(
248
				'parent' => sanitize_title_with_dashes( $menu_item[2] ),
249
				'slug'   => sanitize_title_with_dashes( $submenu_item[2] ),
250
				'title'  => $submenu_item[0],
251
				'type'   => 'submenu-item',
252
				'url'    => $this->prepare_menu_item_url( $submenu_item[2], $menu_item[2] ),
253
			);
254
255
			$parsed_item = $this->parse_markup_data( $item['title'] );
256
			if ( ! empty( $parsed_item ) ) {
257
				$item = array_merge( $item, $parsed_item );
258
			}
259
		}
260
261
		return $item;
262
	}
263
264
	/**
265
	 * Prepares a menu icon for consumption by Calypso.
266
	 *
267
	 * @param string $icon Menu icon.
268
	 * @return string
269
	 */
270
	private function prepare_menu_item_icon( $icon ) {
271
		$img = 'dashicons-admin-generic';
272
273
		if ( ! empty( $icon ) && 'none' !== $icon && 'div' !== $icon ) {
274
			$img = esc_url( $icon );
275
276
			if ( 0 === strpos( $icon, 'data:image/svg+xml' ) ) {
277
				$img = $icon;
278
			} elseif ( 0 === strpos( $icon, 'dashicons-' ) ) {
279
				$img = sanitize_html_class( $icon );
280
			}
281
		}
282
283
		return $img;
284
	}
285
286
	/**
287
	 * Prepares a menu item url for consumption by Calypso.
288
	 *
289
	 * @param string $url         Menu slug.
290
	 * @param string $parent_slug Optional. Parent menu item slug. Default empty string.
291
	 * @return string
292
	 */
293
	private function prepare_menu_item_url( $url, $parent_slug = '' ) {
294
		// External URLS.
295
		if ( preg_match( '/^https?:\/\//', $url ) ) {
296
			// Allow URLs pointing to WordPress.com.
297
			if ( 0 === strpos( $url, 'https://wordpress.com/' ) ) {
298
				// Calypso needs the domain removed so they're not interpreted as external links.
299
				$url = str_replace( 'https://wordpress.com', '', $url );
300
				return esc_url_raw( $url );
301
			}
302
303
			// Disallow other external URLs.
304
			return '';
305
		}
306
307
		// Internal URLs.
308
		$menu_hook   = get_plugin_page_hook( $url, $parent_slug );
309
		$menu_file   = wp_parse_url( $url, PHP_URL_PATH ); // Removes query args to get a file name.
310
		$parent_file = wp_parse_url( $parent_slug, PHP_URL_PATH );
311
312
		if (
313
			! empty( $menu_hook ) ||
314
			(
315
				'index.php' !== $url &&
316
				file_exists( WP_PLUGIN_DIR . "/$menu_file" ) &&
317
				! file_exists( ABSPATH . "/wp-admin/$menu_file" )
318
			)
319
		) {
320
			if (
321
				( 'admin.php' !== $parent_file && file_exists( WP_PLUGIN_DIR . "/$parent_file" ) && ! is_dir( WP_PLUGIN_DIR . "/$parent_file" ) ) ||
322
				( file_exists( ABSPATH . "/wp-admin/$parent_file" ) && ! is_dir( ABSPATH . "/wp-admin/$parent_file" ) )
323
			) {
324
				$url = add_query_arg( array( 'page' => $url ), admin_url( $parent_slug ) );
325
			} else {
326
				$url = add_query_arg( array( 'page' => $url ), admin_url( 'admin.php' ) );
327
			}
328
		} elseif ( file_exists( ABSPATH . "/wp-admin/$menu_file" ) ) {
329
			$url = admin_url( $url );
330
		}
331
332
		return esc_url_raw( $url );
333
	}
334
335
	/**
336
	 * Parses the update count from a given menu item title and removes the associated markup.
337
	 *
338
	 * "Plugin" and "Updates" menu items have a count badge when there are updates available.
339
	 * This method parses that information and adds it to the response.
340
	 *
341
	 * @param array $item containing title to parse.
342
	 * @return array
343
	 */
344
	private function parse_count_data( $item ) {
345
		$title = $item['title'];
346
347
		if ( false !== strpos( $title, 'count-' ) ) {
348
			preg_match( '/class="(.+\s)?count-(\d*)/', $title, $matches );
349
350
			$count = absint( $matches[2] );
351
			if ( $count > 0 ) {
352
				$item['count'] = $count;
353
			}
354
		}
355
356
		return $item;
357
	}
358
359
	/**
360
	 * Removes unexpected markup from the title.
361
	 *
362
	 * @param array $item containing title to parse.
363
	 * @return array
364
	 */
365
	private function sanitize_title( $item ) {
366
		$title = $item['title'];
367
368
		if ( wp_strip_all_tags( $title ) !== trim( $title ) ) {
369
			$item['title'] = trim( substr( $title, 0, strpos( $title, '<' ) ) );
370
		}
371
372
		return $item;
373
	}
374
375
	/**
376
	 * Parses data from the markup in titles and sanitizes titles from unexpected markup.
377
	 *
378
	 * @param string $title Title to parse.
379
	 * @return array
380
	 */
381
	private function parse_markup_data( $title ) {
382
		$item = array(
383
			'title' => $title,
384
		);
385
386
		$item = $this->parse_count_data( $item );
387
		// It's important we sanitize the title after parsing data to remove the markup.
388
		$item = $this->sanitize_title( $item );
389
390
		return $item;
391
	}
392
}
393
394
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Admin_Menu' );
395