Completed
Push — update/dialogue-focus-on-conte... ( 9f1745...fa862f )
by
unknown
80:03 queued 71:18
created

WPCOM_JSON_API_Menus_Widgets   A

Complexity

Total Complexity 5

Size/Duplication

Total Lines 18
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 0

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 10
c 0
b 0
f 0
wmc 5
lcom 0
cbo 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A get() 0 16 5
1
<?php
2
abstract class WPCOM_JSON_API_Menus_Abstract_Endpoint extends WPCOM_JSON_API_Endpoint {
3
4
	protected function switch_to_blog_and_validate_user( $site ) {
5
		$site_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
6
		if ( is_wp_error( $site_id ) ) {
7
			return $site_id;
8
		}
9
10
		if ( ! current_user_can( 'edit_theme_options' ) ) {
11
			return new WP_Error( 'unauthorised', 'User cannot edit theme options on this site.', 403 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unauthorised'.

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...
12
		}
13
14
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
15
			$this->load_theme_functions();
16
		}
17
18
		return $site_id;
19
	}
20
21
22
	protected function get_locations() {
23
		$locations = array();
24
		$menus = get_registered_nav_menus();
25
		if ( !empty( $menus ) ) {
26
			foreach( $menus as $name => $description ) {
27
				$locations[] = array( 'name' => $name, 'description' => $description );
28
			}
29
		}
30
31
		$locations = array_merge( $locations, WPCOM_JSON_API_Menus_Widgets::get() );
32
33
		// Primary (first) location should have defaultState -> default,
34
		// all other locations (including widgets) should have defaultState -> empty.
35
		for ( $i = 0; $i < count( $locations ); $i++ ) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
36
			$locations[ $i ]['defaultState'] = $i ? 'empty' : 'default';
37
		}
38
		return $locations;
39
	}
40
41
	protected function simplify( $data ) {
42
		$simplifier = new WPCOM_JSON_API_Menus_Simplifier( $data );
43
		return $simplifier->translate();
44
	}
45
46
	protected function complexify( $data ) {
47
		$complexifier = new WPCOM_JSON_API_Menus_Complexify( $data );
48
		return $complexifier->translate();
49
	}
