PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 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 All 508 releases
← All changes | json-endpoints/class.wpcom-json-api-menus-v1-1-endpoint.php +1256 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,1256 @@
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 +}