1<?php
2/**
3 * Navigation Menu functions
4 *
5 * @package WordPress
6 * @subpackage Nav_Menus
7 * @since 3.0.0
8 */
9
10/**
11 * Returns a navigation menu object.
12 *
13 * @since 3.0.0
14 *
15 * @param int|string|WP_Term $menu Menu ID, slug, name, or object.
16 * @return WP_Term|false Menu object on success, false if $menu param isn't supplied or term does not exist.
17 */
18function wp_get_nav_menu_object( $menu ) {
19	$menu_obj = false;
20
21	if ( is_object( $menu ) ) {
22		$menu_obj = $menu;
23	}
24
25	if ( $menu && ! $menu_obj ) {
26		$menu_obj = get_term( $menu, 'nav_menu' );
27
28		if ( ! $menu_obj ) {
29			$menu_obj = get_term_by( 'slug', $menu, 'nav_menu' );
30		}
31
32		if ( ! $menu_obj ) {
33			$menu_obj = get_term_by( 'name', $menu, 'nav_menu' );
34		}
35	}
36
37	if ( ! $menu_obj || is_wp_error( $menu_obj ) ) {
38		$menu_obj = false;
39	}
40
41	/**
42	 * Filters the nav_menu term retrieved for wp_get_nav_menu_object().
43	 *
44	 * @since 4.3.0
45	 *
46	 * @param WP_Term|false      $menu_obj Term from nav_menu taxonomy, or false if nothing had been found.
47	 * @param int|string|WP_Term $menu     The menu ID, slug, name, or object passed to wp_get_nav_menu_object().
48	 */
49	return apply_filters( 'wp_get_nav_menu_object', $menu_obj, $menu );
50}
51
52/**
53 * Check if the given ID is a navigation menu.
54 *
55 * Returns true if it is; false otherwise.
56 *
57 * @since 3.0.0
58 *
59 * @param int|string|WP_Term $menu Menu ID, slug, name, or object of menu to check.
60 * @return bool Whether the menu exists.
61 */
62function is_nav_menu( $menu ) {
63	if ( ! $menu ) {
64		return false;
65	}
66
67	$menu_obj = wp_get_nav_menu_object( $menu );
68
69	if (
70		$menu_obj &&
71		! is_wp_error( $menu_obj ) &&
72		! empty( $menu_obj->taxonomy ) &&
73		'nav_menu' === $menu_obj->taxonomy
74	) {
75		return true;
76	}
77
78	return false;
79}
80
81/**
82 * Registers navigation menu locations for a theme.
83 *
84 * @since 3.0.0
85 *
86 * @global array $_wp_registered_nav_menus
87 *
88 * @param string[] $locations Associative array of menu location identifiers (like a slug) and descriptive text.
89 */
90function register_nav_menus( $locations = array() ) {
91	global $_wp_registered_nav_menus;
92
93	add_theme_support( 'menus' );
94
95	foreach ( $locations as $key => $value ) {
96		if ( is_int( $key ) ) {
97			_doing_it_wrong( __FUNCTION__, __( 'Nav menu locations must be strings.' ), '5.3.0' );
98			break;
99		}
100	}
101
102	$_wp_registered_nav_menus = array_merge( (array) $_wp_registered_nav_menus, $locations );
103}
104
105/**
106 * Unregisters a navigation menu location for a theme.
107 *
108 * @since 3.1.0
109 *
110 * @global array $_wp_registered_nav_menus
111 *
112 * @param string $location The menu location identifier.
113 * @return bool True on success, false on failure.
114 */
115function unregister_nav_menu( $location ) {
116	global $_wp_registered_nav_menus;
117
118	if ( is_array( $_wp_registered_nav_menus ) && isset( $_wp_registered_nav_menus[ $location ] ) ) {
119		unset( $_wp_registered_nav_menus[ $location ] );
120		if ( empty( $_wp_registered_nav_menus ) ) {
121			_remove_theme_support( 'menus' );
122		}
123		return true;
124	}
125	return false;
126}
127
128/**
129 * Registers a navigation menu location for a theme.
130 *
131 * @since 3.0.0
132 *
133 * @param string $location    Menu location identifier, like a slug.
134 * @param string $description Menu location descriptive text.
135 */
136function register_nav_menu( $location, $description ) {
137	register_nav_menus( array( $location => $description ) );
138}
139/**
140 * Retrieves all registered navigation menu locations in a theme.
141 *
142 * @since 3.0.0
143 *
144 * @global array $_wp_registered_nav_menus
145 *
146 * @return string[] Associative array of egistered navigation menu descriptions keyed
147 *                  by their location. If none are registered, an empty array.
148 */
149function get_registered_nav_menus() {
150	global $_wp_registered_nav_menus;
151	if ( isset( $_wp_registered_nav_menus ) ) {
152		return $_wp_registered_nav_menus;
153	}
154	return array();
155}
156
157/**
158 * Retrieves all registered navigation menu locations and the menus assigned to them.
159 *
160 * @since 3.0.0
161 *
162 * @return int[] Associative array of egistered navigation menu IDs keyed by their
163 *               location name. If none are registered, an empty array.
164 */
165function get_nav_menu_locations() {
166	$locations = get_theme_mod( 'nav_menu_locations' );
167	return ( is_array( $locations ) ) ? $locations : array();
168}
169
170/**
171 * Determines whether a registered nav menu location has a menu assigned to it.
172 *
173 * @since 3.0.0
174 *
175 * @param string $location Menu location identifier.
176 * @return bool Whether location has a menu.
177 */
178function has_nav_menu( $location ) {
179	$has_nav_menu = false;
180
181	$registered_nav_menus = get_registered_nav_menus();
182	if ( isset( $registered_nav_menus[ $location ] ) ) {
183		$locations    = get_nav_menu_locations();
184		$has_nav_menu = ! empty( $locations[ $location ] );
185	}
186
187	/**
188	 * Filters whether a nav menu is assigned to the specified location.
189	 *
190	 * @since 4.3.0
191	 *
192	 * @param bool   $has_nav_menu Whether there is a menu assigned to a location.
193	 * @param string $location     Menu location.
194	 */
195	return apply_filters( 'has_nav_menu', $has_nav_menu, $location );
196}
197
198/**
199 * Returns the name of a navigation menu.
200 *
201 * @since 4.9.0
202 *
203 * @param string $location Menu location identifier.
204 * @return string Menu name.
205 */
206function wp_get_nav_menu_name( $location ) {
207	$menu_name = '';
208
209	$locations = get_nav_menu_locations();
210
211	if ( isset( $locations[ $location ] ) ) {
212		$menu = wp_get_nav_menu_object( $locations[ $location ] );
213
214		if ( $menu && $menu->name ) {
215			$menu_name = $menu->name;
216		}
217	}
218
219	/**
220	 * Filters the navigation menu name being returned.
221	 *
222	 * @since 4.9.0
223	 *
224	 * @param string $menu_name Menu name.
225	 * @param string $location  Menu location identifier.
226	 */
227	return apply_filters( 'wp_get_nav_menu_name', $menu_name, $location );
228}
229
230/**
231 * Determines whether the given ID is a nav menu item.
232 *
233 * @since 3.0.0
234 *
235 * @param int $menu_item_id The ID of the potential nav menu item.
236 * @return bool Whether the given ID is that of a nav menu item.
237 */
238function is_nav_menu_item( $menu_item_id = 0 ) {
239	return ( ! is_wp_error( $menu_item_id ) && ( 'nav_menu_item' === get_post_type( $menu_item_id ) ) );
240}
241
242/**
243 * Creates a navigation menu.
244 *
245 * Note that `$menu_name` is expected to be pre-slashed.
246 *
247 * @since 3.0.0
248 *
249 * @param string $menu_name Menu name.
250 * @return int|WP_Error Menu ID on success, WP_Error object on failure.
251 */
252function wp_create_nav_menu( $menu_name ) {
253	// expected_slashed ($menu_name)
254	return wp_update_nav_menu_object( 0, array( 'menu-name' => $menu_name ) );
255}
256
257/**
258 * Delete a Navigation Menu.
259 *
260 * @since 3.0.0
261 *
262 * @param int|string|WP_Term $menu Menu ID, slug, name, or object.
263 * @return bool|WP_Error True on success, false or WP_Error object on failure.
264 */
265function wp_delete_nav_menu( $menu ) {
266	$menu = wp_get_nav_menu_object( $menu );
267	if ( ! $menu ) {
268		return false;
269	}
270
271	$menu_objects = get_objects_in_term( $menu->term_id, 'nav_menu' );
272	if ( ! empty( $menu_objects ) ) {
273		foreach ( $menu_objects as $item ) {
274			wp_delete_post( $item );
275		}
276	}
277
278	$result = wp_delete_term( $menu->term_id, 'nav_menu' );
279
280	// Remove this menu from any locations.
281	$locations = get_nav_menu_locations();
282	foreach ( $locations as $location => $menu_id ) {
283		if ( $menu_id == $menu->term_id ) {
284			$locations[ $location ] = 0;
285		}
286	}
287	set_theme_mod( 'nav_menu_locations', $locations );
288
289	if ( $result && ! is_wp_error( $result ) ) {
290
291		/**
292		 * Fires after a navigation menu has been successfully deleted.
293		 *
294		 * @since 3.0.0
295		 *
296		 * @param int $term_id ID of the deleted menu.
297		 */
298		do_action( 'wp_delete_nav_menu', $menu->term_id );
299	}
300
301	return $result;
302}
303
304/**
305 * Save the properties of a menu or create a new menu with those properties.
306 *
307 * Note that `$menu_data` is expected to be pre-slashed.
308 *
309 * @since 3.0.0
310 *
311 * @param int   $menu_id   The ID of the menu or "0" to create a new menu.
312 * @param array $menu_data The array of menu data.
313 * @return int|WP_Error Menu ID on success, WP_Error object on failure.
314 */
315function wp_update_nav_menu_object( $menu_id = 0, $menu_data = array() ) {
316	// expected_slashed ($menu_data)
317	$menu_id = (int) $menu_id;
318
319	$_menu = wp_get_nav_menu_object( $menu_id );
320
321	$args = array(
322		'description' => ( isset( $menu_data['description'] ) ? $menu_data['description'] : '' ),
323		'name'        => ( isset( $menu_data['menu-name'] ) ? $menu_data['menu-name'] : '' ),
324		'parent'      => ( isset( $menu_data['parent'] ) ? (int) $menu_data['parent'] : 0 ),
325		'slug'        => null,
326	);
327
328	// Double-check that we're not going to have one menu take the name of another.
329	$_possible_existing = get_term_by( 'name', $menu_data['menu-name'], 'nav_menu' );
330	if (
331		$_possible_existing &&
332		! is_wp_error( $_possible_existing ) &&
333		isset( $_possible_existing->term_id ) &&
334		$_possible_existing->term_id != $menu_id
335	) {
336		return new WP_Error(
337			'menu_exists',
338			sprintf(
339				/* translators: %s: Menu name. */
340				__( 'The menu name %s conflicts with another menu name. Please try another.' ),
341				'<strong>' . esc_html( $menu_data['menu-name'] ) . '</strong>'
342			)
343		);
344	}
345
346	// Menu doesn't already exist, so create a new menu.
347	if ( ! $_menu || is_wp_error( $_menu ) ) {
348		$menu_exists = get_term_by( 'name', $menu_data['menu-name'], 'nav_menu' );
349
350		if ( $menu_exists ) {
351			return new WP_Error(
352				'menu_exists',
353				sprintf(
354					/* translators: %s: Menu name. */
355					__( 'The menu name %s conflicts with another menu name. Please try another.' ),
356					'<strong>' . esc_html( $menu_data['menu-name'] ) . '</strong>'
357				)
358			);
359		}
360
361		$_menu = wp_insert_term( $menu_data['menu-name'], 'nav_menu', $args );
362
363		if ( is_wp_error( $_menu ) ) {
364			return $_menu;
365		}
366
367		/**
368		 * Fires after a navigation menu is successfully created.
369		 *
370		 * @since 3.0.0
371		 *
372		 * @param int   $term_id   ID of the new menu.
373		 * @param array $menu_data An array of menu data.
374		 */
375		do_action( 'wp_create_nav_menu', $_menu['term_id'], $menu_data );
376
377		return (int) $_menu['term_id'];
378	}
379
380	if ( ! $_menu || ! isset( $_menu->term_id ) ) {
381		return 0;
382	}
383
384	$menu_id = (int) $_menu->term_id;
385
386	$update_response = wp_update_term( $menu_id, 'nav_menu', $args );
387
388	if ( is_wp_error( $update_response ) ) {
389		return $update_response;
390	}
391
392	$menu_id = (int) $update_response['term_id'];
393
394	/**
395	 * Fires after a navigation menu has been successfully updated.
396	 *
397	 * @since 3.0.0
398	 *
399	 * @param int   $menu_id   ID of the updated menu.
400	 * @param array $menu_data An array of menu data.
401	 */
402	do_action( 'wp_update_nav_menu', $menu_id, $menu_data );
403	return $menu_id;
404}
405
406/**
407 * Save the properties of a menu item or create a new one.
408 *
409 * The menu-item-title, menu-item-description, and menu-item-attr-title are expected
410 * to be pre-slashed since they are passed directly into `wp_insert_post()`.
411 *
412 * @since 3.0.0
413 *
414 * @param int   $menu_id         The ID of the menu. Required. If "0", makes the menu item a draft orphan.
415 * @param int   $menu_item_db_id The ID of the menu item. If "0", creates a new menu item.
416 * @param array $menu_item_data  The menu item's data.
417 * @return int|WP_Error The menu item's database ID or WP_Error object on failure.
418 */
419function wp_update_nav_menu_item( $menu_id = 0, $menu_item_db_id = 0, $menu_item_data = array() ) {
420	$menu_id         = (int) $menu_id;
421	$menu_item_db_id = (int) $menu_item_db_id;
422
423	// Make sure that we don't convert non-nav_menu_item objects into nav_menu_item objects.
424	if ( ! empty( $menu_item_db_id ) && ! is_nav_menu_item( $menu_item_db_id ) ) {
425		return new WP_Error( 'update_nav_menu_item_failed', __( 'The given object ID is not that of a menu item.' ) );
426	}
427
428	$menu = wp_get_nav_menu_object( $menu_id );
429
430	if ( ! $menu && 0 !== $menu_id ) {
431		return new WP_Error( 'invalid_menu_id', __( 'Invalid menu ID.' ) );
432	}
433
434	if ( is_wp_error( $menu ) ) {
435		return $menu;
436	}
437
438	$defaults = array(
439		'menu-item-db-id'         => $menu_item_db_id,
440		'menu-item-object-id'     => 0,
441		'menu-item-object'        => '',
442		'menu-item-parent-id'     => 0,
443		'menu-item-position'      => 0,
444		'menu-item-type'          => 'custom',
445		'menu-item-title'         => '',
446		'menu-item-url'           => '',
447		'menu-item-description'   => '',
448		'menu-item-attr-title'    => '',
449		'menu-item-target'        => '',
450		'menu-item-classes'       => '',
451		'menu-item-xfn'           => '',
452		'menu-item-status'        => '',
453		'menu-item-post-date'     => '',
454		'menu-item-post-date-gmt' => '',
455	);
456
457	$args = wp_parse_args( $menu_item_data, $defaults );
458
459	if ( 0 == $menu_id ) {
460		$args['menu-item-position'] = 1;
461	} elseif ( 0 == (int) $args['menu-item-position'] ) {
462		$menu_items                 = 0 == $menu_id ? array() : (array) wp_get_nav_menu_items( $menu_id, array( 'post_status' => 'publish,draft' ) );
463		$last_item                  = array_pop( $menu_items );
464		$args['menu-item-position'] = ( $last_item && isset( $last_item->menu_order ) ) ? 1 + $last_item->menu_order : count( $menu_items );
465	}
466
467	$original_parent = 0 < $menu_item_db_id ? get_post_field( 'post_parent', $menu_item_db_id ) : 0;
468
469	if ( 'custom' === $args['menu-item-type'] ) {
470		// If custom menu item, trim the URL.
471		$args['menu-item-url'] = trim( $args['menu-item-url'] );
472	} else {
473		/*
474		 * If non-custom menu item, then:
475		 * - use the original object's URL.
476		 * - blank default title to sync with the original object's title.
477		 */
478
479		$args['menu-item-url'] = '';
480
481		$original_title = '';
482		if ( 'taxonomy' === $args['menu-item-type'] ) {
483			$original_parent = get_term_field( 'parent', $args['menu-item-object-id'], $args['menu-item-object'], 'raw' );
484			$original_title  = get_term_field( 'name', $args['menu-item-object-id'], $args['menu-item-object'], 'raw' );
485		} elseif ( 'post_type' === $args['menu-item-type'] ) {
486
487			$original_object = get_post( $args['menu-item-object-id'] );
488			$original_parent = (int) $original_object->post_parent;
489			$original_title  = $original_object->post_title;
490		} elseif ( 'post_type_archive' === $args['menu-item-type'] ) {
491			$original_object = get_post_type_object( $args['menu-item-object'] );
492			if ( $original_object ) {
493				$original_title = $original_object->labels->archives;
494			}
495		}
496
497		if ( wp_unslash( $args['menu-item-title'] ) === wp_specialchars_decode( $original_title ) ) {
498			$args['menu-item-title'] = '';
499		}
500
501		// Hack to get wp to create a post object when too many properties are empty.
502		if ( '' === $args['menu-item-title'] && '' === $args['menu-item-description'] ) {
503			$args['menu-item-description'] = ' ';
504		}
505	}
506
507	// Populate the menu item object.
508	$post = array(
509		'menu_order'   => $args['menu-item-position'],
510		'ping_status'  => 0,
511		'post_content' => $args['menu-item-description'],
512		'post_excerpt' => $args['menu-item-attr-title'],
513		'post_parent'  => $original_parent,
514		'post_title'   => $args['menu-item-title'],
515		'post_type'    => 'nav_menu_item',
516	);
517
518	$post_date = wp_resolve_post_date( $args['menu-item-post-date'], $args['menu-item-post-date-gmt'] );
519	if ( $post_date ) {
520		$post['post_date'] = $post_date;
521	}
522
523	$update = 0 != $menu_item_db_id;
524
525	// New menu item. Default is draft status.
526	if ( ! $update ) {
527		$post['ID']          = 0;
528		$post['post_status'] = 'publish' === $args['menu-item-status'] ? 'publish' : 'draft';
529		$menu_item_db_id     = wp_insert_post( $post );
530		if ( ! $menu_item_db_id || is_wp_error( $menu_item_db_id ) ) {
531			return $menu_item_db_id;
532		}
533
534		/**
535		 * Fires immediately after a new navigation menu item has been added.
536		 *
537		 * @since 4.4.0
538		 *
539		 * @see wp_update_nav_menu_item()
540		 *
541		 * @param int   $menu_id         ID of the updated menu.
542		 * @param int   $menu_item_db_id ID of the new menu item.
543		 * @param array $args            An array of arguments used to update/add the menu item.
544		 */
545		do_action( 'wp_add_nav_menu_item', $menu_id, $menu_item_db_id, $args );
546	}
547
548	// Associate the menu item with the menu term.
549	// Only set the menu term if it isn't set to avoid unnecessary wp_get_object_terms().
550	if ( $menu_id && ( ! $update || ! is_object_in_term( $menu_item_db_id, 'nav_menu', (int) $menu->term_id ) ) ) {
551		wp_set_object_terms( $menu_item_db_id, array( $menu->term_id ), 'nav_menu' );
552	}
553
554	if ( 'custom' === $args['menu-item-type'] ) {
555		$args['menu-item-object-id'] = $menu_item_db_id;
556		$args['menu-item-object']    = 'custom';
557	}
558
559	$menu_item_db_id = (int) $menu_item_db_id;
560
561	update_post_meta( $menu_item_db_id, '_menu_item_type', sanitize_key( $args['menu-item-type'] ) );
562	update_post_meta( $menu_item_db_id, '_menu_item_menu_item_parent', (string) ( (int) $args['menu-item-parent-id'] ) );
563	update_post_meta( $menu_item_db_id, '_menu_item_object_id', (string) ( (int) $args['menu-item-object-id'] ) );
564	update_post_meta( $menu_item_db_id, '_menu_item_object', sanitize_key( $args['menu-item-object'] ) );
565	update_post_meta( $menu_item_db_id, '_menu_item_target', sanitize_key( $args['menu-item-target'] ) );
566
567	$args['menu-item-classes'] = array_map( 'sanitize_html_class', explode( ' ', $args['menu-item-classes'] ) );
568	$args['menu-item-xfn']     = implode( ' ', array_map( 'sanitize_html_class', explode( ' ', $args['menu-item-xfn'] ) ) );
569	update_post_meta( $menu_item_db_id, '_menu_item_classes', $args['menu-item-classes'] );
570	update_post_meta( $menu_item_db_id, '_menu_item_xfn', $args['menu-item-xfn'] );
571	update_post_meta( $menu_item_db_id, '_menu_item_url', esc_url_raw( $args['menu-item-url'] ) );
572
573	if ( 0 == $menu_id ) {
574		update_post_meta( $menu_item_db_id, '_menu_item_orphaned', (string) time() );
575	} elseif ( get_post_meta( $menu_item_db_id, '_menu_item_orphaned' ) ) {
576		delete_post_meta( $menu_item_db_id, '_menu_item_orphaned' );
577	}
578
579	// Update existing menu item. Default is publish status.
580	if ( $update ) {
581		$post['ID']          = $menu_item_db_id;
582		$post['post_status'] = ( 'draft' === $args['menu-item-status'] ) ? 'draft' : 'publish';
583		wp_update_post( $post );
584	}
585
586	/**
587	 * Fires after a navigation menu item has been updated.
588	 *
589	 * @since 3.0.0
590	 *
591	 * @see wp_update_nav_menu_item()
592	 *
593	 * @param int   $menu_id         ID of the updated menu.
594	 * @param int   $menu_item_db_id ID of the updated menu item.
595	 * @param array $args            An array of arguments used to update a menu item.
596	 */
597	do_action( 'wp_update_nav_menu_item', $menu_id, $menu_item_db_id, $args );
598
599	return $menu_item_db_id;
600}
601
602/**
603 * Returns all navigation menu objects.
604 *
605 * @since 3.0.0
606 * @since 4.1.0 Default value of the 'orderby' argument was changed from 'none'
607 *              to 'name'.
608 *
609 * @param array $args Optional. Array of arguments passed on to get_terms().
610 *                    Default empty array.
611 * @return WP_Term[] An array of menu objects.
612 */
613function wp_get_nav_menus( $args = array() ) {
614	$defaults = array(
615		'taxonomy'   => 'nav_menu',
616		'hide_empty' => false,
617		'orderby'    => 'name',
618	);
619	$args     = wp_parse_args( $args, $defaults );
620
621	/**
622	 * Filters the navigation menu objects being returned.
623	 *
624	 * @since 3.0.0
625	 *
626	 * @see get_terms()
627	 *
628	 * @param WP_Term[] $menus An array of menu objects.
629	 * @param array     $args  An array of arguments used to retrieve menu objects.
630	 */
631	return apply_filters( 'wp_get_nav_menus', get_terms( $args ), $args );
632}
633
634/**
635 * Return if a menu item is valid.
636 *
637 * @link https://core.trac.wordpress.org/ticket/13958
638 *
639 * @since 3.2.0
640 * @access private
641 *
642 * @param object $item The menu item to check.
643 * @return bool False if invalid, otherwise true.
644 */
645function _is_valid_nav_menu_item( $item ) {
646	return empty( $item->_invalid );
647}
648
649/**
650 * Retrieves all menu items of a navigation menu.
651 *
652 * Note: Most arguments passed to the `$args` parameter – save for 'output_key' – are
653 * specifically for retrieving nav_menu_item posts from get_posts() and may only
654 * indirectly affect the ultimate ordering and content of the resulting nav menu
655 * items that get returned from this function.
656 *
657 * @since 3.0.0
658 *
659 * @param int|string|WP_Term $menu Menu ID, slug, name, or object.
660 * @param array              $args {
661 *     Optional. Arguments to pass to get_posts().
662 *
663 *     @type string $order       How to order nav menu items as queried with get_posts(). Will be ignored
664 *                               if 'output' is ARRAY_A. Default 'ASC'.
665 *     @type string $orderby     Field to order menu items by as retrieved from get_posts(). Supply an orderby
666 *                               field via 'output_key' to affect the output order of nav menu items.
667 *                               Default 'menu_order'.
668 *     @type string $post_type   Menu items post type. Default 'nav_menu_item'.
669 *     @type string $post_status Menu items post status. Default 'publish'.
670 *     @type string $output      How to order outputted menu items. Default ARRAY_A.
671 *     @type string $output_key  Key to use for ordering the actual menu items that get returned. Note that
672 *                               that is not a get_posts() argument and will only affect output of menu items
673 *                               processed in this function. Default 'menu_order'.
674 *     @type bool   $nopaging    Whether to retrieve all menu items (true) or paginate (false). Default true.
675 * }
676 * @return array|false Array of menu items, otherwise false.
677 */
678function wp_get_nav_menu_items( $menu, $args = array() ) {
679	$menu = wp_get_nav_menu_object( $menu );
680
681	if ( ! $menu ) {
682		return false;
683	}
684
685	static $fetched = array();
686
687	$items = get_objects_in_term( $menu->term_id, 'nav_menu' );
688	if ( is_wp_error( $items ) ) {
689		return false;
690	}
691
692	$defaults        = array(
693		'order'       => 'ASC',
694		'orderby'     => 'menu_order',
695		'post_type'   => 'nav_menu_item',
696		'post_status' => 'publish',
697		'output'      => ARRAY_A,
698		'output_key'  => 'menu_order',
699		'nopaging'    => true,
700	);
701	$args            = wp_parse_args( $args, $defaults );
702	$args['include'] = $items;
703
704	if ( ! empty( $items ) ) {
705		$items = get_posts( $args );
706	} else {
707		$items = array();
708	}
709
710	// Get all posts and terms at once to prime the caches.
711	if ( empty( $fetched[ $menu->term_id ] ) && ! wp_using_ext_object_cache() ) {
712		$fetched[ $menu->term_id ] = true;
713		$posts                     = array();
714		$terms                     = array();
715		foreach ( $items as $item ) {
716			$object_id = get_post_meta( $item->ID, '_menu_item_object_id', true );
717			$object    = get_post_meta( $item->ID, '_menu_item_object', true );
718			$type      = get_post_meta( $item->ID, '_menu_item_type', true );
719
720			if ( 'post_type' === $type ) {
721				$posts[ $object ][] = $object_id;
722			} elseif ( 'taxonomy' === $type ) {
723				$terms[ $object ][] = $object_id;
724			}
725		}
726
727		if ( ! empty( $posts ) ) {
728			foreach ( array_keys( $posts ) as $post_type ) {
729				get_posts(
730					array(
731						'post__in'               => $posts[ $post_type ],
732						'post_type'              => $post_type,
733						'nopaging'               => true,
734						'update_post_term_cache' => false,
735					)
736				);
737			}
738		}
739		unset( $posts );
740
741		if ( ! empty( $terms ) ) {
742			foreach ( array_keys( $terms ) as $taxonomy ) {
743				get_terms(
744					array(
745						'taxonomy'     => $taxonomy,
746						'include'      => $terms[ $taxonomy ],
747						'hierarchical' => false,
748					)
749				);
750			}
751		}
752		unset( $terms );
753	}
754
755	$items = array_map( 'wp_setup_nav_menu_item', $items );
756
757	if ( ! is_admin() ) { // Remove invalid items only on front end.
758		$items = array_filter( $items, '_is_valid_nav_menu_item' );
759	}
760
761	if ( ARRAY_A === $args['output'] ) {
762		$items = wp_list_sort(
763			$items,
764			array(
765				$args['output_key'] => 'ASC',
766			)
767		);
768
769		$i = 1;
770
771		foreach ( $items as $k => $item ) {
772			$items[ $k ]->{$args['output_key']} = $i++;
773		}
774	}
775
776	/**
777	 * Filters the navigation menu items being returned.
778	 *
779	 * @since 3.0.0
780	 *
781	 * @param array  $items An array of menu item post objects.
782	 * @param object $menu  The menu object.
783	 * @param array  $args  An array of arguments used to retrieve menu item objects.
784	 */
785	return apply_filters( 'wp_get_nav_menu_items', $items, $menu, $args );
786}
787
788/**
789 * Decorates a menu item object with the shared navigation menu item properties.
790 *
791 * Properties:
792 * - ID:               The term_id if the menu item represents a taxonomy term.
793 * - attr_title:       The title attribute of the link element for this menu item.
794 * - classes:          The array of class attribute values for the link element of this menu item.
795 * - db_id:            The DB ID of this item as a nav_menu_item object, if it exists (0 if it doesn't exist).
796 * - description:      The description of this menu item.
797 * - menu_item_parent: The DB ID of the nav_menu_item that is this item's menu parent, if any. 0 otherwise.
798 * - object:           The type of object originally represented, such as 'category', 'post', or 'attachment'.
799 * - object_id:        The DB ID of the original object this menu item represents, e.g. ID for posts and term_id for categories.
800 * - post_parent:      The DB ID of the original object's parent object, if any (0 otherwise).
801 * - post_title:       A "no title" label if menu item represents a post that lacks a title.
802 * - target:           The target attribute of the link element for this menu item.
803 * - title:            The title of this menu item.
804 * - type:             The family of objects originally represented, such as 'post_type' or 'taxonomy'.
805 * - type_label:       The singular label used to describe this type of menu item.
806 * - url:              The URL to which this menu item points.
807 * - xfn:              The XFN relationship expressed in the link of this menu item.
808 * - _invalid:         Whether the menu item represents an object that no longer exists.
809 *
810 * @since 3.0.0
811 *
812 * @param object $menu_item The menu item to modify.
813 * @return object The menu item with standard menu item properties.
814 */
815function wp_setup_nav_menu_item( $menu_item ) {
816	if ( isset( $menu_item->post_type ) ) {
817		if ( 'nav_menu_item' === $menu_item->post_type ) {
818			$menu_item->db_id            = (int) $menu_item->ID;
819			$menu_item->menu_item_parent = ! isset( $menu_item->menu_item_parent ) ? get_post_meta( $menu_item->ID, '_menu_item_menu_item_parent', true ) : $menu_item->menu_item_parent;
820			$menu_item->object_id        = ! isset( $menu_item->object_id ) ? get_post_meta( $menu_item->ID, '_menu_item_object_id', true ) : $menu_item->object_id;
821			$menu_item->object           = ! isset( $menu_item->object ) ? get_post_meta( $menu_item->ID, '_menu_item_object', true ) : $menu_item->object;
822			$menu_item->type             = ! isset( $menu_item->type ) ? get_post_meta( $menu_item->ID, '_menu_item_type', true ) : $menu_item->type;
823
824			if ( 'post_type' === $menu_item->type ) {
825				$object = get_post_type_object( $menu_item->object );
826				if ( $object ) {
827					$menu_item->type_label = $object->labels->singular_name;
828					// Denote post states for special pages (only in the admin).
829					if ( function_exists( 'get_post_states' ) ) {
830						$menu_post   = get_post( $menu_item->object_id );
831						$post_states = get_post_states( $menu_post );
832						if ( $post_states ) {
833							$menu_item->type_label = wp_strip_all_tags( implode( ', ', $post_states ) );
834						}
835					}
836				} else {
837					$menu_item->type_label = $menu_item->object;
838					$menu_item->_invalid   = true;
839				}
840
841				if ( 'trash' === get_post_status( $menu_item->object_id ) ) {
842					$menu_item->_invalid = true;
843				}
844
845				$original_object = get_post( $menu_item->object_id );
846
847				if ( $original_object ) {
848					$menu_item->url = get_permalink( $original_object->ID );
849					/** This filter is documented in wp-includes/post-template.php */
850					$original_title = apply_filters( 'the_title', $original_object->post_title, $original_object->ID );
851				} else {
852					$menu_item->url      = '';
853					$original_title      = '';
854					$menu_item->_invalid = true;
855				}
856
857				if ( '' === $original_title ) {
858					/* translators: %d: ID of a post. */
859					$original_title = sprintf( __( '#%d (no title)' ), $menu_item->object_id );
860				}
861
862				$menu_item->title = ( '' === $menu_item->post_title ) ? $original_title : $menu_item->post_title;
863
864			} elseif ( 'post_type_archive' === $menu_item->type ) {
865				$object = get_post_type_object( $menu_item->object );
866				if ( $object ) {
867					$menu_item->title      = ( '' === $menu_item->post_title ) ? $object->labels->archives : $menu_item->post_title;
868					$post_type_description = $object->description;
869				} else {
870					$post_type_description = '';
871					$menu_item->_invalid   = true;
872				}
873
874				$menu_item->type_label = __( 'Post Type Archive' );
875				$post_content          = wp_trim_words( $menu_item->post_content, 200 );
876				$post_type_description = ( '' === $post_content ) ? $post_type_description : $post_content;
877				$menu_item->url        = get_post_type_archive_link( $menu_item->object );
878
879			} elseif ( 'taxonomy' === $menu_item->type ) {
880				$object = get_taxonomy( $menu_item->object );
881				if ( $object ) {
882					$menu_item->type_label = $object->labels->singular_name;
883				} else {
884					$menu_item->type_label = $menu_item->object;
885					$menu_item->_invalid   = true;
886				}
887
888				$original_object = get_term( (int) $menu_item->object_id, $menu_item->object );
889
890				if ( $original_object && ! is_wp_error( $original_object ) ) {
891					$menu_item->url = get_term_link( (int) $menu_item->object_id, $menu_item->object );
892					$original_title = $original_object->name;
893				} else {
894					$menu_item->url      = '';
895					$original_title      = '';
896					$menu_item->_invalid = true;
897				}
898
899				if ( '' === $original_title ) {
900					/* translators: %d: ID of a term. */
901					$original_title = sprintf( __( '#%d (no title)' ), $menu_item->object_id );
902				}
903
904				$menu_item->title = ( '' === $menu_item->post_title ) ? $original_title : $menu_item->post_title;
905
906			} else {
907				$menu_item->type_label = __( 'Custom Link' );
908				$menu_item->title      = $menu_item->post_title;
909				$menu_item->url        = ! isset( $menu_item->url ) ? get_post_meta( $menu_item->ID, '_menu_item_url', true ) : $menu_item->url;
910			}
911
912			$menu_item->target = ! isset( $menu_item->target ) ? get_post_meta( $menu_item->ID, '_menu_item_target', true ) : $menu_item->target;
913
914			/**
915			 * Filters a navigation menu item's title attribute.
916			 *
917			 * @since 3.0.0
918			 *
919			 * @param string $item_title The menu item title attribute.
920			 */
921			$menu_item->attr_title = ! isset( $menu_item->attr_title ) ? apply_filters( 'nav_menu_attr_title', $menu_item->post_excerpt ) : $menu_item->attr_title;
922
923			if ( ! isset( $menu_item->description ) ) {
924				/**
925				 * Filters a navigation menu item's description.
926				 *
927				 * @since 3.0.0
928				 *
929				 * @param string $description The menu item description.
930				 */
931				$menu_item->description = apply_filters( 'nav_menu_description', wp_trim_words( $menu_item->post_content, 200 ) );
932			}
933
934			$menu_item->classes = ! isset( $menu_item->classes ) ? (array) get_post_meta( $menu_item->ID, '_menu_item_classes', true ) : $menu_item->classes;
935			$menu_item->xfn     = ! isset( $menu_item->xfn ) ? get_post_meta( $menu_item->ID, '_menu_item_xfn', true ) : $menu_item->xfn;
936		} else {
937			$menu_item->db_id            = 0;
938			$menu_item->menu_item_parent = 0;
939			$menu_item->object_id        = (int) $menu_item->ID;
940			$menu_item->type             = 'post_type';
941
942			$object                = get_post_type_object( $menu_item->post_type );
943			$menu_item->object     = $object->name;
944			$menu_item->type_label = $object->labels->singular_name;
945
946			if ( '' === $menu_item->post_title ) {
947				/* translators: %d: ID of a post. */
948				$menu_item->post_title = sprintf( __( '#%d (no title)' ), $menu_item->ID );
949			}
950
951			$menu_item->title  = $menu_item->post_title;
952			$menu_item->url    = get_permalink( $menu_item->ID );
953			$menu_item->target = '';
954
955			/** This filter is documented in wp-includes/nav-menu.php */
956			$menu_item->attr_title = apply_filters( 'nav_menu_attr_title', '' );
957
958			/** This filter is documented in wp-includes/nav-menu.php */
959			$menu_item->description = apply_filters( 'nav_menu_description', '' );
960			$menu_item->classes     = array();
961			$menu_item->xfn         = '';
962		}
963	} elseif ( isset( $menu_item->taxonomy ) ) {
964		$menu_item->ID               = $menu_item->term_id;
965		$menu_item->db_id            = 0;
966		$menu_item->menu_item_parent = 0;
967		$menu_item->object_id        = (int) $menu_item->term_id;
968		$menu_item->post_parent      = (int) $menu_item->parent;
969		$menu_item->type             = 'taxonomy';
970
971		$object                = get_taxonomy( $menu_item->taxonomy );
972		$menu_item->object     = $object->name;
973		$menu_item->type_label = $object->labels->singular_name;
974
975		$menu_item->title       = $menu_item->name;
976		$menu_item->url         = get_term_link( $menu_item, $menu_item->taxonomy );
977		$menu_item->target      = '';
978		$menu_item->attr_title  = '';
979		$menu_item->description = get_term_field( 'description', $menu_item->term_id, $menu_item->taxonomy );
980		$menu_item->classes     = array();
981		$menu_item->xfn         = '';
982
983	}
984
985	/**
986	 * Filters a navigation menu item object.
987	 *
988	 * @since 3.0.0
989	 *
990	 * @param object $menu_item The menu item object.
991	 */
992	return apply_filters( 'wp_setup_nav_menu_item', $menu_item );
993}
994
995/**
996 * Get the menu items associated with a particular object.
997 *
998 * @since 3.0.0
999 *
1000 * @param int    $object_id   Optional. The ID of the original object. Default 0.
1001 * @param string $object_type Optional. The type of object, such as 'post_type' or 'taxonomy'.
1002 *                            Default 'post_type'.
1003 * @param string $taxonomy    Optional. If $object_type is 'taxonomy', $taxonomy is the name
1004 *                            of the tax that $object_id belongs to. Default empty.
1005 * @return int[] The array of menu item IDs; empty array if none.
1006 */
1007function wp_get_associated_nav_menu_items( $object_id = 0, $object_type = 'post_type', $taxonomy = '' ) {
1008	$object_id     = (int) $object_id;
1009	$menu_item_ids = array();
1010
1011	$query      = new WP_Query;
1012	$menu_items = $query->query(
1013		array(
1014			'meta_key'       => '_menu_item_object_id',
1015			'meta_value'     => $object_id,
1016			'post_status'    => 'any',
1017			'post_type'      => 'nav_menu_item',
1018			'posts_per_page' => -1,
1019		)
1020	);
1021	foreach ( (array) $menu_items as $menu_item ) {
1022		if ( isset( $menu_item->ID ) && is_nav_menu_item( $menu_item->ID ) ) {
1023			$menu_item_type = get_post_meta( $menu_item->ID, '_menu_item_type', true );
1024			if (
1025				'post_type' === $object_type &&
1026				'post_type' === $menu_item_type
1027			) {
1028				$menu_item_ids[] = (int) $menu_item->ID;
1029			} elseif (
1030				'taxonomy' === $object_type &&
1031				'taxonomy' === $menu_item_type &&
1032				get_post_meta( $menu_item->ID, '_menu_item_object', true ) == $taxonomy
1033			) {
1034				$menu_item_ids[] = (int) $menu_item->ID;
1035			}
1036		}
1037	}
1038
1039	return array_unique( $menu_item_ids );
1040}
1041
1042/**
1043 * Callback for handling a menu item when its original object is deleted.
1044 *
1045 * @since 3.0.0
1046 * @access private
1047 *
1048 * @param int $object_id The ID of the original object being trashed.
1049 */
1050function _wp_delete_post_menu_item( $object_id ) {
1051	$object_id = (int) $object_id;
1052
1053	$menu_item_ids = wp_get_associated_nav_menu_items( $object_id, 'post_type' );
1054
1055	foreach ( (array) $menu_item_ids as $menu_item_id ) {
1056		wp_delete_post( $menu_item_id, true );
1057	}
1058}
1059
1060/**
1061 * Serves as a callback for handling a menu item when its original object is deleted.
1062 *
1063 * @since 3.0.0
1064 * @access private
1065 *
1066 * @param int    $object_id The ID of the original object being trashed.
1067 * @param int    $tt_id     Term taxonomy ID. Unused.
1068 * @param string $taxonomy  Taxonomy slug.
1069 */
1070function _wp_delete_tax_menu_item( $object_id, $tt_id, $taxonomy ) {
1071	$object_id = (int) $object_id;
1072
1073	$menu_item_ids = wp_get_associated_nav_menu_items( $object_id, 'taxonomy', $taxonomy );
1074
1075	foreach ( (array) $menu_item_ids as $menu_item_id ) {
1076		wp_delete_post( $menu_item_id, true );
1077	}
1078}
1079
1080/**
1081 * Automatically add newly published page objects to menus with that as an option.
1082 *
1083 * @since 3.0.0
1084 * @access private
1085 *
1086 * @param string  $new_status The new status of the post object.
1087 * @param string  $old_status The old status of the post object.
1088 * @param WP_Post $post       The post object being transitioned from one status to another.
1089 */
1090function _wp_auto_add_pages_to_menu( $new_status, $old_status, $post ) {
1091	if ( 'publish' !== $new_status || 'publish' === $old_status || 'page' !== $post->post_type ) {
1092		return;
1093	}
1094	if ( ! empty( $post->post_parent ) ) {
1095		return;
1096	}
1097	$auto_add = get_option( 'nav_menu_options' );
1098	if ( empty( $auto_add ) || ! is_array( $auto_add ) || ! isset( $auto_add['auto_add'] ) ) {
1099		return;
1100	}
1101	$auto_add = $auto_add['auto_add'];
1102	if ( empty( $auto_add ) || ! is_array( $auto_add ) ) {
1103		return;
1104	}
1105
1106	$args = array(
1107		'menu-item-object-id' => $post->ID,
1108		'menu-item-object'    => $post->post_type,
1109		'menu-item-type'      => 'post_type',
1110		'menu-item-status'    => 'publish',
1111	);
1112
1113	foreach ( $auto_add as $menu_id ) {
1114		$items = wp_get_nav_menu_items( $menu_id, array( 'post_status' => 'publish,draft' ) );
1115		if ( ! is_array( $items ) ) {
1116			continue;
1117		}
1118		foreach ( $items as $item ) {
1119			if ( $post->ID == $item->object_id ) {
1120				continue 2;
1121			}
1122		}
1123		wp_update_nav_menu_item( $menu_id, 0, $args );
1124	}
1125}
1126
1127/**
1128 * Delete auto-draft posts associated with the supplied changeset.
1129 *
1130 * @since 4.8.0
1131 * @access private
1132 *
1133 * @param int $post_id Post ID for the customize_changeset.
1134 */
1135function _wp_delete_customize_changeset_dependent_auto_drafts( $post_id ) {
1136	$post = get_post( $post_id );
1137
1138	if ( ! $post || 'customize_changeset' !== $post->post_type ) {
1139		return;
1140	}
1141
1142	$data = json_decode( $post->post_content, true );
1143	if ( empty( $data['nav_menus_created_posts']['value'] ) ) {
1144		return;
1145	}
1146	remove_action( 'delete_post', '_wp_delete_customize_changeset_dependent_auto_drafts' );
1147	foreach ( $data['nav_menus_created_posts']['value'] as $stub_post_id ) {
1148		if ( empty( $stub_post_id ) ) {
1149			continue;
1150		}
1151		if ( 'auto-draft' === get_post_status( $stub_post_id ) ) {
1152			wp_delete_post( $stub_post_id, true );
1153		} elseif ( 'draft' === get_post_status( $stub_post_id ) ) {
1154			wp_trash_post( $stub_post_id );
1155			delete_post_meta( $stub_post_id, '_customize_changeset_uuid' );
1156		}
1157	}
1158	add_action( 'delete_post', '_wp_delete_customize_changeset_dependent_auto_drafts' );
1159}
1160
1161/**
1162 * Handle menu config after theme change.
1163 *
1164 * @access private
1165 * @since 4.9.0
1166 */
1167function _wp_menus_changed() {
1168	$old_nav_menu_locations    = get_option( 'theme_switch_menu_locations', array() );
1169	$new_nav_menu_locations    = get_nav_menu_locations();
1170	$mapped_nav_menu_locations = wp_map_nav_menu_locations( $new_nav_menu_locations, $old_nav_menu_locations );
1171
1172	set_theme_mod( 'nav_menu_locations', $mapped_nav_menu_locations );
1173	delete_option( 'theme_switch_menu_locations' );
1174}
1175
1176/**
1177 * Maps nav menu locations according to assignments in previously active theme.
1178 *
1179 * @since 4.9.0
1180 *
1181 * @param array $new_nav_menu_locations New nav menu locations assignments.
1182 * @param array $old_nav_menu_locations Old nav menu locations assignments.
1183 * @return array Nav menus mapped to new nav menu locations.
1184 */
1185function wp_map_nav_menu_locations( $new_nav_menu_locations, $old_nav_menu_locations ) {
1186	$registered_nav_menus   = get_registered_nav_menus();
1187	$new_nav_menu_locations = array_intersect_key( $new_nav_menu_locations, $registered_nav_menus );
1188
1189	// Short-circuit if there are no old nav menu location assignments to map.
1190	if ( empty( $old_nav_menu_locations ) ) {
1191		return $new_nav_menu_locations;
1192	}
1193
1194	// If old and new theme have just one location, map it and we're done.
1195	if ( 1 === count( $old_nav_menu_locations ) && 1 === count( $registered_nav_menus ) ) {
1196		$new_nav_menu_locations[ key( $registered_nav_menus ) ] = array_pop( $old_nav_menu_locations );
1197		return $new_nav_menu_locations;
1198	}
1199
1200	$old_locations = array_keys( $old_nav_menu_locations );
1201
1202	// Map locations with the same slug.
1203	foreach ( $registered_nav_menus as $location => $name ) {
1204		if ( in_array( $location, $old_locations, true ) ) {
1205			$new_nav_menu_locations[ $location ] = $old_nav_menu_locations[ $location ];
1206			unset( $old_nav_menu_locations[ $location ] );
1207		}
1208	}
1209
1210	// If there are no old nav menu locations left, then we're done.
1211	if ( empty( $old_nav_menu_locations ) ) {
1212		return $new_nav_menu_locations;
1213	}
1214
1215	/*
1216	 * If old and new theme both have locations that contain phrases
1217	 * from within the same group, make an educated guess and map it.
1218	 */
1219	$common_slug_groups = array(
1220		array( 'primary', 'menu-1', 'main', 'header', 'navigation', 'top' ),
1221		array( 'secondary', 'menu-2', 'footer', 'subsidiary', 'bottom' ),
1222		array( 'social' ),
1223	);
1224
1225	// Go through each group...
1226	foreach ( $common_slug_groups as $slug_group ) {
1227
1228		// ...and see if any of these slugs...
1229		foreach ( $slug_group as $slug ) {
1230
1231			// ...and any of the new menu locations...
1232			foreach ( $registered_nav_menus as $new_location => $name ) {
1233
1234				// ...actually match!
1235				if ( is_string( $new_location ) && false === stripos( $new_location, $slug ) && false === stripos( $slug, $new_location ) ) {
1236					continue;
1237				} elseif ( is_numeric( $new_location ) && $new_location !== $slug ) {
1238					continue;
1239				}
1240
1241				// Then see if any of the old locations...
1242				foreach ( $old_nav_menu_locations as $location => $menu_id ) {
1243
1244					// ...and any slug in the same group...
1245					foreach ( $slug_group as $slug ) {
1246
1247						// ... have a match as well.
1248						if ( is_string( $location ) && false === stripos( $location, $slug ) && false === stripos( $slug, $location ) ) {
1249							continue;
1250						} elseif ( is_numeric( $location ) && $location !== $slug ) {
1251							continue;
1252						}
1253
1254						// Make sure this location wasn't mapped and removed previously.
1255						if ( ! empty( $old_nav_menu_locations[ $location ] ) ) {
1256
1257							// We have a match that can be mapped!
1258							$new_nav_menu_locations[ $new_location ] = $old_nav_menu_locations[ $location ];
1259
1260							// Remove the mapped location so it can't be mapped again.
1261							unset( $old_nav_menu_locations[ $location ] );
1262
1263							// Go back and check the next new menu location.
1264							continue 3;
1265						}
1266					} // End foreach ( $slug_group as $slug ).
1267				} // End foreach ( $old_nav_menu_locations as $location => $menu_id ).
1268			} // End foreach foreach ( $registered_nav_menus as $new_location => $name ).
1269		} // End foreach ( $slug_group as $slug ).
1270	} // End foreach ( $common_slug_groups as $slug_group ).
1271
1272	return $new_nav_menu_locations;
1273}
1274