: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* WordPress Customize Nav Menus classes
* Customize Nav Menus class.
* Implements menu management in the Customizer.
* @see WP_Customize_Manager
#[AllowDynamicProperties]
final class WP_Customize_Nav_Menus {
* WP_Customize_Manager instance.
* @var WP_Customize_Manager
* Original nav menu locations before the theme was switched.
protected $original_nav_menu_locations;
* @param WP_Customize_Manager $manager Customizer bootstrap instance.
public function __construct( $manager ) {
$this->manager = $manager;
$this->original_nav_menu_locations = get_nav_menu_locations();
// See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L469-L499
add_action( 'customize_register', array( $this, 'customize_register' ), 11 );
add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_dynamic_setting_args' ), 10, 2 );
add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_dynamic_setting_class' ), 10, 3 );
add_action( 'customize_save_nav_menus_created_posts', array( $this, 'save_nav_menus_created_posts' ) );
// Skip remaining hooks when the user can't manage nav menus anyway.
if ( ! current_user_can( 'edit_theme_options' ) ) {
add_filter( 'customize_refresh_nonces', array( $this, 'filter_nonces' ) );
add_action( 'wp_ajax_load-available-menu-items-customizer', array( $this, 'ajax_load_available_items' ) );
add_action( 'wp_ajax_search-available-menu-items-customizer', array( $this, 'ajax_search_available_items' ) );
add_action( 'wp_ajax_customize-nav-menus-insert-auto-draft', array( $this, 'ajax_insert_auto_draft_post' ) );
add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_templates' ) );
add_action( 'customize_controls_print_footer_scripts', array( $this, 'available_items_template' ) );
add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
add_action( 'customize_preview_init', array( $this, 'make_auto_draft_status_previewable' ) );
// Selective Refresh partials.
add_filter( 'customize_dynamic_partial_args', array( $this, 'customize_dynamic_partial_args' ), 10, 2 );
* Adds a nonce for customizing menus.
* @param string[] $nonces Array of nonces.
* @return string[] Modified array of nonces.
public function filter_nonces( $nonces ) {
$nonces['customize-menus'] = wp_create_nonce( 'customize-menus' );
* Ajax handler for loading available menu items.
public function ajax_load_available_items() {
check_ajax_referer( 'customize-menus', 'customize-menus-nonce' );
if ( ! current_user_can( 'edit_theme_options' ) ) {
if ( isset( $_POST['item_types'] ) && is_array( $_POST['item_types'] ) ) {
$item_types = wp_unslash( $_POST['item_types'] );
} elseif ( isset( $_POST['type'] ) && isset( $_POST['object'] ) ) { // Back compat.
'type' => wp_unslash( $_POST['type'] ),
'object' => wp_unslash( $_POST['object'] ),
'page' => empty( $_POST['page'] ) ? 0 : absint( $_POST['page'] ),
wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' );
foreach ( $item_types as $item_type ) {
if ( empty( $item_type['type'] ) || empty( $item_type['object'] ) ) {
wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' );
$type = sanitize_key( $item_type['type'] );
$object = sanitize_key( $item_type['object'] );
$page = empty( $item_type['page'] ) ? 0 : absint( $item_type['page'] );
$items = $this->load_available_items_query( $type, $object, $page );
if ( is_wp_error( $items ) ) {
wp_send_json_error( $items->get_error_code() );
$all_items[ $item_type['type'] . ':' . $item_type['object'] ] = $items;
wp_send_json_success( array( 'items' => $all_items ) );
* Performs the post_type and taxonomy queries for loading available menu items.
* @param string $object_type Optional. Accepts any custom object type and has built-in support for
* 'post_type' and 'taxonomy'. Default is 'post_type'.
* @param string $object_name Optional. Accepts any registered taxonomy or post type name. Default is 'page'.
* @param int $page Optional. The page number used to generate the query offset. Default is '0'.
* @return array|WP_Error An array of menu items on success, a WP_Error object on failure.
public function load_available_items_query( $object_type = 'post_type', $object_name = 'page', $page = 0 ) {
if ( 'post_type' === $object_type ) {
$post_type = get_post_type_object( $object_name );
return new WP_Error( 'nav_menus_invalid_post_type' );
* If we're dealing with pages, let's prioritize the Front Page,
* Posts Page and Privacy Policy Page at the top of the list.
$important_pages = array();
$suppress_page_ids = array();
if ( 0 === $page && 'page' === $object_name ) {
// Insert Front Page or custom "Home" link.
$front_page = 'page' === get_option( 'show_on_front' ) ? (int) get_option( 'page_on_front' ) : 0;
if ( ! empty( $front_page ) ) {
$front_page_obj = get_post( $front_page );
$important_pages[] = $front_page_obj;
$suppress_page_ids[] = $front_page_obj->ID;
// Add "Home" link. Treat as a page, but switch to custom on add.
'title' => _x( 'Home', 'nav menu home label' ),
'type_label' => __( 'Custom Link' ),
$posts_page = 'page' === get_option( 'show_on_front' ) ? (int) get_option( 'page_for_posts' ) : 0;
if ( ! empty( $posts_page ) ) {
$posts_page_obj = get_post( $posts_page );
$important_pages[] = $posts_page_obj;
$suppress_page_ids[] = $posts_page_obj->ID;
// Insert Privacy Policy Page.
$privacy_policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' );
if ( ! empty( $privacy_policy_page_id ) ) {
$privacy_policy_page = get_post( $privacy_policy_page_id );
if ( $privacy_policy_page instanceof WP_Post && 'publish' === $privacy_policy_page->post_status ) {
$important_pages[] = $privacy_policy_page;
$suppress_page_ids[] = $privacy_policy_page->ID;
} elseif ( 'post' !== $object_name && 0 === $page && $post_type->has_archive ) {
// Add a post type archive link.
$title = $post_type->labels->archives;
'id' => $object_name . '-archive',
'original_title' => $title,
'type' => 'post_type_archive',
'type_label' => __( 'Post Type Archive' ),
'object' => $object_name,
'url' => get_post_type_archive_link( $object_name ),
// Prepend posts with nav_menus_created_posts on first page.
if ( 0 === $page && $this->manager->get_setting( 'nav_menus_created_posts' ) ) {
foreach ( $this->manager->get_setting( 'nav_menus_created_posts' )->value() as $post_id ) {
$auto_draft_post = get_post( $post_id );
if ( $post_type->name === $auto_draft_post->post_type ) {
$posts[] = $auto_draft_post;
'post_type' => $object_name,
// Add suppression array to arguments for get_posts.
if ( ! empty( $suppress_page_ids ) ) {
$args['post__not_in'] = $suppress_page_ids;
foreach ( $posts as $post ) {
$post_title = $post->post_title;
if ( '' === $post_title ) {
/* translators: %d: ID of a post. */
$post_title = sprintf( __( '#%d (no title)' ), $post->ID );
$post_type_label = get_post_type_object( $post->post_type )->labels->singular_name;
$post_states = get_post_states( $post );
if ( ! empty( $post_states ) ) {
$post_type_label = implode( ',', $post_states );
$title = html_entity_decode( $post_title, ENT_QUOTES, get_bloginfo( 'charset' ) );
'id' => "post-{$post->ID}",
'original_title' => $title,
'type_label' => $post_type_label,
'object' => $post->post_type,
'object_id' => (int) $post->ID,
'url' => get_permalink( (int) $post->ID ),
} elseif ( 'taxonomy' === $object_type ) {
'taxonomy' => $object_name,
if ( is_wp_error( $terms ) ) {
foreach ( $terms as $term ) {
$title = html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) );
'id' => "term-{$term->term_id}",
'original_title' => $title,
'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name,
'object' => $term->taxonomy,
'object_id' => (int) $term->term_id,
'url' => get_term_link( (int) $term->term_id, $term->taxonomy ),
* Filters the available menu items.
* @param array $items The array of menu items.
* @param string $object_type The object type.
* @param string $object_name The object name.
* @param int $page The current page number.
$items = apply_filters( 'customize_nav_menu_available_items', $items, $object_type, $object_name, $page );
* Ajax handler for searching available menu items.
public function ajax_search_available_items() {
check_ajax_referer( 'customize-menus', 'customize-menus-nonce' );
if ( ! current_user_can( 'edit_theme_options' ) ) {
if ( empty( $_POST['search'] ) ) {
wp_send_json_error( 'nav_menus_missing_search_parameter' );
$p = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0;
$s = sanitize_text_field( wp_unslash( $_POST['search'] ) );
$items = $this->search_available_items_query(
wp_send_json_error( array( 'message' => __( 'No results found.' ) ) );
wp_send_json_success( array( 'items' => $items ) );
* Performs post queries for available-item searching.
* Based on WP_Editor::wp_link_query().
* @param array $args Optional. Accepts 'pagenum' and 's' (search) arguments.
* @return array Menu items.
public function search_available_items_query( $args = array() ) {
$post_type_objects = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
'post_type' => array_keys( $post_type_objects ),
'suppress_filters' => true,
'update_post_term_cache' => false,
'update_post_meta_cache' => false,
'post_status' => 'publish',
$args['pagenum'] = isset( $args['pagenum'] ) ? absint( $args['pagenum'] ) : 1;
$query['offset'] = $args['pagenum'] > 1 ? $query['posts_per_page'] * ( $args['pagenum'] - 1 ) : 0;
if ( isset( $args['s'] ) ) {
$query['s'] = $args['s'];
// Prepend list of posts with nav_menus_created_posts search results on first page.
$nav_menus_created_posts_setting = $this->manager->get_setting( 'nav_menus_created_posts' );
if ( 1 === $args['pagenum'] && $nav_menus_created_posts_setting && count( $nav_menus_created_posts_setting->value() ) > 0 ) {
$stub_post_query = new WP_Query(
'post_status' => 'auto-draft',
'post__in' => $nav_menus_created_posts_setting->value(),
$posts = array_merge( $posts, $stub_post_query->posts );
$get_posts = new WP_Query( $query );
$posts = array_merge( $posts, $get_posts->posts );
// Create items for posts.
foreach ( $posts as $post ) {
$post_title = $post->post_title;
if ( '' === $post_title ) {
/* translators: %d: ID of a post. */
$post_title = sprintf( __( '#%d (no title)' ), $post->ID );
$post_type_label = $post_type_objects[ $post->post_type ]->labels->singular_name;
$post_states = get_post_states( $post );
if ( ! empty( $post_states ) ) {
$post_type_label = implode( ',', $post_states );
'id' => 'post-' . $post->ID,
'title' => html_entity_decode( $post_title, ENT_QUOTES, get_bloginfo( 'charset' ) ),
'type_label' => $post_type_label,
'object' => $post->post_type,
'object_id' => (int) $post->ID,
'url' => get_permalink( (int) $post->ID ),
$taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'names' );
'taxonomies' => $taxonomies,
'name__like' => $args['s'],
'offset' => 20 * ( $args['pagenum'] - 1 ),
// Check if any taxonomies were found.
if ( ! empty( $terms ) ) {
foreach ( $terms as $term ) {
'id' => 'term-' . $term->term_id,
'title' => html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) ),
'type_label' => get_taxonomy( $term->taxonomy )->labels->singular_name,
'object' => $term->taxonomy,
'object_id' => (int) $term->term_id,
'url' => get_term_link( (int) $term->term_id, $term->taxonomy ),
// Add "Home" link if search term matches. Treat as a page, but switch to custom on add.
if ( isset( $args['s'] ) ) {
// Only insert custom "Home" link if there's no Front Page
$front_page = 'page' === get_option( 'show_on_front' ) ? (int) get_option( 'page_on_front' ) : 0;
if ( empty( $front_page ) ) {
$title = _x( 'Home', 'nav menu home label' );
$matches = function_exists( 'mb_stripos' ) ? false !== mb_stripos( $title, $args['s'] ) : false !== stripos( $title, $args['s'] );
'type_label' => __( 'Custom Link' ),
* Filters the available menu items during a search request.
* @param array $items The array of menu items.
* @param array $args Includes 'pagenum' and 's' (search) arguments.
$items = apply_filters( 'customize_nav_menu_searched_items', $items, $args );
* Enqueues scripts and styles for Customizer pane.
public function enqueue_scripts() {
wp_enqueue_style( 'customize-nav-menus' );
wp_enqueue_script( 'customize-nav-menus' );
$temp_nav_menu_setting = new WP_Customize_Nav_Menu_Setting( $this->manager, 'nav_menu[-1]' );
$temp_nav_menu_item_setting = new WP_Customize_Nav_Menu_Item_Setting( $this->manager, 'nav_menu_item[-1]' );
$num_locations = count( get_registered_nav_menus() );
if ( 1 === $num_locations ) {
$locations_description = __( 'Your theme can display menus in one location.' );