Completed
Push — update/conversation-remove-par... ( 498b22...5ced53 )
by
unknown
18:38 queued 07:08
created

WPCOM_REST_API_V2_Endpoint_Admin_Menu   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 375
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 1

Importance

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