PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 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 All 506 releases
jetpack / json-endpoints / class.wpcom-json-api-menus-v1-1-endpoint.php

class.wpcom-json-api-menus-v1-1-endpoint.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.7, at json-endpoints/class.wpcom-json-api-menus-v1-1-endpoint.php

1,257 lines 30.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3 // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit( 0 );
7 }
8
9 /**
10 * Menus abstract endpoint class.
11 */
12 abstract class WPCOM_JSON_API_Menus_Abstract_Endpoint extends WPCOM_JSON_API_Endpoint {
13
14 /**
15 * Switch to blog and validate user.
16 *
17 * @param string $site - the site we want to validate.
18 *
19 * @return int
20 */
21 protected function switch_to_blog_and_validate_user( $site ) {
22 $site_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
23 if ( is_wp_error( $site_id ) ) {
24 return $site_id;
25 }
26
27 if ( ! current_user_can( 'edit_theme_options' ) ) {
28 return new WP_Error( 'unauthorised', 'User cannot edit theme options on this site.', 403 );
29 }
30
31 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
32 $this->load_theme_functions();
33 }
34
35 return $site_id;
36 }
37
38 /**
39 * Get the locations of the menus.
40 *
41 * @return array[]
42 */
43 protected function get_locations() {
44 $locations = array();
45 $menus = get_registered_nav_menus();
46 if ( ! empty( $menus ) ) {
47 foreach ( $menus as $name => $description ) {
48 $locations[] = array(
49 'name' => $name,
50 'description' => $description,
51 );
52 }
53 }
54
55 $locations = array_merge( $locations, WPCOM_JSON_API_Menus_Widgets::get() );
56
57 // Primary (first) location should have defaultState -> default,
58 // all other locations (including widgets) should have defaultState -> empty.
59 for ( $i = 0, $l = count( $locations ); $i < $l; $i++ ) {
60 $locations[ $i ]['defaultState'] = $i ? 'empty' : 'default';
61 }
62 return $locations;
63 }
64
65 /**
66 * Simplify the menus.
67 *
68 * @param WP_Term|WP_Term[] $data - the menus we're simplifying.
69 * @return array|array[] Simplified menu data.
70 */
71 protected function simplify( $data ) {
72 $simplifier = new WPCOM_JSON_API_Menus_Simplifier( $data );
73 return $simplifier->translate();
74 }
75
76 /**
77 * Complexify the menus.
78 *
79 * @param array[] $data - the menu data we're complexifying.
80 * @return array[]|WP_Error Complexified menu data, or WP_Error on error.
81 */
82 protected function complexify( $data ) {
83 $complexifier = new WPCOM_JSON_API_Menus_Complexify( $data );
84 return $complexifier->translate();
85 }
86 }
87
88 /**
89 * The menu translator class.
90 */
91 abstract class WPCOM_JSON_API_Menus_Translator {
92 /**
93 * A string identifying this class.
94 *
95 * @var string
96 */
97 protected $filter = '';
98
99 /**
100 * List of filter method names.
101 *
102 * Filter methods are passed an array, and return a transformed array or WP_Error.
103 *
104 * @var array
105 */
106 protected $filters = array();
107
108 /**
109 * False if $menus was an array on construct, true otherwise.
110 *
111 * @var bool
112 */
113 public $is_single_menu;
114
115 /**
116 * A menu or array of menus.
117 *
118 * @var mixed
119 */
120 public $menus;
121
122 /**
123 * Class constructor.
124 *
125 * @param mixed $menus - a menu or list of menus.
126 */
127 public function __construct( $menus ) {
128 $this->is_single_menu = ! is_array( $menus );
129 $this->menus = is_array( $menus ) ? $menus : array( $menus );
130 }
131
132 /**
133 * Translate the menus.
134 *
135 * @return array|array[]|WP_Error
136 */
137 public function translate() {
138 $result = $this->menus;
139 foreach ( $this->filters as $f ) {
140 $result = call_user_func( array( $this, $f ), $result );
141 if ( is_wp_error( $result ) ) {
142 return $result;
143 }
144 }
145 return $this->maybe_extract( $result );
146 }
147
148 /**
149 * Return a single menu or an array of menus.
150 *
151 * @param array $menus - the menu list.
152 *
153 * @return array|array[]
154 */
155 protected function maybe_extract( $menus ) {
156 return $this->is_single_menu ? $menus[0] : $menus;
157 }
158
159 /**
160 * See if we need to whitelist and rename.
161 *
162 * @param object|array $object - the object (or associative array) we're checking.
163 * @param array $dict Associative array holding the key whitelist and renaming/casting data.
164 * Keys are the keys from $object` to preserve. Values are the key to use in the output or an
165 * assoc where 'name' specifies the output key and 'type' specifies the PHP type to cast the value to.
166 *
167 * @return array
168 */
169 public function whitelist_and_rename_with( $object, $dict ) {
170 $return = array();
171 foreach ( (array) $object as $k => $v ) {
172 if ( isset( $dict[ $k ] ) ) {
173 if ( is_array( $dict[ $k ] ) ) {
174 settype( $v, $dict[ $k ]['type'] );
175 $return[ $dict[ $k ]['name'] ] = $v;
176 } else {
177 $new_k = $dict[ $k ];
178 $return[ $new_k ] = $v;
179 }
180 }
181 }
182 return $return;
183 }
184 }
185
186 /**
187 * The simplifier class.
188 */
189 class WPCOM_JSON_API_Menus_Simplifier extends WPCOM_JSON_API_Menus_Translator {
190
191 /**
192 * The simplify translator class.
193 *
194 * @var string
195 */
196 protected $filter = 'wpcom_menu_api_translator_simplify';
197
198 /**
199 * The simplify filters.
200 *
201 * @var array
202 */
203 protected $filters = array(
204 'whitelist_and_rename_keys',
205 'add_locations',
206 'treeify',
207 'add_widget_locations',
208 );
209
210 /**
211 * The menu whitelist.
212 *
213 * @var array
214 */
215 protected $menu_whitelist = array(
216 'term_id' => array(
217 'name' => 'id',
218 'type' => 'int',
219 ),
220 'name' => array(
221 'name' => 'name',
222 'type' => 'string',
223 ),
224 'description' => array(
225 'name' => 'description',
226 'type' => 'string',
227 ),
228 'items' => array(
229 'name' => 'items',
230 'type' => 'array',
231 ),
232 );
233
234 /**
235 * The menu item whitelist.
236 *
237 * @var array
238 */
239 protected $menu_item_whitelist = array(
240 'db_id' => array(
241 'name' => 'id',
242 'type' => 'int',
243 ),
244 'object_id' => array(
245 'name' => 'content_id',
246 'type' => 'int',
247 ),
248 'object' => array(
249 'name' => 'type',
250 'type' => 'string',
251 ),
252 'type' => array(
253 'name' => 'type_family',
254 'type' => 'string',
255 ),
256 'type_label' => array(
257 'name' => 'type_label',
258 'type' => 'string',
259 ),
260 'title' => array(
261 'name' => 'name',
262 'type' => 'string',
263 ),
264 'menu_order' => array(
265 'name' => 'order',
266 'type' => 'int',
267 ),
268 'menu_item_parent' => array(
269 'name' => 'parent',
270 'type' => 'int',
271 ),
272 'url' => array(
273 'name' => 'url',
274 'type' => 'string',
275 ),
276 'target' => array(
277 'name' => 'link_target',
278 'type' => 'string',
279 ),
280 'attr_title' => array(
281 'name' => 'link_title',
282 'type' => 'string',
283 ),
284 'description' => array(
285 'name' => 'description',
286 'type' => 'string',
287 ),
288 'classes' => array(
289 'name' => 'classes',
290 'type' => 'array',
291 ),
292 'xfn' => array(
293 'name' => 'xfn',
294 'type' => 'string',
295 ),
296 );
297
298 /**************************
299 * Filters methods
300 **************************/
301
302 /**
303 * Treeify the menus.
304 *
305 * @param array $menus - the menu list.
306 *
307 * @return array
308 */
309 public function treeify( $menus ) {
310 return array_map( array( $this, 'treeify_menu' ), $menus );
311 }
312
313 /**
314 * Turn the flat item list into a tree of items.
315 *
316 * @param array $menu - the menu.
317 *
318 * @return array
319 */
320 protected function treeify_menu( $menu ) {
321 $indexed_nodes = array();
322 $tree = array();
323
324 foreach ( $menu['items'] as &$item ) {
325 $indexed_nodes[ $item['id'] ] = &$item;
326 }
327
328 foreach ( $menu['items'] as &$item ) {
329 if ( $item['parent'] && isset( $indexed_nodes[ $item['parent'] ] ) ) {
330 $parent_node = &$indexed_nodes[ $item['parent'] ];
331 if ( ! isset( $parent_node['items'] ) ) {
332 $parent_node['items'] = array();
333 }
334 $parent_node['items'][ $item['order'] ] = &$item;
335 } else {
336 $tree[ $item['order'] ] = &$item;
337 }
338 unset( $item['order'] );
339 unset( $item['parent'] );
340 }
341
342 $menu['items'] = $tree;
343 $this->remove_item_keys( $menu );
344 return $menu;
345 }
346
347 /**
348 * Recursively ensure item lists are contiguous.
349 *
350 * @param array $item - the item list.
351 */
352 protected function remove_item_keys( &$item ) {
353 if ( ! isset( $item['items'] ) || ! is_array( $item['items'] ) ) {
354 return;
355 }
356
357 foreach ( $item['items'] as &$it ) {
358 $this->remove_item_keys( $it );
359 }
360
361 $item['items'] = array_values( $item['items'] );
362 }
363
364 /**
365 * Whitelist and rename keys.
366 *
367 * @param (object|array)[] $menus - the menu list.
368 *
369 * @return array[]
370 */
371 protected function whitelist_and_rename_keys( $menus ) {
372 $transformed_menus = array();
373
374 foreach ( $menus as $menu ) {
375 $menu = $this->whitelist_and_rename_with( $menu, $this->menu_whitelist );
376
377 if ( isset( $menu['items'] ) ) {
378 foreach ( $menu['items'] as &$item ) {
379 $item = $this->whitelist_and_rename_with( $item, $this->menu_item_whitelist );
380 }
381 }
382
383 $transformed_menus[] = $menu;
384 }
385
386 return $transformed_menus;
387 }
388
389 /**
390 * Add menu locations.
391 *
392 * @param array $menus - the menu list.
393 *
394 * @return array[]
395 */
396 protected function add_locations( $menus ) {
397 $menus_with_locations = array();
398
399 foreach ( $menus as $menu ) {
400 $menu['locations'] = array_keys( get_nav_menu_locations(), $menu['id'] ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
401 $menus_with_locations[] = $menu;
402 }
403
404 return $menus_with_locations;
405 }
406
407 /**
408 * Add widget locations.
409 *
410 * @param array $menus - the menu list.
411 *
412 * @return array[]
413 */
414 protected function add_widget_locations( $menus ) {
415 $nav_menu_widgets = WPCOM_JSON_API_Menus_Widgets::get();
416
417 if ( ! is_array( $nav_menu_widgets ) ) {
418 return $menus;
419 }
420
421 foreach ( $menus as &$menu ) {
422 $widget_locations = array();
423
424 foreach ( $nav_menu_widgets as $key => $widget ) {
425 if ( is_array( $widget ) && isset( $widget['nav_menu'] ) &&
426 $widget['nav_menu'] === $menu['id'] ) {
427 $widget_locations[] = 'nav_menu_widget-' . $key;
428 }
429 }
430 $menu['locations'] = array_merge( $menu['locations'], $widget_locations );
431 }
432
433 return $menus;
434 }
435 }
436
437 /**
438 * Complexify menu class.
439 */
440 class WPCOM_JSON_API_Menus_Complexify extends WPCOM_JSON_API_Menus_Translator {
441
442 /**
443 * The complexify filter.
444 *
445 * @var string
446 */
447 protected $filter = 'wpcom_menu_api_translator_complexify';
448
449 /**
450 * The filters.
451 *
452 * @var array
453 */
454 protected $filters = array(
455 'untreeify',
456 'set_locations',
457 'whitelist_and_rename_keys',
458 );
459
460 /**
461 * The menu whitelist.
462 *
463 * @var array
464 */
465 protected $menu_whitelist = array(
466 'id' => 'term_id',
467 'name' => 'menu-name',
468 'description' => 'description',
469 'items' => 'items',
470 );
471
472 /**
473 * The item whitelist.
474 *
475 * @var array
476 */
477 protected $menu_item_whitelist = array(
478 'id' => 'menu-item-db-id',
479 'content_id' => 'menu-item-object-id',
480 'type' => 'menu-item-object',
481 'type_family' => 'menu-item-type',
482 'type_label' => 'menu-item-type-label',
483 'name' => 'menu-item-title',
484 'order' => 'menu-item-position',
485 'parent' => 'menu-item-parent-id',
486 'url' => 'menu-item-url',
487 'link_target' => 'menu-item-target',
488 'link_title' => 'menu-item-attr-title',
489 'status' => 'menu-item-status',
490 'tmp_id' => 'tmp_id',
491 'tmp_parent' => 'tmp_parent',
492 'description' => 'menu-item-description',
493 'classes' => 'menu-item-classes',
494 'xfn' => 'menu-item-xfn',
495 );
496
497 /**************************
498 * Filters methods
499 **************************/
500
501 /**
502 * Untreeify the menu.
503 *
504 * @param array $menus - the list of menus.
505 *
506 * @return array[]
507 */
508 public function untreeify( $menus ) {
509 return array_map( array( $this, 'untreeify_menu' ), $menus );
510 }
511
512 /**
513 * Convert the tree of menu items to a flat list suitable for the nav_menu APIs.
514 *
515 * @param array $menu - the menu we're untreeifying.
516 *
517 * @return array
518 */
519 protected function untreeify_menu( $menu ) {
520 if ( empty( $menu['items'] ) ) {
521 return $menu;
522 }
523
524 $items_list = array();
525 $counter = 1;
526 foreach ( $menu['items'] as &$item ) {
527 $item['parent'] = 0;
528 }
529 $this->untreeify_items( $menu['items'], $items_list, $counter );
530 $menu['items'] = $items_list;
531
532 return $menu;
533 }
534
535 /**
536 * Recurse the items tree adding each item to a flat list and restoring
537 * `order` and `parent` fields.
538 *
539 * @param array $items item tree.
540 * @param array $items_list output flat list of items.
541 * @param int $counter for creating temporary IDs.
542 */
543 protected function untreeify_items( $items, &$items_list, &$counter ) {
544 foreach ( $items as $index => $item ) {
545 $item['order'] = $index + 1;
546
547 if ( ! isset( $item['id'] ) ) {
548 $this->set_tmp_id( $item, $counter++ );
549 }
550
551 if ( isset( $item['items'] ) && is_array( $item['items'] ) ) {
552 foreach ( $item['items'] as &$i ) {
553 $i['parent'] = $item['id'];
554 }
555 $this->untreeify_items( $item['items'], $items_list, $counter );
556 unset( $item['items'] );
557 }
558
559 $items_list[] = $item;
560 }
561 }
562
563 /**
564 * Populate `tmp_id` field for a new item, and `tmp_parent` field
565 * for all its children, to maintain the hierarchy.
566 * These fields will be used when creating
567 * new items with wp_update_nav_menu_item().
568 *
569 * @param array $item - the item tree.
570 * @param string $tmp_id - the tmp ID.
571 */
572 private function set_tmp_id( &$item, $tmp_id ) {
573 $item['tmp_id'] = $tmp_id;
574 if ( ! isset( $item['items'] ) || ! is_array( $item['items'] ) ) {
575 return;
576 }
577 foreach ( $item['items'] as &$child ) {
578 $child['tmp_parent'] = $tmp_id;
579 }
580 }
581
582 /**
583 * Whitelist and rename keys.
584 *
585 * @param array $menus - the menus.
586 *
587 * @return array[]
588 */
589 protected function whitelist_and_rename_keys( $menus ) {
590 $transformed_menus = array();
591 foreach ( $menus as $menu ) {
592 $menu = $this->whitelist_and_rename_with( $menu, $this->menu_whitelist );
593 if ( isset( $menu['items'] ) ) {
594 $menu['items'] = array_map( array( $this, 'whitelist_and_rename_item_keys' ), $menu['items'] );
595 }
596 $transformed_menus[] = $menu;
597 }
598
599 return $transformed_menus;
600 }
601
602 /**
603 * Whitelist and rename item keys.
604 *
605 * @param array $item - the item.
606 *
607 * @return array
608 */
609 protected function whitelist_and_rename_item_keys( $item ) {
610 $item = $this->implode_array_fields( $item );
611 $item = $this->whitelist_and_rename_with( $item, $this->menu_item_whitelist );
612 return $item;
613 }
614
615 /**
616 * All item fields are set as strings.
617 *
618 * @param array $menu_item - the menu item.
619 * @return array Item with fields imploded.
620 */
621 protected function implode_array_fields( $menu_item ) {
622 return array_map( array( $this, 'implode_array_field' ), $menu_item );
623 }
624
625 /**
626 * Implode an array field.
627 *
628 * @param mixed $field - the field we're imploding.
629 *
630 * @return mixed The imploded string if `$field` was an array, otherwise `$field` unchanged.
631 */
632 protected function implode_array_field( $field ) {
633 if ( is_array( $field ) ) {
634 return implode( ' ', $field );
635 }
636 return $field;
637 }
638
639 /**
640 * Set the menu locations.
641 *
642 * @param array $menus - the menu list.
643 *
644 * @return array[]|WP_Error
645 */
646 protected function set_locations( $menus ) {
647 foreach ( $menus as $menu ) {
648 if ( isset( $menu['locations'] ) ) {
649 if ( true !== $this->locations_are_valid( $menu['locations'] ) ) {
650 return $this->locations_are_valid( $menu['locations'] );
651 }
652 }
653 }
654
655 return array_map( array( $this, 'set_location' ), $menus );
656 }
657
658 /**
659 * Set the menu locations.
660 *
661 * @param array $menu - the menu.
662 *
663 * @return array
664 */
665 protected function set_location( $menu ) {
666 $this->set_menu_at_locations( $menu['locations'], $menu['id'] );
667 return $menu;
668 }
669
670 /**
671 * Set the menu at locations.
672 *
673 * @param array $locations - the locations.
674 * @param int $menu_id - the menu ID.
675 */
676 protected function set_menu_at_locations( $locations, $menu_id ) {
677 $location_map = get_nav_menu_locations();
678 $this->remove_menu_from_all_locations( $menu_id, $location_map );
679
680 if ( is_array( $locations ) ) {
681 foreach ( $locations as $location ) {
682 $location_map[ $location ] = $menu_id;
683 }
684 }
685
686 set_theme_mod( 'nav_menu_locations', $location_map );
687
688 $this->set_widget_menu_at_locations( $locations, $menu_id );
689 }
690
691 /**
692 * Remove from all locations.
693 *
694 * @param int $menu_id - the menu ID.
695 * @param array $location_map - the location map.
696 */
697 protected function remove_menu_from_all_locations( $menu_id, &$location_map ) {
698 foreach ( get_nav_menu_locations() as $existing_location => $existing_menu_id ) {
699 if ( $existing_menu_id === $menu_id ) {
700 unset( $location_map[ $existing_location ] );
701 }
702 }
703 }
704
705 /**
706 * Set widget menu at locations.
707 *
708 * @param array $locations - the locations.
709 * @param int $menu_id - the menu ID.
710 */
711 protected function set_widget_menu_at_locations( $locations, $menu_id ) {
712 $nav_menu_widgets = get_option( 'widget_nav_menu' );
713
714 if ( ! is_array( $nav_menu_widgets ) ) {
715 return;
716 }
717
718 // Remove menus from all custom menu widget locations
719 foreach ( $nav_menu_widgets as &$widget ) {
720 if ( is_array( $widget ) && isset( $widget['nav_menu'] ) && $widget['nav_menu'] === $menu_id ) {
721 $widget['nav_menu'] = 0;
722 }
723 }
724
725 if ( is_array( $locations ) ) {
726 foreach ( $locations as $location ) {
727 if ( preg_match( '/^nav_menu_widget-(\d+)/', $location, $matches ) ) {
728 if ( isset( $matches[1] ) ) {
729 $nav_menu_widgets[ $matches[1] ]['nav_menu'] = $menu_id;
730 }
731 }
732 }
733 }
734
735 update_option( 'widget_nav_menu', $nav_menu_widgets );
736 }
737
738 /**
739 * Check if the locations are valid.
740 *
741 * @param int|array $locations - the location we're checking.
742 *
743 * @return bool|WP_Error
744 */
745 protected function locations_are_valid( $locations ) {
746 if ( is_int( $locations ) ) {
747 if ( $locations !== 0 ) {
748 return new WP_Error( 'locations-int', 'Locations int must be 0.', 400 );
749 } else {
750 return true;
751 }
752 } elseif ( is_array( $locations ) ) {
753 foreach ( $locations as $location_name ) {
754 if ( ! $this->location_name_exists( $location_name ) ) {
755 return new WP_Error(
756 'locations-array',
757 sprintf( "Location '%s' does not exist.", $location_name ),
758 404
759 );
760 }
761 }
762 return true;
763 }
764 return new WP_Error( 'locations', 'Locations must be array or integer.', 400 );
765 }
766
767 /**
768 * Check if the location name exists.
769 *
770 * @param string $location_name - the location name.
771 *
772 * @return bool
773 */
774 protected function location_name_exists( $location_name ) {
775 $widget_location_names = wp_list_pluck( WPCOM_JSON_API_Menus_Widgets::get(), 'name' );
776
777 $existing_locations = get_nav_menu_locations();
778
779 if ( ! is_array( get_registered_nav_menus() ) ) {
780 return false;
781 }
782
783 return array_key_exists( $location_name, get_registered_nav_menus() ) ||
784 array_key_exists( $location_name, $existing_locations ) ||
785 in_array( $location_name, $widget_location_names, true );
786 }
787 }
788
789 new WPCOM_JSON_API_Menus_New_Menu_Endpoint(
790 array(
791 'method' => 'POST',
792 'description' => 'Create a new navigation menu.',
793 'group' => 'menus',
794 'stat' => 'menus:new-menu',
795 'path' => '/sites/%s/menus/new',
796 'path_labels' => array(
797 '$site' => '(int|string) Site ID or domain',
798 ),
799 'request_format' => array(
800 'name' => '(string) Name of menu',
801 ),
802 'response_format' => array(
803 'id' => '(int) Newly created menu ID',
804 ),
805 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/new',
806 'example_request_data' => array(
807 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
808 'body' => array(
809 'name' => 'Menu 1',
810 ),
811 ),
812 )
813 );
814
815 /**
816 * New menu endpoint class.
817 *
818 * @phan-constructor-used-for-side-effects
819 */
820 class WPCOM_JSON_API_Menus_New_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
821
822 /**
823 * The API Callback.
824 *
825 * @param string $path - the path.
826 * @param int $site - the site ID.
827 *
828 * @return array|WP_Error
829 */
830 public function callback( $path = '', $site = 0 ) {
831 $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
832
833 if ( is_wp_error( $site_id ) ) {
834 return $site_id;
835 }
836
837 $data = $this->input();
838
839 $id = wp_create_nav_menu( $data['name'] );
840
841 if ( is_wp_error( $id ) ) {
842 return $id;
843 }
844
845 return array( 'id' => $id );
846 }
847 }
848
849 new WPCOM_JSON_API_Menus_Update_Menu_Endpoint(
850 array(
851 'method' => 'POST',
852 'description' => 'Update a navigation menu.',
853 'group' => 'menus',
854 'stat' => 'menus:update-menu',
855 'path' => '/sites/%s/menus/%d',
856 'path_labels' => array(
857 '$site' => '(int|string) Site ID or domain',
858 '$menu_id' => '(int) Menu ID',
859 ),
860 'request_format' => array(
861 'name' => '(string) Name of menu',
862 'items' => '(array) A list of menu item objects.
863 <br/><br/>
864 Item objects contain fields relating to that item, e.g. id, type, content_id,
865 but they can also contain other items objects - this nesting represents parents
866 and child items in the item tree.',
867 ),
868 'response_format' => array(
869 'menu' => '(object) Updated menu object',
870 ),
871 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/510604099',
872 'example_request_data' => array(
873 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
874 'body' => array(
875 'name' => 'Test Menu',
876 ),
877 ),
878 )
879 );
880
881 /**
882 * Update menu endpoint class.
883 *
884 * @phan-constructor-used-for-side-effects
885 */
886 class WPCOM_JSON_API_Menus_Update_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
887
888 /**
889 * The API Callback.
890 *
891 * @param string $path - the path.
892 * @param int $site - the site ID.
893 * @param int $menu_id - the menu ID.
894 *
895 * @return array|WP_Error
896 */
897 public function callback( $path = '', $site = 0, $menu_id = 0 ) {
898 $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
899
900 if ( is_wp_error( $site_id ) ) {
901 return $site_id;
902 }
903
904 if ( $menu_id <= 0 ) {
905 return new WP_Error( 'menu-id', 'Menu ID must be greater than 0.', 400 );
906 }
907
908 $data = $this->input( true, false );
909 $data['id'] = $menu_id;
910 $data = $this->complexify( array( $data ) );
911 if ( is_wp_error( $data ) ) {
912 return $data;
913 }
914 $data = $data[0];
915
916 // Avoid special-case handling of an unset 'items' field in empty menus
917 $data['items'] ??= array();
918
919 $data = $this->create_new_items( $data, $menu_id );
920
921 $result = wp_update_nav_menu_object( $menu_id, array( 'menu-name' => $data['menu-name'] ) );
922
923 if ( is_wp_error( $result ) ) {
924 return $result;
925 }
926
927 $delete_status = $this->delete_items_not_present( $menu_id, $data['items'] );
928 if ( is_wp_error( $delete_status ) ) {
929 return $delete_status;
930 }
931
932 foreach ( $data['items'] as $item ) {
933 $item_id = $item['menu-item-db-id'] ?? 0;
934 $result = wp_update_nav_menu_item( $menu_id, $item_id, $item );
935 if ( is_wp_error( $result ) ) {
936 return $result;
937 }
938 }
939
940 $items = wp_get_nav_menu_items( $menu_id, array( 'update_post_term_cache' => false ) );
941
942 if ( is_wp_error( $items ) ) {
943 return $items;
944 }
945
946 $menu = wp_get_nav_menu_object( $menu_id );
947 $menu->items = $items;
948
949 return array( 'menu' => $this->simplify( $menu ) );
950 }
951
952 /**
953 * New items can have a 'tmp_id', allowing them to
954 * be used as parent items before they have been created.
955 *
956 * This function will create items that have a 'tmp_id' set, and
957 * update any items with a 'tmp_parent' to use the
958 * newly created item as a parent.
959 *
960 * @param array $data - the data we're checking.
961 * @param int $menu_id - the menu ID.
962 * @return array `$data` with new item IDs filled in.
963 */
964 public function create_new_items( $data, $menu_id ) {
965 $tmp_to_actual_ids = array();
966 foreach ( $data['items'] as &$item ) {
967 if ( isset( $item['tmp_id'] ) ) {
968 $actual_id = wp_update_nav_menu_item( $menu_id, 0, $item );
969 $tmp_to_actual_ids[ $item['tmp_id'] ] = $actual_id;
970 unset( $item['tmp_id'] );
971 $item['menu-item-db-id'] = $actual_id;
972 }
973 }
974
975 foreach ( $data['items'] as &$item ) {
976 if ( isset( $item['tmp_parent'] ) ) {
977 $item['menu-item-parent-id'] = $tmp_to_actual_ids[ $item['tmp_parent'] ];
978 unset( $item['tmp_parent'] );
979 }
980 }
981
982 return $data;
983 }
984
985 /**
986 * Remove any existing menu items not present in the supplied array.
987 * returns wp_error if an item cannot be deleted.
988 *
989 * @param int $menu_id - the menu ID.
990 * @param array $menu_items - the menu items.
991 *
992 * @return bool|WP_Error
993 */
994 public function delete_items_not_present( $menu_id, $menu_items ) {
995
996 $existing_items = wp_get_nav_menu_items( $menu_id, array( 'update_post_term_cache' => false ) );
997 if ( ! is_array( $existing_items ) ) {
998 return true;
999 }
1000
1001 $existing_ids = wp_list_pluck( $existing_items, 'db_id' );
1002 $ids_to_keep = wp_list_pluck( $menu_items, 'menu-item-db-id' );
1003 $ids_to_remove = array_diff( $existing_ids, $ids_to_keep );
1004
1005 foreach ( $ids_to_remove as $id ) {
1006 if ( false === wp_delete_post( $id, true ) ) {
1007 return new WP_Error(
1008 'menu-item',
1009 sprintf( 'Failed to delete menu item with id: %d.', $id ),
1010 400
1011 );
1012 }
1013 }
1014
1015 return true;
1016 }
1017 }
1018
1019 new WPCOM_JSON_API_Menus_List_Menus_Endpoint(
1020 array(
1021 'method' => 'GET',
1022 'description' => 'Get a list of all navigation menus.',
1023 'group' => 'menus',
1024 'stat' => 'menus:list-menu',
1025 'path' => '/sites/%s/menus',
1026 'path_labels' => array(
1027 '$site' => '(int|string) Site ID or domain',
1028 ),
1029 'response_format' => array(
1030 'menus' => '(array) A list of menu objects.<br/><br/>
1031 A menu object contains a name, items, locations, etc.
1032 Check the example response for the full structure.
1033 <br/><br/>
1034 Item objects contain fields relating to that item, e.g. id, type, content_id,
1035 but they can also contain other items objects - this nesting represents parents
1036 and child items in the item tree.',
1037 'locations' => '(array) Locations where menus can be placed. List of objects, one per location.',
1038 ),
1039 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus',
1040 'example_request_data' => array(
1041 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
1042 ),
1043 )
1044 );
1045
1046 /**
1047 * List menus endpoint class.
1048 *
1049 * @phan-constructor-used-for-side-effects
1050 */
1051 class WPCOM_JSON_API_Menus_List_Menus_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
1052
1053 /**
1054 * The API Callback.
1055 *
1056 * @param string $path - the path.
1057 * @param int $site - the site ID.
1058 *
1059 * @return array|WP_Error
1060 */
1061 public function callback( $path = '', $site = 0 ) {
1062 $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
1063
1064 if ( is_wp_error( $site_id ) ) {
1065 return $site_id;
1066 }
1067
1068 $menus = wp_get_nav_menus( array( 'orderby' => 'term_id' ) );
1069
1070 if ( is_wp_error( $menus ) ) {
1071 return $menus;
1072 }
1073
1074 foreach ( $menus as $m ) {
1075 $items = wp_get_nav_menu_items( $m->term_id, array( 'update_post_term_cache' => false ) );
1076 if ( is_wp_error( $items ) ) {
1077 return $items;
1078 }
1079 $m->items = $items;
1080 }
1081
1082 $menus = $this->simplify( $menus );
1083
1084 if ( is_wp_error( $this->get_locations() ) ) {
1085 return $this->get_locations();
1086 }
1087
1088 return array(
1089 'menus' => $menus,
1090 'locations' => $this->get_locations(),
1091 );
1092 }
1093 }
1094
1095 new WPCOM_JSON_API_Menus_Get_Menu_Endpoint(
1096 array(
1097 'method' => 'GET',
1098 'description' => 'Get a single navigation menu.',
1099 'group' => 'menus',
1100 'stat' => 'menus:get-menu',
1101 'path' => '/sites/%s/menus/%d',
1102 'path_labels' => array(
1103 '$site' => '(int|string) Site ID or domain',
1104 '$menu_id' => '(int) Menu ID',
1105 ),
1106 'response_format' => array(
1107 'menu' => '(object) A menu object.<br/><br/>
1108 A menu object contains a name, items, locations, etc.
1109 Check the example response for the full structure.
1110 <br/><br/>
1111 Item objects contain fields relating to that item, e.g. id, type, content_id,
1112 but they can also contain other items objects - this nesting represents parents
1113 and child items in the item tree.',
1114 ),
1115 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/510604099',
1116 'example_request_data' => array(
1117 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
1118 ),
1119 )
1120 );
1121
1122 /**
1123 * Get menu endpoint class.
1124 *
1125 * @phan-constructor-used-for-side-effects
1126 */
1127 class WPCOM_JSON_API_Menus_Get_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
1128
1129 /**
1130 * The API Callback.
1131 *
1132 * @param string $path - the path.
1133 * @param int $site - the site ID.
1134 * @param int $menu_id - the menu ID.
1135 *
1136 * @return array|WP_Error
1137 */
1138 public function callback( $path = '', $site = 0, $menu_id = 0 ) {
1139 $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
1140
1141 if ( is_wp_error( $site_id ) ) {
1142 return $site_id;
1143 }
1144
1145 if ( $menu_id <= 0 ) {
1146 return new WP_Error( 'menu-id', 'Menu ID must be greater than 0.', 400 );
1147 }
1148
1149 $menu = get_term( $menu_id, 'nav_menu' );
1150
1151 if ( is_wp_error( $menu ) ) {
1152 return $menu;
1153 }
1154
1155 if ( ! $menu instanceof WP_Term ) {
1156 return new WP_Error( 'menu-not-found', 'Menu not found.', 404 );
1157 }
1158
1159 $items = wp_get_nav_menu_items( $menu_id, array( 'update_post_term_cache' => false ) );
1160
1161 if ( is_wp_error( $items ) ) {
1162 return $items;
1163 }
1164
1165 $menu->items = $items;
1166
1167 return array( 'menu' => $this->simplify( $menu ) );
1168 }
1169 }
1170
1171 new WPCOM_JSON_API_Menus_Delete_Menu_Endpoint(
1172 array(
1173 'method' => 'POST',
1174 'description' => 'Delete a navigation menu',
1175 'group' => 'menus',
1176 'stat' => 'menus:delete-menu',
1177 'path' => '/sites/%s/menus/%d/delete',
1178 'path_labels' => array(
1179 '$site' => '(int|string) Site ID or domain',
1180 '$menu_id' => '(int) Menu ID',
1181 ),
1182 'response_format' => array(
1183 'deleted' => '(bool) Has the menu been deleted?',
1184 ),
1185 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/$menu_id/delete',
1186 'example_request_data' => array(
1187 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
1188 ),
1189 )
1190 );
1191
1192 /**
1193 * Delete menu endpoint class.
1194 *
1195 * @phan-constructor-used-for-side-effects
1196 */
1197 class WPCOM_JSON_API_Menus_Delete_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
1198
1199 /**
1200 * The API Callback.
1201 *
1202 * @param string $path - the path.
1203 * @param int $site - the site ID.
1204 * @param int $menu_id - the menu ID.
1205 *
1206 * @return array|WP_Error
1207 */
1208 public function callback( $path = '', $site = 0, $menu_id = 0 ) {
1209 $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
1210
1211 if ( is_wp_error( $site_id ) ) {
1212 return $site_id;
1213 }
1214
1215 if ( $menu_id <= 0 ) {
1216 return new WP_Error( 'menu-id', 'Menu ID must be greater than 0.', 400 );
1217 }
1218
1219 $result = wp_delete_nav_menu( $menu_id );
1220 if ( ! is_wp_error( $result ) ) {
1221 $result = array( 'deleted' => $result );
1222 }
1223
1224 return $result;
1225 }
1226 }
1227
1228 /**
1229 * API Menus widgets class.
1230 */
1231 class WPCOM_JSON_API_Menus_Widgets {
1232 /**
1233 * Get the menu locations.
1234 *
1235 * @return array
1236 */
1237 public static function get() {
1238 $locations = array();
1239 $nav_menu_widgets = get_option( 'widget_nav_menu' );
1240
1241 if ( ! is_array( $nav_menu_widgets ) ) {
1242 return $locations;
1243 }
1244
1245 foreach ( $nav_menu_widgets as $k => $v ) {
1246 if ( is_array( $v ) && isset( $v['title'] ) ) {
1247 $locations[ $k ] = array(
1248 'name' => 'nav_menu_widget-' . $k,
1249 'description' => $v['title'],
1250 );
1251 }
1252 }
1253
1254 return $locations;
1255 }
1256 }
1257