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