50
}
51
52
abstract class WPCOM_JSON_API_Menus_Translator {
53
	protected $filter = '';
54
55
	protected $filters = array();
56
57
	public function __construct( $menus ) {
58
		$this->is_single_menu = ! is_array( $menus );
0 ignored issues
show
Bug introduced by
The property is_single_menu does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
59
		$this->menus = is_array( $menus ) ? $menus : array( $menus );
0 ignored issues
show
Bug introduced by
The property menus does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
60
	}
61
62
	public function translate() {
63
		$result = $this->menus;
64
		foreach ( $this->filters as $f ) {
65
			$result = call_user_func( array( $this, $f ), $result );
66
			if ( is_wp_error($result ) ) {
67
				return $result;
68
			}
69
		}
70
		return $this->maybe_extract( $result );
71
	}
72
73
	protected function maybe_extract( $menus ) {
74
		return $this->is_single_menu ? $menus[0] : $menus;
75
	}
76
77
	public function whitelist_and_rename_with( $object, $dict ) {
78
		$keys = array_keys( $dict );
79
		$return = array();
80
		foreach ( (array) $object as $k => $v ) {
81
			if ( in_array( $k, $keys ) ) {
82
				if ( is_array( $dict[ $k ] ) ) {
83
					settype( $v, $dict[ $k ]['type'] );
84
					$return[ $dict[ $k ]['name'] ] = $v;
85
				} else {
86
					$new_k = $dict[ $k ];
87
					$return[ $new_k ] = $v;
88
				}
89
			}
90
		}
91
		return $return;
92
	}
93
}
94
95
class WPCOM_JSON_API_Menus_Simplifier extends WPCOM_JSON_API_Menus_Translator {
96
	protected $filter = 'wpcom_menu_api_translator_simplify';
97
98
	protected $filters = array(
99
		'whitelist_and_rename_keys',
100
		'add_locations',
101
		'treeify',
102
		'add_widget_locations',
103
	);
104
105
	protected $menu_whitelist = array(
106
		'term_id'       => array( 'name' => 'id', 'type' => 'int' ),
107
		'name'          => array( 'name' => 'name', 'type' => 'string' ),
108
		'description'   => array( 'name' => 'description', 'type' => 'string' ),
109
		'items'         => array( 'name' => 'items', 'type' => 'array' ),
110
	);
111
112
	protected $menu_item_whitelist = array(
113
		'db_id'             => array( 'name' => 'id', 'type' => 'int' ),
114
		'object_id'         => array( 'name' => 'content_id', 'type' => 'int' ),
115
		'object'            => array( 'name' => 'type', 'type' => 'string' ),
116
		'type'              => array( 'name' => 'type_family', 'type' => 'string' ),
117
		'type_label'        => array( 'name' => 'type_label', 'type' => 'string' ),
118
		'title'             => array( 'name' => 'name', 'type' => 'string' ),
119
		'menu_order'        => array( 'name' => 'order', 'type' => 'int' ),
120
		'menu_item_parent'  => array( 'name' => 'parent', 'type' => 'int' ),
121
		'url'               => array( 'name' => 'url', 'type' => 'string' ),
122
		'target'            => array( 'name' => 'link_target', 'type' => 'string' ),
123
		'attr_title'        => array( 'name' => 'link_title', 'type' => 'string' ),
124
		'description'       => array( 'name' => 'description', 'type' => 'string' ),
125
		'classes'           => array( 'name' => 'classes', 'type' => 'array' ),
126
		'xfn'               => array( 'name' => 'xfn', 'type' => 'string' ),
127
	);
128
129
	/**************************
130
	 * Filters methods
131
	 **************************/
132
133
	public function treeify( $menus ) {
134
		return array_map( array( $this, 'treeify_menu' ), $menus );
135
	}
136
137
	// turn the flat item list into a tree of items
138
	protected function treeify_menu( $menu ) {
139
		$indexed_nodes = array();
140
		$tree = array();
141
142
		foreach( $menu['items'] as &$item ) {
143
			$indexed_nodes[ $item['id'] ] = &$item;
144
		}
145
146
		foreach( $menu['items'] as &$item ) {
147
			if ( $item['parent'] && isset( $indexed_nodes[ $item['parent'] ] ) ) {
148
				$parent_node = &$indexed_nodes[ $item['parent'] ];
149
				if ( !isset( $parent_node['items'] ) ) {
150
					$parent_node['items'] = array();
151
				}
152
				$parent_node['items'][ $item['order'] ] = &$item;
153
			} else {
154
				$tree[ $item['order'] ] = &$item;
155
			}
156
			unset( $item['order'] );
157
			unset( $item['parent'] );
158
		}
159
160
		$menu['items'] = $tree;
161
		$this->remove_item_keys( $menu );
162
		return $menu;
163
	}
164
165
	// recursively ensure item lists are contiguous
166 View Code Duplication
	protected function remove_item_keys( &$item ) {
167
		if ( ! isset( $item['items'] ) || ! is_array( $item['items'] ) ) {
168
			return;
169
		}
170
171
172
		foreach( $item['items'] as &$it ) {
173
			$this->remove_item_keys( $it );
174
		}
175
176
		$item['items'] = array_values( $item['items'] );
177
	}
178
179
	protected function whitelist_and_rename_keys( $menus ) {
180
		$transformed_menus = array();
181
182
		foreach ( $menus as $menu ) {
183
			$menu = $this->whitelist_and_rename_with( $menu, $this->menu_whitelist );
184
185
			if ( isset( $menu['items'] ) ) {
186
				foreach ( $menu['items'] as &$item ) {
187
					$item = $this->whitelist_and_rename_with( $item, $this->menu_item_whitelist );
188
				}
189
			}
190
191
			$transformed_menus[] = $menu;
192
		}
193
194
		return $transformed_menus;
195
	}
196
197
	protected function add_locations( $menus ) {
198
		$menus_with_locations = array();
199
200
		foreach( $menus as $menu ) {
201
			$menu['locations'] = array_keys( get_nav_menu_locations(), $menu['id'] );
202
			$menus_with_locations[] = $menu;
203
		}
204
205
		return $menus_with_locations;
206
	}
207
208
	protected function add_widget_locations( $menus ) {
209
		$nav_menu_widgets = WPCOM_JSON_API_Menus_Widgets::get();
210
211
		if ( ! is_array( $nav_menu_widgets ) ) {
212
			return $menus;
213
		}
214
215
		foreach ( $menus as &$menu ) {
216
			$widget_locations = array();
217
218 View Code Duplication
			foreach ( $nav_menu_widgets as $key => $widget ) {
219
				if ( is_array( $widget ) && isset( $widget['nav_menu'] ) &&
220
				    $widget['nav_menu'] === $menu['id'] ) {
221
					$widget_locations[] = 'nav_menu_widget-' . $key;
222
				}
223
			}
224
			$menu['locations'] = array_merge( $menu['locations'], $widget_locations );
225
		}
226
227
		return $menus;
228
	}
229
}
230
231
class WPCOM_JSON_API_Menus_Complexify extends WPCOM_JSON_API_Menus_Translator {
232
	protected $filter = 'wpcom_menu_api_translator_complexify';
233
234
	protected $filters = array(
235
		'untreeify',
236
		'set_locations',
237
		'whitelist_and_rename_keys',
238
	);
239
240
	protected $menu_whitelist = array(
241
		'id' => 'term_id',
242
		'name' => 'menu-name',
243
		'description' => 'description',
244
		'items' => 'items',
245
	);
246
247
	protected $menu_item_whitelist = array(
248
		'id' => 'menu-item-db-id',
249
		'content_id' => 'menu-item-object-id',
250
		'type' => 'menu-item-object',
251
		'type_family' => 'menu-item-type',
252
		'type_label' => 'menu-item-type-label',
253
		'name' => 'menu-item-title',
254
		'order' => 'menu-item-position',
255
		'parent' => 'menu-item-parent-id',
256
		'url' => 'menu-item-url',
257
		'link_target' => 'menu-item-target',
258
		'link_title' => 'menu-item-attr-title',
259
		'status' => 'menu-item-status',
260
		'tmp_id' => 'tmp_id',
261
		'tmp_parent' => 'tmp_parent',
262
		'description' => 'menu-item-description',
263
		'classes' => 'menu-item-classes',
264
		'xfn' => 'menu-item-xfn',
265
	);
266
267
	/**************************
268
	 * Filters methods
269
	 **************************/
270
271
	public function untreeify( $menus ) {
272
		return array_map( array( $this, 'untreeify_menu' ), $menus );
273
	}
274
275
	// convert the tree of menu items to a flat list suitable for
276
	// the nav_menu APIs
277
	protected function untreeify_menu( $menu ) {
278
		if ( empty( $menu['items'] ) ) {
279
			return $menu;
280
		}
281
282
		$items_list = array();
283
		$counter = 1;
284
		foreach ( $menu['items'] as &$item ) {
285
			$item[ 'parent' ] = 0;
286
		}
287
		$this->untreeify_items( $menu['items'], $items_list, $counter );
288
		$menu['items'] = $items_list;
289
290
		return $menu;
291
	}
292
293
	/**
294
	 * Recurse the items tree adding each item to a flat list and restoring
295
	 * `order` and `parent` fields.
296
	 *
297
	 * @param array $items item tree
298
	 * @param array &$items_list output flat list of items
299
	 * @param int &$counter for creating temporary IDs
300
	 */
301
	protected function untreeify_items( $items, &$items_list, &$counter ) {
302
		foreach( $items as $index => $item ) {
303
			$item['order'] = $index + 1;
304
305
			if( ! isset( $item['id'] ) ) {
306
				$this->set_tmp_id( $item, $counter++ );
307
			}
308
309
			if ( isset( $item['items'] ) && is_array( $item['items'] ) ) {
310
				foreach ( $item['items'] as &$i ) {
311
					$i['parent'] = $item['id'];
312
				}
313
				$this->untreeify_items( $item[ 'items' ], $items_list, $counter );
314
				unset( $item['items'] );
315
			}
316
317
			$items_list[] = $item;
318
		}
319
	}
320
321
	/**
322
	 * Populate `tmp_id` field for a new item, and `tmp_parent` field
323
	 * for all its children, to maintain the hierarchy.
324
	 * These fields will be used when creating
325
	 * new items with wp_update_nav_menu_item().
326
	 */
327 View Code Duplication
	private function set_tmp_id( &$item, $tmp_id ) {
328
		$item['tmp_id'] = $tmp_id;
329
		if ( ! isset( $item['items'] ) || ! is_array( $item['items'] ) ) {
330
			return;
331
		}
332
		foreach ( $item['items'] as &$child ) {
333
			$child['tmp_parent'] = $tmp_id;
334
		}
335
	}
336
337
	protected function whitelist_and_rename_keys( $menus ) {
338
		$transformed_menus = array();
339
		foreach ( $menus as $menu ) {
340
			$menu = $this->whitelist_and_rename_with( $menu, $this->menu_whitelist );
341
			if ( isset( $menu['items'] ) ) {
342
				$menu['items'] = array_map( array( $this, 'whitelist_and_rename_item_keys' ), $menu['items'] );
343
			}
344
			$transformed_menus[] = $menu;
345
		}
346
347
		return $transformed_menus;
348
	}
349
350
	protected function whitelist_and_rename_item_keys( $item ) {
351
		$item = $this->implode_array_fields( $item );
352
		$item = $this->whitelist_and_rename_with( $item, $this->menu_item_whitelist );
353
		return $item;
354
	}
355
356
	// all item fields are set as strings
357
	protected function implode_array_fields( $menu_item ) {
358
		return array_map( array( $this, 'implode_array_field' ), $menu_item );
359
	}
360
361
	protected function implode_array_field( $field ) {
362
		if ( is_array( $field ) ) {
363
			return implode( ' ', $field );
364
		}
365
		return $field;
366
	}
367
368
	protected function set_locations( $menus ) {
369
		foreach ( $menus as $menu ) {
370
			if ( isset( $menu['locations'] ) ) {
371
				if ( true !== $this->locations_are_valid( $menu['locations'] ) ) {
372
					return $this->locations_are_valid( $menu['locations'] );
373
				}
374
			}
375
		}
376
377
		return array_map( array( $this, 'set_location' ), $menus );
378
	}
379
380
	protected function set_location( $menu ) {
381
		$this->set_menu_at_locations( $menu['locations'], $menu['id'] );
382
		return $menu;
383
	}
384
385
	protected function set_menu_at_locations( $locations, $menu_id ) {
386
		$location_map =  get_nav_menu_locations();
387
		$this->remove_menu_from_all_locations( $menu_id, $location_map );
388
389
		if ( is_array( $locations ) ) {
390
			foreach ( $locations as $location ) {
391
				$location_map[ $location ] = $menu_id;
392
			}
393
		}
394
395
		set_theme_mod( 'nav_menu_locations', $location_map );
396
397
		$this->set_widget_menu_at_locations( $locations, $menu_id );
398
	}
399
400
	protected function remove_menu_from_all_locations( $menu_id, &$location_map ) {
401
		foreach ( get_nav_menu_locations() as $existing_location => $existing_menu_id) {
402
			if ( $existing_menu_id == $menu_id ) {
403
				unset( $location_map[$existing_location] );
404
			}
405
		}
406
	}
407
408
	protected function set_widget_menu_at_locations( $locations, $menu_id ) {
409
		$nav_menu_widgets = get_option( 'widget_nav_menu' );
410
411
		if ( ! is_array( $nav_menu_widgets ) ) {
412
			return;
413
		}
414
415
		// Remove menus from all custom menu widget locations
416 View Code Duplication
		foreach ( $nav_menu_widgets as &$widget ) {
417
			if ( is_array( $widget ) && isset( $widget['nav_menu'] ) &&  $widget['nav_menu'] == $menu_id ) {
418
				$widget['nav_menu'] = 0;
419
			}
420
		}
421
422
		if ( is_array( $locations ) ) {
423
			foreach ( $locations as $location ) {
424
				if ( preg_match( '/^nav_menu_widget-(\d+)/', $location, $matches ) ) {
425
					if ( isset( $matches[1] ) ) {
426
						$nav_menu_widgets[$matches[1]]['nav_menu'] = $menu_id;
427
					}
428
				}
429
			}
430
		}
431
432
		update_option( 'widget_nav_menu', $nav_menu_widgets );
433
	}
434
435
	protected function locations_are_valid( $locations ) {
436
		if ( is_int( $locations ) ) {
437
			if ( $locations != 0) {
438
				return new WP_Error( 'locations-int', 'Locations int must be 0.', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'locations-int'.

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...
439
			} else {
440
				return true;
441
			}
442
		} elseif ( is_array( $locations ) ) {
443
			foreach ( $locations as $location_name ) {
444
				if ( ! $this->location_name_exists( $location_name ) ) {
445
					return new WP_Error( 'locations-array',
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'locations-array'.

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...
446
						sprintf( "Location '%s' does not exist.", $location_name ), 404 );
447
				}
448
			}
449
			return true;
450
		}
451
		return new WP_Error( 'locations', 'Locations must be array or integer.', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'locations'.

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...
452
	}
453
454
	protected function location_name_exists( $location_name ) {
455
		$widget_location_names = wp_list_pluck( WPCOM_JSON_API_Menus_Widgets::get(), 'name' );
456
457
		$existing_locations = get_nav_menu_locations();
458
459
		if ( ! is_array( get_registered_nav_menus() ) ) {
460
			return false;
461
		}
462
463
		return array_key_exists( $location_name, get_registered_nav_menus() ) ||
464
			array_key_exists( $location_name, $existing_locations ) ||
465
			in_array( $location_name, $widget_location_names );
466
	}
467
468
}
469
470
new WPCOM_JSON_API_Menus_New_Menu_Endpoint( array (
471
	'method' => 'POST',
472
	'description' => 'Create a new navigation menu.',
473
	'group' => 'menus',
474
	'stat' => 'menus:new-menu',
475
	'path' => '/sites/%s/menus/new',
476
	'path_labels' => array(
477
		'$site' => '(int|string) Site ID or domain',
478
	),
479
	'request_format'  => array(
480
		'name' => '(string) Name of menu',
481
	),
482
	'response_format' => array(
483
		'id' => '(int) Newly created menu ID',
484
	),
485
	'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/new',
486
	'example_request_data' => array(
487
		'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
488
		'body' => array(
489
			'name' => 'Menu 1'
490
		)
491
	),
492
) );
493
494
class WPCOM_JSON_API_Menus_New_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
495
	function callback( $path = '', $site = 0 ) {
496
		$site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
497
498
		if ( is_wp_error( $site_id ) ) {
499
			return $site_id;
500
		}
501
502
		$data = $this->input();
503
504
		$id = wp_create_nav_menu( $data['name'] );
505
506
		if ( is_wp_error( $id ) ) {
507
			return $id;
508
		}
509
510
		return array( 'id' => $id );
511
	}
512
}
513
514
new WPCOM_JSON_API_Menus_Update_Menu_Endpoint( array (
515
	'method' => 'POST',
516
	'description' => 'Update a navigation menu.',
517
	'group' => 'menus',
518
	'stat' => 'menus:update-menu',
519
	'path' => '/sites/%s/menus/%d',
520
	'path_labels' => array(
521
		'$site' => '(int|string) Site ID or domain',
522
		'$menu_id' => '(int) Menu ID',
523
	),
524
	'request_format'  => array(
525
		'name'  => '(string) Name of menu',
526
		'items' => '(array) A list of menu item objects.
527
			<br/><br/>
528
			Item objects contain fields relating to that item, e.g. id, type, content_id,
529
			but they can also contain other items objects - this nesting represents parents
530
			and child items in the item tree.'
531
	),
532
	'response_format' => array(
533
		'menu' => '(object) Updated menu object',
534
	),
535
	'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/510604099',
536
	'example_request_data' => array(
537
		'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
538
		'body' => array(
539
			'name' => 'Test Menu'
540
		),
541
	),
542
) );
543
544
class WPCOM_JSON_API_Menus_Update_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
545
	function callback( $path = '', $site = 0, $menu_id = 0 ) {
546
		$site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
547
548
		if ( is_wp_error( $site_id ) ) {
549
			return $site_id;
550
		}
551
552
		if ( $menu_id <= 0 ) {
553
			return new WP_Error( 'menu-id', 'Menu ID must be greater than 0.', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'menu-id'.

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...
554
		}
555
556
		$data = $this->input( true, false );
557
		$data['id'] = $menu_id;
558
		$data = $this->complexify( array( $data ) );
559
		if ( is_wp_error( $data ) ) {
560
			return $data;
561
		}
562
		$data = $data[0];
563
564
		// Avoid special-case handling of an unset 'items' field in empty menus
565
		$data['items'] = isset( $data['items'] ) ? $data['items'] : array();
566
567
		$data = $this->create_new_items( $data, $menu_id );
568
569
		$result = wp_update_nav_menu_object( $menu_id, array( 'menu-name' => $data['menu-name'] ) );
570
571
		if ( is_wp_error( $result ) ) {
572
			return $result;
573
		}
574
575
		$delete_status = $this->delete_items_not_present( $menu_id, $data['items'] );
576
		if( is_wp_error( $delete_status ) ) {
577
			return $delete_status;
578
		}
579
580
		foreach ( $data['items'] as $item ) {
581
			$item_id = isset( $item['menu-item-db-id'] ) ? $item['menu-item-db-id'] : 0;
582
			$result = wp_update_nav_menu_item( $menu_id, $item_id, $item );
583
			if ( is_wp_error( $result ) ) {
584
				return $result;
585
			}
586
		}
587
588
		$items = wp_get_nav_menu_items( $menu_id, array( 'update_post_term_cache' => false ) );
589
590
		if ( is_wp_error( $items ) ) {
591
			return $items;
592
		}
593
594
		$menu = wp_get_nav_menu_object( $menu_id );
595
		$menu->items = $items;
596
597
		return array( 'menu' => $this->simplify( $menu ) );
598
	}
599
600
	/**
601
	 * New items can have a 'tmp_id', allowing them to
602
	 * be used as parent items before they have been created.
603
	 *
604
	 * This function will create items that have a 'tmp_id' set, and
605
	 * update any items with a 'tmp_parent' to use the
606
	 * newly created item as a parent.
607
	 */
608
	function create_new_items( $data, $menu_id ) {
609
		$tmp_to_actual_ids = array();
610
		foreach ( $data['items'] as &$item ) {
611
			if ( isset( $item['tmp_id'] ) ) {
612
				$actual_id = wp_update_nav_menu_item( $menu_id, 0, $item );
613
				$tmp_to_actual_ids[ $item['tmp_id'] ] = $actual_id;
614
				unset( $item['tmp_id'] );
615
				$item['menu-item-db-id'] = $actual_id;
616
			}
617
		}
618
619
		foreach ( $data['items'] as &$item ) {
620
			if ( isset( $item['tmp_parent'] ) ) {
621
				$item['menu-item-parent-id'] = $tmp_to_actual_ids[ $item['tmp_parent'] ];
622
				unset( $item['tmp_parent'] );
623
			}
624
		}
625
626
		return $data;
627
	}
628
629
	/**
630
	 * remove any existing menu items not present in the supplied array.
631
	 * returns wp_error if an item cannot be deleted.
632
	 */
633
	function delete_items_not_present( $menu_id, $menu_items ) {
634
635
		$existing_items = wp_get_nav_menu_items( $menu_id, array( 'update_post_term_cache' => false ) );
636
		if ( ! is_array( $existing_items ) ) {
637
			return true;
638
		}
639
640
		$existing_ids = wp_list_pluck( $existing_items, 'db_id' );
641
		$ids_to_keep = wp_list_pluck( $menu_items, 'menu-item-db-id' );
642
		$ids_to_remove = array_diff( $existing_ids, $ids_to_keep );
643
644
		foreach ( $ids_to_remove as $id ) {
645
			if ( false === wp_delete_post( $id, true ) ) {
646
				return new WP_Error( 'menu-item',
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'menu-item'.

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...
647
					sprintf( 'Failed to delete menu item with id: %d.', $id ), 400 );
648
			}
649
		}
650
651
		return true;
652
	}
653
}
654
655
new WPCOM_JSON_API_Menus_List_Menus_Endpoint( array (
656
	'method'=> 'GET',
657
	'description' => 'Get a list of all navigation menus.',
658
	'group' => 'menus',
659
	'stat' => 'menus:list-menu',
660
	'path' => '/sites/%s/menus',
661
	'path_labels' => array(
662
		'$site' => '(int|string) Site ID or domain',
663
	),
664
	'response_format' => array(
665
		'menus' => '(array) A list of menu objects.<br/><br/>
666
			A menu object contains a name, items, locations, etc.
667
			Check the example response for the full structure.
668
			<br/><br/>
669
			Item objects contain fields relating to that item, e.g. id, type, content_id,
670
			but they can also contain other items objects - this nesting represents parents
671
			and child items in the item tree.',
672
		'locations' => '(array) Locations where menus can be placed. List of objects, one per location.'
673
	),
674
	'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus',
675
	'example_request_data' => array(
676
		'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
677
	),
678
) );
679
680
class WPCOM_JSON_API_Menus_List_Menus_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
681
	function callback( $path = '', $site = 0 ) {
682
		$site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
683
684
		if ( is_wp_error( $site_id ) ) {
685
			return $site_id;
686
		}
687
688
		$menus = wp_get_nav_menus( array( 'orderby' => 'term_id' ) );
689
690
		if ( is_wp_error( $menus ) ) {
691
			return $menus;
692
		}
693
694
		foreach ( $menus as $m ) {
695
			$items = wp_get_nav_menu_items( $m->term_id, array( 'update_post_term_cache' => false ) );
696
			if ( is_wp_error( $items ) ) {
697
				return $items;
698
			}
699
			$m->items = $items;
700
		}
701
702
		$menus = $this->simplify( $menus );
703
704
		if ( is_wp_error( $this->get_locations() ) ) {
705
			return $this->get_locations();
706
		}
707
708
		return array( 'menus' => $menus, 'locations' => $this->get_locations() );
709
	}
710
}
711
712
new WPCOM_JSON_API_Menus_Get_Menu_Endpoint( array (
713
	'method'=> 'GET',
714
	'description' => 'Get a single navigation menu.',
715
	'group' => 'menus',
716
	'stat' => 'menus:get-menu',
717
	'path' => '/sites/%s/menus/%d',
718
	'path_labels' => array(
719
		'$site' => '(int|string) Site ID or domain',
720
		'$menu_id' => '(int) Menu ID',
721
	),
722
	'response_format' => array(
723
		'menu' => '(object) A menu object.<br/><br/>
724
			A menu object contains a name, items, locations, etc.
725
			Check the example response for the full structure.
726
			<br/><br/>
727
			Item objects contain fields relating to that item, e.g. id, type, content_id,
728
			but they can also contain other items objects - this nesting represents parents
729
			and child items in the item tree.'
730
	),
731
	'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/510604099',
732
	'example_request_data' => array(
733
		'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
734
	),
735
) );
736
737
class WPCOM_JSON_API_Menus_Get_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
738
	function callback( $path = '', $site = 0, $menu_id = 0 ) {
739
		$site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
740
741
		if ( is_wp_error( $site_id ) ) {
742
			return $site_id;
743
		}
744
745
		if ( $menu_id <= 0 ) {
746
			return new WP_Error( 'menu-id', 'Menu ID must be greater than 0.', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'menu-id'.

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...
747
		}
748
749
		$menu = get_term( $menu_id, 'nav_menu' );
750
751
		if ( is_wp_error( $menu ) ) {
752
			return $menu;
753
		}
754
755
		$items = wp_get_nav_menu_items( $menu_id, array( 'update_post_term_cache' => false ) );
756
757
		if ( is_wp_error( $items ) ) {
758
			return $items;
759
		}
760
761
		$menu->items = $items;
762
763
		return array( 'menu' => $this->simplify( $menu ) );
764
	}
765
}
766
767
new WPCOM_JSON_API_Menus_Delete_Menu_Endpoint( array (
768
	'method' => 'POST',
769
	'description' => 'Delete a navigation menu',
770
	'group' => 'menus',
771
	'stat' => 'menus:delete-menu',
772
	'path' => '/sites/%s/menus/%d/delete',
773
	'path_labels' => array(
774
		'$site' => '(int|string) Site ID or domain',
775
		'$menu_id' => '(int) Menu ID',
776
	),
777
	'response_format' => array(
778
		'deleted' => '(bool) Has the menu been deleted?',
779
	),
780
	'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/$menu_id/delete',
781
	'example_request_data' => array(
782
		'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
783
	),
784
) );
785
786
class WPCOM_JSON_API_Menus_Delete_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
787
	function callback( $path = '', $site = 0, $menu_id = 0 ) {
788
		$site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
789
790
		if ( is_wp_error( $site_id ) ) {
791
			return $site_id;
792
		}
793
794
		if ( $menu_id <= 0 ) {
795
			return new WP_Error( 'menu-id', 'Menu ID must be greater than 0.', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'menu-id'.

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...
796
		}
797
798
		$result = wp_delete_nav_menu( $menu_id );
799
		if ( ! is_wp_error( $result ) ) {
800
			$result = array( 'deleted' => $result );
801
		}
802
803
		return $result;
804
	}
805
}
806
807
class WPCOM_JSON_API_Menus_Widgets {
808
	static function get() {
809
		$locations = array();
810
		$nav_menu_widgets = get_option( 'widget_nav_menu' );
811
812
		if ( ! is_array( $nav_menu_widgets ) ) {
813
			return $locations;
814
		}
815
816
		foreach ( $nav_menu_widgets as $k => $v ) {
817
			if ( is_array( $v ) && isset( $v['title'] ) ) {
818
				$locations[$k] = array( 'name' => 'nav_menu_widget-' . $k, 'description' => $v['title'] );
819
			}
820
		}
821
822
		return $locations;
823
	}
824
}
825