Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 411
0.00% covered (danger)
0.00%
0 / 37
CRAP
0.00% covered (danger)
0.00%
0 / 10
WPCOM_JSON_API_Menus_Abstract_Endpoint
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 4
156
0.00% covered (danger)
0.00%
0 / 1
 switch_to_blog_and_validate_user
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 get_locations
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 simplify
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 complexify
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
WPCOM_JSON_API_Menus_Translator
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 4
132
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 translate
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 maybe_extract
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 whitelist_and_rename_with
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
WPCOM_JSON_API_Menus_Simplifier
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 6
600
0.00% covered (danger)
0.00%
0 / 1
 treeify
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 treeify_menu
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 remove_item_keys
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 whitelist_and_rename_keys
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 add_locations
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 add_widget_locations
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
56
WPCOM_JSON_API_Menus_Complexify
0.00% covered (danger)
0.00%
0 / 89
0.00% covered (danger)
0.00%
0 / 15
2756
0.00% covered (danger)
0.00%
0 / 1
 untreeify
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 untreeify_menu
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 untreeify_items
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 set_tmp_id
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 whitelist_and_rename_keys
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 whitelist_and_rename_item_keys
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 implode_array_fields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 implode_array_field
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 set_locations
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 set_location
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 set_menu_at_locations
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 remove_menu_from_all_locations
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 set_widget_menu_at_locations
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
110
 locations_are_valid
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 location_name_exists
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
WPCOM_JSON_API_Menus_New_Menu_Endpoint
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 1
 callback
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
WPCOM_JSON_API_Menus_Update_Menu_Endpoint
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 3
420
0.00% covered (danger)
0.00%
0 / 1
 callback
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
132
 create_new_items
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 delete_items_not_present
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
WPCOM_JSON_API_Menus_List_Menus_Endpoint
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 1
 callback
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
WPCOM_JSON_API_Menus_Get_Menu_Endpoint
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 1
 callback
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
WPCOM_JSON_API_Menus_Delete_Menu_Endpoint
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 1
 callback
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
WPCOM_JSON_API_Menus_Widgets
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 1
 get
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
4
5if ( ! defined( 'ABSPATH' ) ) {
6    exit( 0 );
7}
8
9/**
10 * Menus abstract endpoint class.
11 */
12abstract 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 */
91abstract 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 */
189class 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 */
440class 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
789new 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 */
820class 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
849new 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 */
886class 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'] = isset( $data['items'] ) ? $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 = isset( $item['menu-item-db-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
1019new 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 */
1051class 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
1095new 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 */
1127class 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
1171new 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 */
1197class 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 */
1231class 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}